Merge branch 'new-ui--main-screens--v1' into 'master'

Initial UI for new main screens

See merge request !433
This commit is contained in:
Peter Serwylo 2017-03-07 08:59:29 +00:00
commit d67f23b60c
68 changed files with 3165 additions and 296 deletions

View File

@ -1,4 +1,4 @@
image: fdroid/ci:client-20161223 image: registry.gitlab.com/fdroid/ci-images:client
cache: cache:
paths: paths:

View File

@ -17,14 +17,21 @@ repositories {
jcenter() jcenter()
} }
ext {
supportLibVersion = '25.0.1'
}
dependencies { dependencies {
compile 'com.android.support:support-v4:24.2.1' compile "com.android.support:support-v4:${supportLibVersion}"
compile 'com.android.support:appcompat-v7:24.2.1' compile "com.android.support:appcompat-v7:${supportLibVersion}"
compile 'com.android.support:gridlayout-v7:24.2.1' compile "com.android.support:gridlayout-v7:${supportLibVersion}"
compile 'com.android.support:support-annotations:24.2.1' compile "com.android.support:support-annotations:${supportLibVersion}"
compile 'com.android.support:design:24.2.1' compile "com.android.support:recyclerview-v7:${supportLibVersion}"
compile 'com.android.support:cardview-v7:24.2.1' compile "com.android.support:cardview-v7:${supportLibVersion}"
compile "com.android.support:recyclerview-v7:24.2.1" compile "com.android.support:design:${supportLibVersion}"
compile "com.android.support:support-vector-drawable:${supportLibVersion}"
compile 'com.android.support.constraint:constraint-layout:1.0.1'
compile "com.android.support:palette-v7:${supportLibVersion}"
compile 'com.nostra13.universalimageloader:universal-image-loader:1.9.5' compile 'com.nostra13.universalimageloader:universal-image-loader:1.9.5'
compile 'com.google.zxing:core:3.2.1' compile 'com.google.zxing:core:3.2.1'
@ -50,7 +57,7 @@ dependencies {
testCompile "org.mockito:mockito-core:1.10.19" testCompile "org.mockito:mockito-core:1.10.19"
androidTestCompile 'com.android.support:support-annotations:24.2.1' androidTestCompile "com.android.support:support-annotations:${supportLibVersion}"
androidTestCompile 'com.android.support.test:runner:0.5' androidTestCompile 'com.android.support.test:runner:0.5'
androidTestCompile 'com.android.support.test:rules:0.5' androidTestCompile 'com.android.support.test:rules:0.5'
} }
@ -94,20 +101,24 @@ if (!hasProperty('sourceDeps')) {
'ch.acra:acra:d2762968c448757a7d6acc9f141881d9632f664988e9723ece33b5f7c79f3bc9', 'ch.acra:acra:d2762968c448757a7d6acc9f141881d9632f664988e9723ece33b5f7c79f3bc9',
'commons-io:commons-io:a10418348d234968600ccb1d988efcbbd08716e1d96936ccc1880e7d22513474', 'commons-io:commons-io:a10418348d234968600ccb1d988efcbbd08716e1d96936ccc1880e7d22513474',
'commons-net:commons-net:c25b0da668b3c5649f002d504def22d1b4cb30d206f05428d2fe168fa1a901c2', 'commons-net:commons-net:c25b0da668b3c5649f002d504def22d1b4cb30d206f05428d2fe168fa1a901c2',
'com.android.support:animated-vector-drawable:5aa30f578e1daefb26bef0ce06414266fbb4cdf5d4259f42a92c7bd83dcd81b4', 'com.android.support.constraint:constraint-layout-solver:d03a406eb505dfa673b0087bf17e16d5a4d6bf8afdf452ee175e346207948cdf',
'com.android.support:appcompat-v7:ead7ac8011fb40676df8adc2856cae934edab55fc4444654c0ac6ea443736088', 'com.android.support.constraint:constraint-layout:df1add69d11063eebba521818d63537b22207376b65f30cc35feea172b84e300',
'com.android.support:support-annotations:1e4d471c5378b283d95abfb128e7ed3c6b3cb19bb6f0c317a9b75e48e99365ff', 'com.android.support:animated-vector-drawable:70443a2857f9968c4e2c27c107657ce2291d774f8a50f03444e12ab637451175',
'com.android.support:support-compat:8e4fe0078b68073e8f5bcb52aa5b6407fd456d47c51aa0f8e8d1e23c69da06c1', 'com.android.support:appcompat-v7:7fead560a22ea4b15848ce3000f312ef611fac0953bf90ca8710a72a1f6e36ea',
'com.android.support:support-core-ui:ecc9184b7f438980e1c4a08b089d62dbc53ff90091f442d91fec27322a02c73c', 'com.android.support:cardview-v7:50d88fae8cd1076cb90504d36ca5ee9df4018555c8f041bd28f43274c0fc9e1f',
'com.android.support:support-core-utils:0fbc508e41dd6e8c634f310ee88452aaf8f48b6a843a369b115130b80d2fc05f', 'com.android.support:design:07a72eb68c888b38d7b78e450e1af8a84e571406e0cf911889e0645d5a41f1e4',
'com.android.support:support-fragment:d8030f0bf0f64214a29dc4e14d5ccd225e59f66ed15eb37f3a5022e773dd1fda', 'com.android.support:gridlayout-v7:cc11d2a3ee484e078c358a51d23a37e4bfbc542de410cacf275eafc5624bb888',
'com.android.support:support-media-compat:fa29a23eadd685631584b2c0c624a36e3bb79a33e257b00304501ad682fa2be3', 'com.android.support:palette-v7:89700afeedd988b471f0ce528ba916f368f549b47889b86b84d68eee42ea487c',
'com.android.support:support-v4:cac2956f5c4bb363cc0ba824ac16ea2a687d1c305d434416a34772a5f9375ed7', 'com.android.support:recyclerview-v7:803baba7be537ace8c5cb8a775e37547c22a04c4b028833796c45c26ec1deca2',
'com.android.support:support-vector-drawable:6ee37a7f7b93c1df1294e6f6f97df3724ac989fcda0549faf677001085330548', 'com.android.support:support-annotations:bd94ab42c841db16fb480f4c65d33d297e544655ecc498b37c5cf33a0c5f1968',
'com.android.support:design:89842bb1243507fe3079066ea4ea58795effe69cdf9a819e05274d21760adfc2', 'com.android.support:support-compat:d04f15aa5f2ae9e8cb7d025bf02dfd4fd6f6800628ceb107e0589634c9e4e537',
'com.android.support:cardview-v7:2303b351686d1db060b5fcf1a9c709c79b4a54a85bfda0fb3c4849e244606ee1', 'com.android.support:support-core-ui:29205ac978a1839d92be3d32db2385dac10f8688bba649e51650023c76de2f00',
'com.android.support:gridlayout-v7:1a31c248d69faa815cc155883ddcb0ccc7ba8e14e69ec58dd18d8017e23d76f5', 'com.android.support:support-core-utils:632c3750bd991da8b591f24a8916e74ca6063ae7f525f005c96981725c9bf491',
'com.android.support:recyclerview-v7:9077766a1a0f4e89528fbf9dcdf6d5880a8686f0266fa852d58d803beeef18fa', 'com.android.support:support-fragment:da47261a1d7c3d33e6e911335a7f4ce01135923bb221d3ab84625d005fa1969f',
'com.android.support:support-media-compat:01cac57af687bed9a6cb4ce803bebd1b7e6b8469c14f1f9ac6b4596637ff73d6',
'com.android.support:support-v4:50da261acc4ca3d2dea9a43106bf65488711ca97b20a4daa095dba381c205c98',
'com.android.support:support-vector-drawable:071ae3695bf8427d3cbfc8791492a3d9c804a4b111aa2a72fbfe7790ea268e5d',
'com.android.support:transition:9fd1e6d27cb70b3c5cd19f842b48bbb05cb4e5c93a22372769c342523393e8ea',
'com.google.zxing:core:b4d82452e7a6bf6ec2698904b332431717ed8f9a850224f295aec89de80f2259', 'com.google.zxing:core:b4d82452e7a6bf6ec2698904b332431717ed8f9a850224f295aec89de80f2259',
'com.madgag.spongycastle:core:9b6b7ac856b91bcda2ede694eccd26cefb0bf0b09b89f13cda05b5da5ff68c6b', 'com.madgag.spongycastle:core:9b6b7ac856b91bcda2ede694eccd26cefb0bf0b09b89f13cda05b5da5ff68c6b',
'com.madgag.spongycastle:pkix:6aba9b2210907a3d46dd3dcac782bb3424185290468d102d5207ebdc9796a905', 'com.madgag.spongycastle:pkix:6aba9b2210907a3d46dd3dcac782bb3424185290468d102d5207ebdc9796a905',

View File

@ -137,12 +137,6 @@
android:configChanges="layoutDirection|locale|keyboardHidden|orientation|screenSize" > android:configChanges="layoutDirection|locale|keyboardHidden|orientation|screenSize" >
<!-- App URLs --> <!-- App URLs -->
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<intent-filter> <intent-filter>
<action android:name="android.intent.action.VIEW" /> <action android:name="android.intent.action.VIEW" />
@ -407,7 +401,6 @@
android:label="@string/app_details" android:label="@string/app_details"
android:exported="true" android:exported="true"
android:parentActivityName=".FDroid" android:parentActivityName=".FDroid"
android:theme="@style/AppThemeLight.NoActionBar"
android:configChanges="layoutDirection|locale" > android:configChanges="layoutDirection|locale" >
<meta-data <meta-data
android:name="android.support.PARENT_ACTIVITY" android:name="android.support.PARENT_ACTIVITY"
@ -506,6 +499,17 @@
<service <service
android:name=".data.InstalledAppProviderService" android:name=".data.InstalledAppProviderService"
android:exported="false" /> android:exported="false" />
<activity android:name=".views.main.MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<activity android:name=".views.apps.AppListActivity" />
</application> </application>
</manifest> </manifest>

View File

@ -16,7 +16,6 @@ import android.support.design.widget.CoordinatorLayout;
import android.support.v4.content.LocalBroadcastManager; import android.support.v4.content.LocalBroadcastManager;
import android.support.v7.app.AlertDialog; import android.support.v7.app.AlertDialog;
import android.support.v7.app.AppCompatActivity; import android.support.v7.app.AppCompatActivity;
import android.support.v7.app.AppCompatDelegate;
import android.support.v7.widget.LinearLayoutManager; import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView; import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.Toolbar; import android.support.v7.widget.Toolbar;
@ -47,9 +46,6 @@ import org.fdroid.fdroid.views.AppDetailsRecyclerViewAdapter;
import org.fdroid.fdroid.views.ShareChooserDialog; import org.fdroid.fdroid.views.ShareChooserDialog;
public class AppDetails2 extends AppCompatActivity implements ShareChooserDialog.ShareChooserDialogListener, AppDetailsRecyclerViewAdapter.AppDetailsRecyclerViewAdapterCallbacks { public class AppDetails2 extends AppCompatActivity implements ShareChooserDialog.ShareChooserDialogListener, AppDetailsRecyclerViewAdapter.AppDetailsRecyclerViewAdapterCallbacks {
static {
AppCompatDelegate.setCompatVectorFromResourcesEnabled(true);
}
private static final String TAG = "AppDetails2"; private static final String TAG = "AppDetails2";

View File

@ -242,7 +242,7 @@ public class FDroid extends AppCompatActivity implements SearchView.OnQueryTextL
if (!TextUtils.isEmpty(packageName)) { if (!TextUtils.isEmpty(packageName)) {
Utils.debugLog(TAG, "FDroid launched via app link for '" + packageName + "'"); Utils.debugLog(TAG, "FDroid launched via app link for '" + packageName + "'");
Intent intentToInvoke = new Intent(this, AppDetails.class); Intent intentToInvoke = new Intent(this, AppDetails2.class);
intentToInvoke.putExtra(AppDetails.EXTRA_APPID, packageName); intentToInvoke.putExtra(AppDetails.EXTRA_APPID, packageName);
startActivity(intentToInvoke); startActivity(intentToInvoke);
finish(); finish();

View File

@ -410,7 +410,7 @@ public class UpdateService extends IntentService {
// now that downloading the index is done, start downloading updates // now that downloading the index is done, start downloading updates
if (changes && fdroidPrefs.isAutoDownloadEnabled()) { if (changes && fdroidPrefs.isAutoDownloadEnabled()) {
autoDownloadUpdates(); autoDownloadUpdates(this);
} }
} }
@ -469,8 +469,8 @@ public class UpdateService extends IntentService {
} }
} }
private void autoDownloadUpdates() { public static void autoDownloadUpdates(Context context) {
Cursor cursor = getContentResolver().query( Cursor cursor = context.getContentResolver().query(
AppProvider.getCanUpdateUri(), AppProvider.getCanUpdateUri(),
Schema.AppMetadataTable.Cols.ALL, Schema.AppMetadataTable.Cols.ALL,
null, null, null); null, null, null);
@ -478,8 +478,8 @@ public class UpdateService extends IntentService {
cursor.moveToFirst(); cursor.moveToFirst();
for (int i = 0; i < cursor.getCount(); i++) { for (int i = 0; i < cursor.getCount(); i++) {
App app = new App(cursor); App app = new App(cursor);
Apk apk = ApkProvider.Helper.findApkFromAnyRepo(this, app.packageName, app.suggestedVersionCode); Apk apk = ApkProvider.Helper.findApkFromAnyRepo(context, app.packageName, app.suggestedVersionCode);
InstallManagerService.queue(this, app, apk); InstallManagerService.queue(context, app, apk);
cursor.moveToNext(); cursor.moveToNext();
} }
cursor.close(); cursor.close();

View File

@ -29,7 +29,11 @@ import android.support.annotation.Nullable;
import android.support.annotation.RequiresApi; import android.support.annotation.RequiresApi;
import android.text.Editable; import android.text.Editable;
import android.text.Html; import android.text.Html;
import android.text.SpannableStringBuilder;
import android.text.Spanned;
import android.text.TextUtils; import android.text.TextUtils;
import android.text.style.CharacterStyle;
import android.text.style.TypefaceSpan;
import android.util.DisplayMetrics; import android.util.DisplayMetrics;
import android.util.Log; import android.util.Log;
import android.util.TypedValue; import android.util.TypedValue;
@ -487,6 +491,23 @@ public final class Utils {
return formatDateFormat(TIME_FORMAT, date, fallback); return formatDateFormat(TIME_FORMAT, date, fallback);
} }
/**
* Formats the app name using "sans-serif" and then appends the summary after a space with
* "sans-serif-light". Doesn't mandate any font sizes or any other styles, that is up to the
* {@link android.widget.TextView} which it ends up being displayed in.
*/
public static CharSequence formatAppNameAndSummary(String appName, String summary) {
String toFormat = appName + ' ' + summary;
CharacterStyle normal = new TypefaceSpan("sans-serif");
CharacterStyle light = new TypefaceSpan("sans-serif-light");
SpannableStringBuilder sb = new SpannableStringBuilder(toFormat);
sb.setSpan(normal, 0, appName.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
sb.setSpan(light, appName.length(), toFormat.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
return sb;
}
// Need this to add the unimplemented support for ordered and unordered // Need this to add the unimplemented support for ordered and unordered
// lists to Html.fromHtml(). // lists to Html.fromHtml().
public static class HtmlTagHandler implements Html.TagHandler { public static class HtmlTagHandler implements Html.TagHandler {

View File

@ -199,16 +199,26 @@ public class AppProvider extends FDroidProvider {
public AppQuerySelection add(AppQuerySelection query) { public AppQuerySelection add(AppQuerySelection query) {
QuerySelection both = super.add(query); QuerySelection both = super.add(query);
AppQuerySelection bothWithJoin = new AppQuerySelection(both.getSelection(), both.getArgs()); AppQuerySelection bothWithJoin = new AppQuerySelection(both.getSelection(), both.getArgs());
if (this.naturalJoinToInstalled() || query.naturalJoinToInstalled()) { ensureJoinsCopied(query, bothWithJoin);
bothWithJoin.requireNaturalInstalledTable();
}
if (this.leftJoinToPrefs() || query.leftJoinToPrefs()) {
bothWithJoin.requireLeftJoinPrefs();
}
return bothWithJoin; return bothWithJoin;
} }
public AppQuerySelection not(AppQuerySelection query) {
QuerySelection both = super.not(query);
AppQuerySelection bothWithJoin = new AppQuerySelection(both.getSelection(), both.getArgs());
ensureJoinsCopied(query, bothWithJoin);
return bothWithJoin;
}
private void ensureJoinsCopied(AppQuerySelection toAdd, AppQuerySelection newlyCreated) {
if (this.naturalJoinToInstalled() || toAdd.naturalJoinToInstalled()) {
newlyCreated.requireNaturalInstalledTable();
}
if (this.leftJoinToPrefs() || toAdd.leftJoinToPrefs()) {
newlyCreated.requireLeftJoinPrefs();
}
}
} }
protected class Query extends QueryBuilder { protected class Query extends QueryBuilder {
@ -373,7 +383,6 @@ public class AppProvider extends FDroidProvider {
protected static final String PATH_APPS = "apps"; protected static final String PATH_APPS = "apps";
protected static final String PATH_SPECIFIC_APP = "app"; protected static final String PATH_SPECIFIC_APP = "app";
private static final String PATH_RECENTLY_UPDATED = "recentlyUpdated"; private static final String PATH_RECENTLY_UPDATED = "recentlyUpdated";
private static final String PATH_NEWLY_ADDED = "newlyAdded";
private static final String PATH_CATEGORY = "category"; private static final String PATH_CATEGORY = "category";
private static final String PATH_REPO = "repo"; private static final String PATH_REPO = "repo";
private static final String PATH_HIGHEST_PRIORITY = "highestPriority"; private static final String PATH_HIGHEST_PRIORITY = "highestPriority";
@ -386,8 +395,7 @@ public class AppProvider extends FDroidProvider {
private static final int SEARCH_TEXT = INSTALLED + 1; private static final int SEARCH_TEXT = INSTALLED + 1;
private static final int SEARCH_TEXT_AND_CATEGORIES = SEARCH_TEXT + 1; private static final int SEARCH_TEXT_AND_CATEGORIES = SEARCH_TEXT + 1;
private static final int RECENTLY_UPDATED = SEARCH_TEXT_AND_CATEGORIES + 1; private static final int RECENTLY_UPDATED = SEARCH_TEXT_AND_CATEGORIES + 1;
private static final int NEWLY_ADDED = RECENTLY_UPDATED + 1; private static final int CATEGORY = RECENTLY_UPDATED + 1;
private static final int CATEGORY = NEWLY_ADDED + 1;
private static final int CALC_SUGGESTED_APKS = CATEGORY + 1; private static final int CALC_SUGGESTED_APKS = CATEGORY + 1;
private static final int REPO = CALC_SUGGESTED_APKS + 1; private static final int REPO = CALC_SUGGESTED_APKS + 1;
private static final int SEARCH_REPO = REPO + 1; private static final int SEARCH_REPO = REPO + 1;
@ -401,7 +409,6 @@ public class AppProvider extends FDroidProvider {
MATCHER.addURI(getAuthority(), null, CODE_LIST); MATCHER.addURI(getAuthority(), null, CODE_LIST);
MATCHER.addURI(getAuthority(), PATH_CALC_SUGGESTED_APKS, CALC_SUGGESTED_APKS); MATCHER.addURI(getAuthority(), PATH_CALC_SUGGESTED_APKS, CALC_SUGGESTED_APKS);
MATCHER.addURI(getAuthority(), PATH_RECENTLY_UPDATED, RECENTLY_UPDATED); MATCHER.addURI(getAuthority(), PATH_RECENTLY_UPDATED, RECENTLY_UPDATED);
MATCHER.addURI(getAuthority(), PATH_NEWLY_ADDED, NEWLY_ADDED);
MATCHER.addURI(getAuthority(), PATH_CATEGORY + "/*", CATEGORY); MATCHER.addURI(getAuthority(), PATH_CATEGORY + "/*", CATEGORY);
MATCHER.addURI(getAuthority(), PATH_SEARCH + "/*/*", SEARCH_TEXT_AND_CATEGORIES); MATCHER.addURI(getAuthority(), PATH_SEARCH + "/*/*", SEARCH_TEXT_AND_CATEGORIES);
MATCHER.addURI(getAuthority(), PATH_SEARCH + "/*", SEARCH_TEXT); MATCHER.addURI(getAuthority(), PATH_SEARCH + "/*", SEARCH_TEXT);
@ -425,10 +432,6 @@ public class AppProvider extends FDroidProvider {
return Uri.withAppendedPath(getContentUri(), PATH_RECENTLY_UPDATED); return Uri.withAppendedPath(getContentUri(), PATH_RECENTLY_UPDATED);
} }
public static Uri getNewlyAddedUri() {
return Uri.withAppendedPath(getContentUri(), PATH_NEWLY_ADDED);
}
private static Uri calcSuggestedApksUri() { private static Uri calcSuggestedApksUri() {
return Uri.withAppendedPath(getContentUri(), PATH_CALC_SUGGESTED_APKS); return Uri.withAppendedPath(getContentUri(), PATH_CALC_SUGGESTED_APKS);
} }
@ -571,7 +574,8 @@ public class AppProvider extends FDroidProvider {
final String ignoreAll = "COALESCE(prefs." + AppPrefsTable.Cols.IGNORE_ALL_UPDATES + ", 0) != 1"; final String ignoreAll = "COALESCE(prefs." + AppPrefsTable.Cols.IGNORE_ALL_UPDATES + ", 0) != 1";
final String ignore = " (" + ignoreCurrent + " AND " + ignoreAll + ") "; final String ignore = " (" + ignoreCurrent + " AND " + ignoreAll + ") ";
final String where = ignore + " AND " + app + "." + Cols.SUGGESTED_VERSION_CODE + " > installed." + InstalledAppTable.Cols.VERSION_CODE; final String nullChecks = app + "." + Cols.SUGGESTED_VERSION_CODE + " IS NOT NULL AND installed." + InstalledAppTable.Cols.VERSION_CODE + " IS NOT NULL ";
final String where = nullChecks + " AND " + ignore + " AND " + app + "." + Cols.SUGGESTED_VERSION_CODE + " > installed." + InstalledAppTable.Cols.VERSION_CODE;
return new AppQuerySelection(where).requireNaturalInstalledTable().requireLeftJoinPrefs(); return new AppQuerySelection(where).requireNaturalInstalledTable().requireLeftJoinPrefs();
} }
@ -583,7 +587,7 @@ public class AppProvider extends FDroidProvider {
} }
private AppQuerySelection queryInstalled() { private AppQuerySelection queryInstalled() {
return new AppQuerySelection().requireNaturalInstalledTable(); return new AppQuerySelection().requireNaturalInstalledTable().not(queryCanUpdate());
} }
private AppQuerySelection querySearch(String query) { private AppQuerySelection querySearch(String query) {
@ -666,12 +670,6 @@ public class AppProvider extends FDroidProvider {
return new AppQuerySelection(selection); return new AppQuerySelection(selection);
} }
private AppQuerySelection queryNewlyAdded() {
final String selection = getTableName() + "." + Cols.ADDED + " > ?";
final String[] args = {Utils.formatDate(Preferences.get().calcMaxHistory(), "")};
return new AppQuerySelection(selection, args);
}
/** /**
* Ensures that for each app metadata row with the same package name, only the one from the repo * Ensures that for each app metadata row with the same package name, only the one from the repo
* with the best priority is represented in the result set. While possible to calculate this * with the best priority is represented in the result set. While possible to calculate this
@ -689,9 +687,7 @@ public class AppProvider extends FDroidProvider {
} }
private AppQuerySelection queryRecentlyUpdated() { private AppQuerySelection queryRecentlyUpdated() {
final String app = getTableName(); final String selection = getTableName() + "." + Cols.LAST_UPDATED + " > ? ";
final String lastUpdated = app + "." + Cols.LAST_UPDATED;
final String selection = app + "." + Cols.ADDED + " != " + lastUpdated + " AND " + lastUpdated + " > ?";
final String[] args = {Utils.formatDate(Preferences.get().calcMaxHistory(), "")}; final String[] args = {Utils.formatDate(Preferences.get().calcMaxHistory(), "")};
return new AppQuerySelection(selection, args); return new AppQuerySelection(selection, args);
} }
@ -810,12 +806,6 @@ public class AppProvider extends FDroidProvider {
includeSwap = false; includeSwap = false;
break; break;
case NEWLY_ADDED:
sortOrder = getTableName() + "." + Cols.ADDED + " DESC";
selection = selection.add(queryNewlyAdded());
includeSwap = false;
break;
case HIGHEST_PRIORITY: case HIGHEST_PRIORITY:
selection = selection.add(queryPackageName(uri.getLastPathSegment())); selection = selection.add(queryPackageName(uri.getLastPathSegment()));
includeSwap = false; includeSwap = false;

View File

@ -78,4 +78,8 @@ public class QuerySelection {
return new QuerySelection(s, a); return new QuerySelection(s, a);
} }
public QuerySelection not(QuerySelection querySelection) {
String where = " NOT (" + querySelection.getSelection() + ") ";
return add(where, querySelection.getArgs());
}
} }

View File

@ -29,17 +29,15 @@ import android.net.Uri;
import android.net.wifi.WifiInfo; import android.net.wifi.WifiInfo;
import android.net.wifi.WifiManager; import android.net.wifi.WifiManager;
import android.os.AsyncTask; import android.os.AsyncTask;
import android.os.Build;
import android.os.Bundle; import android.os.Bundle;
import android.support.annotation.NonNull; import android.support.annotation.NonNull;
import android.support.v4.app.FragmentManager;
import android.support.v4.app.ListFragment;
import android.support.v4.app.LoaderManager; import android.support.v4.app.LoaderManager;
import android.support.v4.app.NavUtils; import android.support.v4.app.NavUtils;
import android.support.v4.content.CursorLoader; import android.support.v4.content.CursorLoader;
import android.support.v4.content.Loader; import android.support.v4.content.Loader;
import android.support.v7.app.ActionBarActivity;
import android.support.v7.app.AlertDialog; import android.support.v7.app.AlertDialog;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.Toolbar;
import android.text.Editable; import android.text.Editable;
import android.text.TextUtils; import android.text.TextUtils;
import android.text.TextWatcher; import android.text.TextWatcher;
@ -47,9 +45,9 @@ import android.util.Log;
import android.view.Menu; import android.view.Menu;
import android.view.MenuItem; import android.view.MenuItem;
import android.view.View; import android.view.View;
import android.widget.AdapterView;
import android.widget.Button; import android.widget.Button;
import android.widget.EditText; import android.widget.EditText;
import android.widget.LinearLayout;
import android.widget.ListView; import android.widget.ListView;
import android.widget.TextView; import android.widget.TextView;
import android.widget.Toast; import android.widget.Toast;
@ -74,7 +72,7 @@ import java.net.URISyntaxException;
import java.net.URL; import java.net.URL;
import java.util.Locale; import java.util.Locale;
public class ManageReposActivity extends ActionBarActivity { public class ManageReposActivity extends AppCompatActivity implements LoaderManager.LoaderCallbacks<Cursor>, RepoAdapter.EnabledListener {
private static final String TAG = "ManageReposActivity"; private static final String TAG = "ManageReposActivity";
private static final String DEFAULT_NEW_REPO_TEXT = "https://"; private static final String DEFAULT_NEW_REPO_TEXT = "https://";
@ -85,7 +83,7 @@ public class ManageReposActivity extends ActionBarActivity {
IS_SWAP IS_SWAP
} }
private RepoListFragment listFragment; private Toolbar toolbar;
/** /**
* True if activity started with an intent such as from QR code. False if * True if activity started with an intent such as from QR code. False if
@ -99,31 +97,23 @@ public class ManageReposActivity extends ActionBarActivity {
((FDroidApp) getApplication()).applyTheme(this); ((FDroidApp) getApplication()).applyTheme(this);
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
FragmentManager fm = getSupportFragmentManager(); setContentView(R.layout.repo_list_activity);
if (fm.findFragmentById(android.R.id.content) == null) {
/*
* Need to set a dummy view (which will get overridden by the
* fragment manager below) so that we can call setContentView().
* This is a work around for a (bug?) thing in 3.0, 3.1 which
* requires setContentView to be invoked before the actionbar is
* played with:
* http://blog.perpetumdesign.com/2011/08/strange-case-of
* -dr-action-and-mr-bar.html
*/
if (Build.VERSION.SDK_INT >= 11 && Build.VERSION.SDK_INT <= 13) {
setContentView(new LinearLayout(this));
}
listFragment = new RepoListFragment();
fm.beginTransaction()
.add(android.R.id.content, listFragment)
.commit();
}
toolbar = (Toolbar) findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
getSupportActionBar().setDisplayHomeAsUpEnabled(true); getSupportActionBar().setDisplayHomeAsUpEnabled(true);
// title is "Repositories" here, but "F-Droid" in VIEW Intent chooser
getSupportActionBar().setTitle(R.string.menu_manage); final ListView repoList = (ListView) findViewById(R.id.list);
repoAdapter = RepoAdapter.create(this, null, CursorAdapterCompat.FLAG_AUTO_REQUERY);
repoAdapter.setEnabledListener(this);
repoList.setAdapter(repoAdapter);
repoList.setOnItemClickListener(new AdapterView.OnItemClickListener() {
@Override
public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
Repo repo = new Repo((Cursor) repoList.getItemAtPosition(position));
editRepo(repo);
}
});
} }
@Override @Override
@ -133,6 +123,9 @@ public class ManageReposActivity extends ActionBarActivity {
/* let's see if someone is trying to send us a new repo */ /* let's see if someone is trying to send us a new repo */
addRepoFromIntent(getIntent()); addRepoFromIntent(getIntent());
// Starts a new or restarts an existing Loader in this manager
getSupportLoaderManager().restartLoader(0, null, this);
} }
@Override @Override
@ -149,7 +142,7 @@ public class ManageReposActivity extends ActionBarActivity {
@Override @Override
public boolean onCreateOptionsMenu(Menu menu) { public boolean onCreateOptionsMenu(Menu menu) {
getMenuInflater().inflate(R.menu.manage_repos, menu); toolbar.inflateMenu(R.menu.manage_repos);
return super.onCreateOptionsMenu(menu); return super.onCreateOptionsMenu(menu);
} }
@ -649,7 +642,7 @@ public class ManageReposActivity extends ActionBarActivity {
values.put(RepoTable.Cols.IN_USE, 1); values.put(RepoTable.Cols.IN_USE, 1);
values.put(RepoTable.Cols.FINGERPRINT, fingerprint); values.put(RepoTable.Cols.FINGERPRINT, fingerprint);
RepoProvider.Helper.update(context, repo, values); RepoProvider.Helper.update(context, repo, values);
listFragment.notifyDataSetChanged(); notifyDataSetChanged();
finishedAddingRepo(); finishedAddingRepo();
} }
@ -703,15 +696,11 @@ public class ManageReposActivity extends ActionBarActivity {
} }
} }
public static class RepoListFragment extends ListFragment
implements LoaderManager.LoaderCallbacks<Cursor>, RepoAdapter.EnabledListener {
private RepoAdapter repoAdapter; private RepoAdapter repoAdapter;
@Override @Override
public Loader<Cursor> onCreateLoader(int i, Bundle bundle) { public Loader<Cursor> onCreateLoader(int i, Bundle bundle) {
Uri uri = RepoProvider.allExceptSwapUri(); Uri uri = RepoProvider.allExceptSwapUri();
Utils.debugLog(TAG, "Creating repo loader '" + uri + "'.");
final String[] projection = { final String[] projection = {
RepoTable.Cols._ID, RepoTable.Cols._ID,
RepoTable.Cols.NAME, RepoTable.Cols.NAME,
@ -719,7 +708,7 @@ public class ManageReposActivity extends ActionBarActivity {
RepoTable.Cols.FINGERPRINT, RepoTable.Cols.FINGERPRINT,
RepoTable.Cols.IN_USE, RepoTable.Cols.IN_USE,
}; };
return new CursorLoader(getActivity(), uri, projection, null, null, null); return new CursorLoader(this, uri, projection, null, null, null);
} }
@Override @Override
@ -752,51 +741,22 @@ public class ManageReposActivity extends ActionBarActivity {
if (repo.inuse != isEnabled) { if (repo.inuse != isEnabled) {
ContentValues values = new ContentValues(1); ContentValues values = new ContentValues(1);
values.put(RepoTable.Cols.IN_USE, isEnabled ? 1 : 0); values.put(RepoTable.Cols.IN_USE, isEnabled ? 1 : 0);
RepoProvider.Helper.update(getActivity(), repo, values); RepoProvider.Helper.update(this, repo, values);
if (isEnabled) { if (isEnabled) {
UpdateService.updateNow(getActivity()); UpdateService.updateNow(this);
} else { } else {
RepoProvider.Helper.purgeApps(getActivity(), repo); RepoProvider.Helper.purgeApps(this, repo);
String notification = getString(R.string.repo_disabled_notification, repo.name); String notification = getString(R.string.repo_disabled_notification, repo.name);
Toast.makeText(getActivity(), notification, Toast.LENGTH_LONG).show(); Toast.makeText(this, notification, Toast.LENGTH_LONG).show();
} }
} }
} }
@Override
public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setRetainInstance(true);
setHasOptionsMenu(true);
repoAdapter = RepoAdapter.create(getActivity(), null, CursorAdapterCompat.FLAG_AUTO_REQUERY);
repoAdapter.setEnabledListener(this);
setListAdapter(repoAdapter);
}
@Override
public void onResume() {
super.onResume();
// Starts a new or restarts an existing Loader in this manager
getLoaderManager().restartLoader(0, null, this);
}
@Override
public void onListItemClick(ListView l, View v, int position, long id) {
super.onListItemClick(l, v, position, id);
Repo repo = new Repo((Cursor) getListView().getItemAtPosition(position));
editRepo(repo);
}
public static final int SHOW_REPO_DETAILS = 1; public static final int SHOW_REPO_DETAILS = 1;
public void editRepo(Repo repo) { public void editRepo(Repo repo) {
Intent intent = new Intent(getActivity(), RepoDetailsActivity.class); Intent intent = new Intent(this, RepoDetailsActivity.class);
intent.putExtra(RepoDetailsActivity.ARG_REPO_ID, repo.getId()); intent.putExtra(RepoDetailsActivity.ARG_REPO_ID, repo.getId());
startActivityForResult(intent, SHOW_REPO_DETAILS); startActivityForResult(intent, SHOW_REPO_DETAILS);
} }
@ -808,7 +768,6 @@ public class ManageReposActivity extends ActionBarActivity {
* repo, and wanting the switch to be changed to on). * repo, and wanting the switch to be changed to on).
*/ */
private void notifyDataSetChanged() { private void notifyDataSetChanged() {
getLoaderManager().restartLoader(0, null, this); getSupportLoaderManager().restartLoader(0, null, this);
}
} }
} }

View File

@ -17,6 +17,7 @@ import android.support.v4.app.NavUtils;
import android.support.v4.content.LocalBroadcastManager; import android.support.v4.content.LocalBroadcastManager;
import android.support.v7.app.ActionBarActivity; import android.support.v7.app.ActionBarActivity;
import android.support.v7.app.AlertDialog; import android.support.v7.app.AlertDialog;
import android.support.v7.widget.Toolbar;
import android.text.TextUtils; import android.text.TextUtils;
import android.text.format.DateUtils; import android.text.format.DateUtils;
import android.view.Menu; import android.view.Menu;
@ -93,8 +94,12 @@ public class RepoDetailsActivity extends ActionBarActivity {
((FDroidApp) getApplication()).applyTheme(this); ((FDroidApp) getApplication()).applyTheme(this);
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
setContentView(R.layout.repodetails); setContentView(R.layout.repodetails);
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
repoView = findViewById(R.id.repoView); repoView = findViewById(R.id.repoView);
repoId = getIntent().getLongExtra(ARG_REPO_ID, 0); repoId = getIntent().getLongExtra(ARG_REPO_ID, 0);

View File

@ -0,0 +1,123 @@
package org.fdroid.fdroid.views.apps;
import android.content.Intent;
import android.database.Cursor;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.app.LoaderManager;
import android.support.v4.content.CursorLoader;
import android.support.v4.content.Loader;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.view.View;
import android.widget.EditText;
import org.fdroid.fdroid.R;
import org.fdroid.fdroid.data.AppProvider;
import org.fdroid.fdroid.data.Schema;
public class AppListActivity extends AppCompatActivity implements LoaderManager.LoaderCallbacks<Cursor>, CategoryTextWatcher.SearchTermsChangedListener {
public static final String EXTRA_CATEGORY = "org.fdroid.fdroid.views.apps.AppListActivity.EXTRA_CATEGORY";
private RecyclerView appView;
private AppListAdapter appAdapter;
private String category;
private String searchTerms;
private EditText searchInput;
@Override
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_app_list);
searchInput = (EditText) findViewById(R.id.search);
searchInput.addTextChangedListener(new CategoryTextWatcher(this, searchInput, this));
View backButton = findViewById(R.id.back);
backButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
finish();
}
});
View clearButton = findViewById(R.id.clear);
clearButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
searchInput.setText("");
}
});
appAdapter = new AppListAdapter(this);
appView = (RecyclerView) findViewById(R.id.app_list);
appView.setHasFixedSize(true);
appView.setLayoutManager(new LinearLayoutManager(this));
appView.setAdapter(appAdapter);
}
@Override
protected void onResume() {
super.onResume();
Intent intent = getIntent();
category = intent.hasExtra(EXTRA_CATEGORY) ? intent.getStringExtra(EXTRA_CATEGORY) : null;
searchInput.setText(getSearchText(category, null));
searchInput.setSelection(searchInput.getText().length());
if (category != null) {
// Do this so that the search input does not get focus by default. This allows for a user
// experience where the user scrolls through the apps in the category.
appView.requestFocus();
}
getSupportLoaderManager().initLoader(0, null, this);
}
private CharSequence getSearchText(@Nullable String category, @Nullable String searchTerms) {
StringBuilder string = new StringBuilder();
if (category != null) {
string.append(category).append(":");
}
if (searchTerms != null) {
string.append(searchTerms);
}
return string.toString();
}
@Override
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
return new CursorLoader(
this,
AppProvider.getSearchUri(searchTerms, category),
Schema.AppMetadataTable.Cols.ALL,
null,
null,
null
);
}
@Override
public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) {
appAdapter.setAppCursor(cursor);
}
@Override
public void onLoaderReset(Loader<Cursor> loader) {
appAdapter.setAppCursor(null);
}
@Override
public void onSearchTermsChanged(@Nullable String category, @NonNull String searchTerms) {
this.category = category;
this.searchTerms = searchTerms;
getSupportLoaderManager().restartLoader(0, null, this);
}
}

View File

@ -0,0 +1,54 @@
package org.fdroid.fdroid.views.apps;
import android.app.Activity;
import android.database.Cursor;
import android.support.v7.widget.RecyclerView;
import android.view.ViewGroup;
import org.fdroid.fdroid.R;
import org.fdroid.fdroid.data.App;
class AppListAdapter extends RecyclerView.Adapter<AppListItemController> {
private Cursor cursor;
private final Activity activity;
private final AppListItemDivider divider;
AppListAdapter(Activity activity) {
this.activity = activity;
divider = new AppListItemDivider(activity);
}
public void setAppCursor(Cursor cursor) {
this.cursor = cursor;
notifyDataSetChanged();
}
@Override
public AppListItemController onCreateViewHolder(ViewGroup parent, int viewType) {
return new AppListItemController(activity, activity.getLayoutInflater().inflate(R.layout.app_list_item, parent, false));
}
@Override
public void onBindViewHolder(AppListItemController holder, int position) {
cursor.moveToPosition(position);
holder.bindModel(new App(cursor));
}
@Override
public int getItemCount() {
return cursor == null ? 0 : cursor.getCount();
}
@Override
public void onAttachedToRecyclerView(RecyclerView recyclerView) {
super.onAttachedToRecyclerView(recyclerView);
recyclerView.addItemDecoration(divider);
}
@Override
public void onDetachedFromRecyclerView(RecyclerView recyclerView) {
recyclerView.removeItemDecoration(divider);
super.onDetachedFromRecyclerView(recyclerView);
}
}

View File

@ -0,0 +1,149 @@
package org.fdroid.fdroid.views.apps;
import android.app.Activity;
import android.content.Intent;
import android.os.Build;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.v4.app.ActivityOptionsCompat;
import android.support.v4.util.Pair;
import android.support.v7.widget.RecyclerView;
import android.view.View;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.TextView;
import com.nostra13.universalimageloader.core.DisplayImageOptions;
import com.nostra13.universalimageloader.core.ImageLoader;
import org.fdroid.fdroid.AppDetails;
import org.fdroid.fdroid.AppDetails2;
import org.fdroid.fdroid.R;
import org.fdroid.fdroid.Utils;
import org.fdroid.fdroid.data.ApkProvider;
import org.fdroid.fdroid.data.App;
import org.fdroid.fdroid.installer.InstallManagerService;
public class AppListItemController extends RecyclerView.ViewHolder {
private final Activity activity;
private final Button installButton;
private final ImageView icon;
private final TextView name;
private final TextView status;
private final DisplayImageOptions displayImageOptions;
private App currentApp;
public AppListItemController(Activity activity, View itemView) {
super(itemView);
this.activity = activity;
installButton = (Button) itemView.findViewById(R.id.install);
installButton.setOnClickListener(onInstallClicked);
icon = (ImageView) itemView.findViewById(R.id.icon);
name = (TextView) itemView.findViewById(R.id.app_name);
status = (TextView) itemView.findViewById(R.id.status);
displayImageOptions = Utils.getImageLoadingOptions().build();
itemView.setOnClickListener(onAppClicked);
}
public void bindModel(@NonNull App app) {
currentApp = app;
name.setText(Utils.formatAppNameAndSummary(app.name, app.summary));
ImageLoader.getInstance().displayImage(app.iconUrl, icon, displayImageOptions);
configureStatusText(app);
configureInstallButton(app);
}
/**
* Sets the text/visibility of the {@link R.id#status} {@link TextView} based on whether the app:
* * Is compatible with the users device
* * Is installed
* * Can be updated
*
* TODO: This button also needs to be repurposed to support the "Downloaded but not installed" state.
*/
private void configureStatusText(@NonNull App app) {
if (status == null) {
return;
}
if (!app.compatible) {
status.setText(activity.getString(R.string.app_incompatible));
status.setVisibility(View.VISIBLE);
} else if (app.isInstalled()) {
if (app.canAndWantToUpdate(activity)) {
String upgradeFromTo = activity.getString(R.string.app_version_x_available, app.getSuggestedVersionName());
status.setText(upgradeFromTo);
} else {
String installed = activity.getString(R.string.app_version_x_installed, app.installedVersionName);
status.setText(installed);
}
status.setVisibility(View.VISIBLE);
} else {
status.setVisibility(View.INVISIBLE);
}
}
/**
* The install button is shown when an app:
* * Is compatible with the users device.
* * Has not been filtered due to anti-features/root/etc.
* * Is either not installed or installed but can be updated.
*
* TODO: This button also needs to be repurposed to support the "Downloaded but not installed" state.
*/
private void configureInstallButton(@NonNull App app) {
if (installButton == null) {
return;
}
boolean installable = app.canAndWantToUpdate(activity) || !app.isInstalled();
boolean shouldAllow = app.compatible && !app.isFiltered();
if (shouldAllow && installable) {
installButton.setVisibility(View.VISIBLE);
} else {
installButton.setVisibility(View.GONE);
}
}
private final View.OnClickListener onAppClicked = new View.OnClickListener() {
@Override
public void onClick(View v) {
if (currentApp == null) {
return;
}
Intent intent = new Intent(activity, AppDetails2.class);
intent.putExtra(AppDetails.EXTRA_APPID, currentApp.packageName);
if (Build.VERSION.SDK_INT >= 21) {
Pair<View, String> iconTransitionPair = Pair.create((View) icon, activity.getString(R.string.transition_app_item_icon));
Bundle bundle = ActivityOptionsCompat.makeSceneTransitionAnimation(activity, iconTransitionPair).toBundle();
activity.startActivity(intent, bundle);
} else {
activity.startActivity(intent);
}
}
};
private final View.OnClickListener onInstallClicked = new View.OnClickListener() {
@Override
public void onClick(View v) {
if (currentApp == null) {
return;
}
InstallManagerService.queue(activity, currentApp, ApkProvider.Helper.findApkFromAnyRepo(activity, currentApp.packageName, currentApp.suggestedVersionCode));
}
};
}

View File

@ -0,0 +1,33 @@
package org.fdroid.fdroid.views.apps;
import android.content.Context;
import android.graphics.Rect;
import android.support.v4.content.ContextCompat;
import android.support.v7.widget.DividerItemDecoration;
import android.support.v7.widget.RecyclerView;
import android.view.View;
import org.fdroid.fdroid.R;
import org.fdroid.fdroid.Utils;
/**
* Draws a faint line between items, to be used with the {@link AppListItemDivider}.
*/
public class AppListItemDivider extends DividerItemDecoration {
private final int itemSpacing;
public AppListItemDivider(Context context) {
super(context, DividerItemDecoration.VERTICAL);
setDrawable(ContextCompat.getDrawable(context, R.drawable.app_list_item_divider));
itemSpacing = Utils.dpToPx(8, context);
}
@Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
super.getItemOffsets(outRect, view, parent, state);
int position = parent.getChildAdapterPosition(view);
if (position > 0) {
outRect.bottom = itemSpacing;
}
}
}

View File

@ -0,0 +1,133 @@
package org.fdroid.fdroid.views.apps;
import android.content.Context;
import android.graphics.Canvas;
import android.graphics.Color;
import android.graphics.Paint;
import android.graphics.RectF;
import android.graphics.drawable.Drawable;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.content.ContextCompat;
import android.text.style.ReplacementSpan;
import org.fdroid.fdroid.R;
import org.fdroid.fdroid.views.categories.CategoryController;
/**
* This draws a category "chip" in the search text view according to the material design specs
* (https://material.google.com/components/chips.html#chips-specs). These contain a circle with an
* icon representing "category" on the left, and the name of the category on the right. It also has
* a background with curved corners behind the category text.
*/
public class CategorySpan extends ReplacementSpan {
private static final int HEIGHT = 32;
private static final int CORNER_RADIUS = 16;
private static final int ICON_BACKGROUND_SIZE = 32;
private static final int ICON_SIZE = 16;
private static final int ICON_PADDING = (ICON_BACKGROUND_SIZE - ICON_SIZE) / 2;
private static final int TEXT_LEADING_PADDING = 8;
private static final int TEXT_TRAILING_PADDING = 12;
private static final int TEXT_BELOW_PADDING = 4;
private static final int WHITE_SPACE_PADDING_AT_END = 4;
private static final float DROP_SHADOW_HEIGHT = 1.5f;
private final Context context;
public CategorySpan(Context context) {
super();
this.context = context;
}
@Nullable
private static CharSequence getCategoryName(@Nullable CharSequence text, int start, int end) {
if (text == null) {
return null;
}
if (start + 1 >= end - 1) {
// This can happen when the spell checker is trying to underline text within our category
// name. It sometimes will ask for sub-lengths of this span.
return null;
}
return text.subSequence(start, end - 1);
}
@Override
public int getSize(@NonNull Paint paint, CharSequence text, int start, int end, Paint.FontMetricsInt fm) {
CharSequence categoryName = getCategoryName(text, start, end);
if (categoryName == null) {
return 0;
}
float density = context.getResources().getDisplayMetrics().density;
int iconBackgroundSize = (int) (ICON_BACKGROUND_SIZE * density);
int textLeadingPadding = (int) (TEXT_LEADING_PADDING * density);
int textWidth = (int) paint.measureText(categoryName.toString());
int textTrailingPadding = (int) (TEXT_TRAILING_PADDING * density);
int whiteSpacePadding = (int) (WHITE_SPACE_PADDING_AT_END * density);
return iconBackgroundSize + textLeadingPadding + textWidth + textTrailingPadding + whiteSpacePadding;
}
@Override
public void draw(@NonNull Canvas canvas, CharSequence text, int start, int end, float x, int top, int y, int bottom, @NonNull Paint paint) {
CharSequence categoryName = getCategoryName(text, start, end);
if (categoryName == null) {
return;
}
float density = context.getResources().getDisplayMetrics().density;
int height = (int) (HEIGHT * density);
int iconBackgroundSize = (int) (ICON_BACKGROUND_SIZE * density);
int cornerRadius = (int) (CORNER_RADIUS * density);
int iconSize = (int) (ICON_SIZE * density);
int iconPadding = (int) (ICON_PADDING * density);
int textWidth = (int) paint.measureText(categoryName.toString());
int textLeadingPadding = (int) (TEXT_LEADING_PADDING * density);
int textTrailingPadding = (int) (TEXT_TRAILING_PADDING * density);
canvas.save();
canvas.translate(x, bottom - height + TEXT_BELOW_PADDING * density);
RectF backgroundRect = new RectF(0, 0, iconBackgroundSize + textLeadingPadding + textWidth + textTrailingPadding, height);
// The shadow below the entire category chip.
canvas.save();
canvas.translate(0, DROP_SHADOW_HEIGHT * density);
Paint shadowPaint = new Paint();
shadowPaint.setColor(0x66000000);
shadowPaint.setAntiAlias(true);
canvas.drawRoundRect(backgroundRect, cornerRadius, cornerRadius, shadowPaint);
canvas.restore();
// The background which goes behind the text.
Paint backgroundPaint = new Paint();
backgroundPaint.setColor(CategoryController.getBackgroundColour(categoryName.toString()));
backgroundPaint.setAntiAlias(true);
canvas.drawRoundRect(backgroundRect, cornerRadius, cornerRadius, backgroundPaint);
// The background behind the category icon.
Paint iconBackgroundPaint = new Paint();
iconBackgroundPaint.setColor(0xffd8d8d8);
iconBackgroundPaint.setAntiAlias(true);
RectF iconBackgroundRect = new RectF(0, 0, iconBackgroundSize, height);
canvas.drawRoundRect(iconBackgroundRect, cornerRadius, cornerRadius, iconBackgroundPaint);
// Category icon on top of the circular background which was just drawn.
Drawable icon = ContextCompat.getDrawable(context, R.drawable.ic_category);
icon.setBounds(iconPadding, iconPadding, iconPadding + iconSize, iconPadding + iconSize);
icon.draw(canvas);
// The category name drawn to the right of the category name.
Paint textPaint = new Paint(paint);
textPaint.setColor(Color.WHITE);
canvas.drawText(categoryName.toString(), iconBackgroundSize + textLeadingPadding, bottom, textPaint);
canvas.restore();
}
}

View File

@ -0,0 +1,157 @@
package org.fdroid.fdroid.views.apps;
import android.annotation.TargetApi;
import android.content.Context;
import android.os.Build;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.text.Editable;
import android.text.Spanned;
import android.text.TextWatcher;
import android.text.style.TtsSpan;
import android.widget.EditText;
import org.fdroid.fdroid.R;
/**
* The search input treats text before the first colon as a category name. Text after this colon
* (or all text if there is no colon) is the free text search terms.
* The behaviour of this search input is:
* * Replacing anything before the first colon with a {@link CategorySpan} that renders a "Chip"
* including an icon representing "category" and the name of the category.
* * Removing the trailing ":" from a category chip will cause it to remove the entire category
* from the input.
*/
public class CategoryTextWatcher implements TextWatcher {
interface SearchTermsChangedListener {
void onSearchTermsChanged(@Nullable String category, @NonNull String searchTerms);
}
private final Context context;
private final EditText widget;
private final SearchTermsChangedListener listener;
private int removeTo = -1;
private boolean requiresSpanRecalculation = false;
public CategoryTextWatcher(final Context context, final EditText widget, final SearchTermsChangedListener listener) {
this.context = context;
this.widget = widget;
this.listener = listener;
}
/**
* If the user removed the first colon in the search text, then request for the entire
* block of text representing the category text to be removed when able.
*/
@Override
public void beforeTextChanged(CharSequence s, int start, int count, int after) {
removeTo = -1;
boolean removingOrReplacing = count > 0;
// Don't bother working out if we need to recalculate spans if we are removing text
// right to the start. This could be if we are removing everything (in which case
// there is no text to span), or we are removing somewhere from after the category
// back to the start (in which case we've removed the category anyway and don't need
// to explicilty request it to be removed.
if (start == 0 && removingOrReplacing) {
return;
}
String string = s.toString();
boolean removingColon = removingOrReplacing && string.indexOf(':', start) < (start + count);
boolean removingFirstColon = removingColon && string.indexOf(':') >= start;
if (removingFirstColon) {
removeTo = start + count - 1;
}
}
/**
* If the user added a colon, and there was not previously a colon before the newly added
* one, then request for a {@link CategorySpan} to be added when able.
*/
@Override
public void onTextChanged(CharSequence s, int start, int before, int count) {
boolean addingOrReplacing = count > 0;
boolean addingColon = addingOrReplacing && s.subSequence(start, start + count).toString().indexOf(':') >= 0;
boolean addingFirstColon = addingColon && s.subSequence(0, start).toString().indexOf(':') == -1;
if (addingFirstColon) {
requiresSpanRecalculation = true;
}
}
/**
* If it was decided that we were removing a category, then ensure that the relevant
* characters are removed. If it was deemed we were adding a new category, then ensure
* that the relevant {@link CategorySpan} is added to {@param searchText}.
*/
@Override
public void afterTextChanged(Editable searchText) {
if (removeTo >= 0) {
removeLeadingCharacters(searchText, removeTo);
removeTo = -1;
} else if (requiresSpanRecalculation) {
prepareSpans(searchText);
requiresSpanRecalculation = false;
}
int colonIndex = searchText.toString().indexOf(':');
String category = colonIndex == -1 ? null : searchText.subSequence(0, colonIndex).toString();
String searchTerms = searchText.subSequence(colonIndex == -1 ? 0 : colonIndex + 1, searchText.length()).toString();
listener.onSearchTermsChanged(category, searchTerms);
}
/**
* Removes all characters from {@param searchText} up until {@param end}.
* Will do so without triggering a further set of callbacks on this {@link TextWatcher},
* though if any other {@link TextWatcher}s have been added, they will be notified.
*/
private void removeLeadingCharacters(Editable searchText, int end) {
widget.removeTextChangedListener(this);
searchText.replace(0, end, "");
widget.addTextChangedListener(this);
}
/**
* Ensures that a {@link CategorySpan} is in {@param textToSpannify} if required.
* Will firstly remove all existing category spans, and then add back one if neccesary.
* In addition, also adds a {@link TtsSpan} to indicate to screen readers that the category
* span has semantic meaning representing a category.
*/
@TargetApi(21)
private void prepareSpans(Editable textToSpannify) {
if (textToSpannify == null) {
return;
}
removeSpans(textToSpannify, CategorySpan.class);
if (Build.VERSION.SDK_INT >= 21) {
removeSpans(textToSpannify, TtsSpan.class);
}
int colonIndex = textToSpannify.toString().indexOf(':');
if (colonIndex > 0) {
CategorySpan span = new CategorySpan(context);
textToSpannify.setSpan(span, 0, colonIndex + 1, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
if (Build.VERSION.SDK_INT >= 21) {
// For accessibility reasons, make this more clear to screen readers that the
// span we just added semantically represents a category.
TtsSpan ttsSpan = new TtsSpan.TextBuilder(context.getString(R.string.category)).build();
textToSpannify.setSpan(ttsSpan, 0, 0, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
}
}
}
/**
* Helper function to remove all spans of a certain type from an {@link Editable}.
*/
private <T> void removeSpans(Editable text, Class<T> clazz) {
T[] spans = text.getSpans(0, text.length(), clazz);
for (T span : spans) {
text.removeSpan(span);
}
}
}

View File

@ -0,0 +1,180 @@
package org.fdroid.fdroid.views.categories;
import android.app.Activity;
import android.content.Intent;
import android.graphics.Bitmap;
import android.os.Build;
import android.os.Bundle;
import android.support.annotation.IdRes;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.support.v4.app.ActivityOptionsCompat;
import android.support.v4.util.Pair;
import android.support.v7.graphics.Palette;
import android.support.v7.widget.RecyclerView;
import android.view.View;
import android.widget.ImageView;
import android.widget.TextView;
import com.nostra13.universalimageloader.core.DisplayImageOptions;
import com.nostra13.universalimageloader.core.ImageLoader;
import com.nostra13.universalimageloader.core.assist.FailReason;
import com.nostra13.universalimageloader.core.listener.ImageLoadingListener;
import org.fdroid.fdroid.AppDetails;
import org.fdroid.fdroid.AppDetails2;
import org.fdroid.fdroid.Preferences;
import org.fdroid.fdroid.R;
import org.fdroid.fdroid.Utils;
import org.fdroid.fdroid.data.App;
import java.util.Date;
/**
* The {@link AppCardController} can bind an app to several different layouts, as long as the layout
* contains the following elements:
* + {@link R.id#icon} ({@link ImageView}, required)
* + {@link R.id#summary} ({@link TextView}, required)
* + {@link R.id#featured_image} ({@link ImageView}, optional)
* + {@link R.id#status} ({@link TextView}, optional)
*/
public class AppCardController extends RecyclerView.ViewHolder implements ImageLoadingListener, View.OnClickListener {
@NonNull
private final ImageView icon;
@NonNull
private final TextView summary;
@Nullable
private final TextView status;
@Nullable
private final ImageView featuredImage;
@Nullable
private App currentApp;
private final Activity activity;
private final int defaultFeaturedImageColour;
private final DisplayImageOptions displayImageOptions;
private final Date recentCuttoffDate;
public AppCardController(Activity activity, View itemView) {
super(itemView);
this.activity = activity;
recentCuttoffDate = Preferences.get().calcMaxHistory();
icon = (ImageView) findViewAndEnsureNonNull(itemView, R.id.icon);
summary = (TextView) findViewAndEnsureNonNull(itemView, R.id.summary);
featuredImage = (ImageView) itemView.findViewById(R.id.featured_image);
status = (TextView) itemView.findViewById(R.id.status);
defaultFeaturedImageColour = activity.getResources().getColor(R.color.cardview_light_background);
displayImageOptions = Utils.getImageLoadingOptions().build();
itemView.setOnClickListener(this);
}
/**
* The contract that this controller has is that it will consume any layout resource, given
* it has some specific view types (with specific IDs) available. This helper function will
* throw an {@link IllegalArgumentException} if the view doesn't exist,
*/
@NonNull
private View findViewAndEnsureNonNull(View view, @IdRes int res) {
View found = view.findViewById(res);
if (found == null) {
String resName = activity.getResources().getResourceName(res);
throw new IllegalArgumentException("Layout for AppCardController requires " + resName);
}
return found;
}
public void bindApp(@NonNull App app) {
currentApp = app;
summary.setText(Utils.formatAppNameAndSummary(app.name, app.summary));
if (status != null) {
if (app.added != null && app.added.after(recentCuttoffDate) && (app.lastUpdated == null || app.added.equals(app.lastUpdated))) {
status.setText(activity.getString(R.string.category_Whats_New));
status.setVisibility(View.VISIBLE);
} else if (app.lastUpdated != null && app.lastUpdated.after(recentCuttoffDate)) {
status.setText(activity.getString(R.string.category_Recently_Updated));
status.setVisibility(View.VISIBLE);
} else {
status.setVisibility(View.GONE);
}
}
if (featuredImage != null) {
featuredImage.setBackgroundColor(defaultFeaturedImageColour);
}
ImageLoader.getInstance().displayImage(app.iconUrl, icon, displayImageOptions, this);
}
/**
* When the user clicks/touches an app card, we launch the {@link AppDetails2} activity in response.
*/
@Override
public void onClick(View v) {
if (currentApp == null) {
return;
}
Intent intent = new Intent(activity, AppDetails2.class);
intent.putExtra(AppDetails.EXTRA_APPID, currentApp.packageName);
if (Build.VERSION.SDK_INT >= 21) {
Pair<View, String> iconTransitionPair = Pair.create((View) icon, activity.getString(R.string.transition_app_item_icon));
@SuppressWarnings("unchecked") // We are passing the right type as the second varargs argument (i.e. a Pair<View, String>).
Bundle bundle = ActivityOptionsCompat.makeSceneTransitionAnimation(activity, iconTransitionPair).toBundle();
activity.startActivity(intent, bundle);
} else {
activity.startActivity(intent);
}
}
// =============================================================================================
// Icon loader callbacks
//
// Most are unused, the main goal is to specify a background colour for the featured image if
// no featured image is specified in the apps metadata.
// =============================================================================================
@Override
public void onLoadingComplete(String imageUri, View view, Bitmap loadedImage) {
final ImageView image = featuredImage;
if (image != null) {
new Palette.Builder(loadedImage).generate(new Palette.PaletteAsyncListener() {
@Override
public void onGenerated(Palette palette) {
image.setBackgroundColor(palette.getDominantColor(defaultFeaturedImageColour));
}
});
}
}
@Override
public void onLoadingStarted(String imageUri, View view) {
// Do nothing
}
@Override
public void onLoadingCancelled(String imageUri, View view) {
// Do nothing
}
@Override
public void onLoadingFailed(String imageUri, View view, FailReason failReason) {
// Do nothing
}
}

View File

@ -0,0 +1,40 @@
package org.fdroid.fdroid.views.categories;
import android.app.Activity;
import android.database.Cursor;
import android.support.v7.widget.RecyclerView;
import android.view.ViewGroup;
import org.fdroid.fdroid.R;
import org.fdroid.fdroid.data.App;
class AppPreviewAdapter extends RecyclerView.Adapter<AppCardController> {
private Cursor cursor;
private final Activity activity;
AppPreviewAdapter(Activity activity) {
this.activity = activity;
}
@Override
public AppCardController onCreateViewHolder(ViewGroup parent, int viewType) {
return new AppCardController(activity, activity.getLayoutInflater().inflate(R.layout.app_card_normal, parent, false));
}
@Override
public void onBindViewHolder(AppCardController holder, int position) {
cursor.moveToPosition(position);
holder.bindApp(new App(cursor));
}
@Override
public int getItemCount() {
return cursor == null ? 0 : cursor.getCount();
}
public void setAppCursor(Cursor cursor) {
this.cursor = cursor;
notifyDataSetChanged();
}
}

View File

@ -0,0 +1,44 @@
package org.fdroid.fdroid.views.categories;
import android.app.Activity;
import android.database.Cursor;
import android.support.v4.app.LoaderManager;
import android.support.v7.widget.RecyclerView;
import android.view.ViewGroup;
import org.fdroid.fdroid.R;
import org.fdroid.fdroid.data.Schema;
public class CategoryAdapter extends RecyclerView.Adapter<CategoryController> {
private Cursor cursor;
private final Activity activity;
private final LoaderManager loaderManager;
public CategoryAdapter(Activity activity, LoaderManager loaderManager) {
this.activity = activity;
this.loaderManager = loaderManager;
}
@Override
public CategoryController onCreateViewHolder(ViewGroup parent, int viewType) {
return new CategoryController(activity, loaderManager, activity.getLayoutInflater().inflate(R.layout.category_item, parent, false));
}
@Override
public void onBindViewHolder(CategoryController holder, int position) {
cursor.moveToPosition(position);
holder.bindModel(cursor.getString(cursor.getColumnIndex(Schema.CategoryTable.Cols.NAME)));
}
@Override
public int getItemCount() {
return cursor == null ? 0 : cursor.getCount();
}
public void setCategoriesCursor(Cursor cursor) {
this.cursor = cursor;
notifyDataSetChanged();
}
}

View File

@ -0,0 +1,177 @@
package org.fdroid.fdroid.views.categories;
import android.app.Activity;
import android.content.Intent;
import android.content.Context;
import android.database.Cursor;
import android.graphics.Color;
import android.graphics.Rect;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.v4.app.LoaderManager;
import android.support.v4.content.CursorLoader;
import android.support.v4.content.Loader;
import android.support.v4.view.ViewCompat;
import android.support.v7.widget.RecyclerView;
import android.view.View;
import android.widget.Button;
import android.widget.FrameLayout;
import android.widget.TextView;
import org.fdroid.fdroid.R;
import org.fdroid.fdroid.data.AppProvider;
import org.fdroid.fdroid.data.Schema;
import org.fdroid.fdroid.views.apps.AppListActivity;
import java.util.Random;
public class CategoryController extends RecyclerView.ViewHolder implements LoaderManager.LoaderCallbacks<Cursor> {
private final Button viewAll;
private final TextView heading;
private final AppPreviewAdapter appCardsAdapter;
private final FrameLayout background;
private final Activity activity;
private final LoaderManager loaderManager;
private String currentCategory;
CategoryController(final Activity activity, LoaderManager loaderManager, View itemView) {
super(itemView);
this.activity = activity;
this.loaderManager = loaderManager;
appCardsAdapter = new AppPreviewAdapter(activity);
viewAll = (Button) itemView.findViewById(R.id.button);
viewAll.setOnClickListener(onViewAll);
heading = (TextView) itemView.findViewById(R.id.name);
background = (FrameLayout) itemView.findViewById(R.id.category_background);
RecyclerView appCards = (RecyclerView) itemView.findViewById(R.id.app_cards);
appCards.setAdapter(appCardsAdapter);
appCards.addItemDecoration(new ItemDecorator(activity));
}
void bindModel(@NonNull String categoryName) {
currentCategory = categoryName;
heading.setText(categoryName);
viewAll.setVisibility(View.INVISIBLE);
loaderManager.initLoader(currentCategory.hashCode(), null, this);
loaderManager.initLoader(currentCategory.hashCode() + 1, null, this);
background.setBackgroundColor(getBackgroundColour(categoryName));
}
public static int getBackgroundColour(@NonNull String categoryName) {
// Seed based on the categoryName, so that each time we try to choose a colour for the same
// category it will look the same for each different user, and each different session.
Random random = new Random(categoryName.toLowerCase().hashCode());
float[] hsv = new float[3];
hsv[0] = random.nextFloat() * 360;
hsv[1] = 0.4f;
hsv[2] = 0.5f;
return Color.HSVToColor(hsv);
}
@Override
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
if (id == currentCategory.hashCode() + 1) {
return new CursorLoader(
activity,
AppProvider.getCategoryUri(currentCategory),
new String[]{Schema.AppMetadataTable.Cols._COUNT},
null,
null,
null
);
} else {
return new CursorLoader(
activity,
AppProvider.getTopFromCategoryUri(currentCategory, 20),
new String[]{
Schema.AppMetadataTable.Cols.NAME,
Schema.AppMetadataTable.Cols.Package.PACKAGE_NAME,
Schema.AppMetadataTable.Cols.SUMMARY,
Schema.AppMetadataTable.Cols.ICON_URL,
},
null,
null,
null
);
}
}
@Override
public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) {
int topAppsId = currentCategory.hashCode();
int countAllAppsId = topAppsId + 1;
// Anything other than these IDs indicates that the loader which just finished finished
// is no longer the one this view holder is interested in, due to the user having
// scrolled away already during the asynchronous query being run.
if (loader.getId() == topAppsId) {
appCardsAdapter.setAppCursor(cursor);
} else if (loader.getId() == countAllAppsId) {
cursor.moveToFirst();
int numAppsInCategory = cursor.getInt(0);
viewAll.setVisibility(View.VISIBLE);
viewAll.setText(activity.getResources().getQuantityString(R.plurals.button_view_all_apps_in_category, numAppsInCategory, numAppsInCategory));
}
}
@Override
public void onLoaderReset(Loader<Cursor> loader) {
appCardsAdapter.setAppCursor(null);
}
private final View.OnClickListener onViewAll = new View.OnClickListener() {
@Override
public void onClick(View v) {
if (currentCategory == null) {
return;
}
Intent intent = new Intent(activity, AppListActivity.class);
intent.putExtra(AppListActivity.EXTRA_CATEGORY, currentCategory);
activity.startActivity(intent);
}
};
/**
* Applies excessive padding to the start of the first item. This is so that the category artwork
* can peek out and make itself visible. This is RTL friendly.
* @see org.fdroid.fdroid.R.dimen#category_preview__app_list__padding__horizontal
* @see org.fdroid.fdroid.R.dimen#category_preview__app_list__padding__horizontal__first
*/
private static class ItemDecorator extends RecyclerView.ItemDecoration {
private final Context context;
ItemDecorator(Context context) {
this.context = context.getApplicationContext();
}
@Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
int horizontalPadding = (int) context.getResources().getDimension(R.dimen.category_preview__app_list__padding__horizontal);
int horizontalPaddingFirst = (int) context.getResources().getDimension(R.dimen.category_preview__app_list__padding__horizontal__first);
boolean isLtr = ViewCompat.getLayoutDirection(parent) == ViewCompat.LAYOUT_DIRECTION_LTR;
int itemPosition = parent.getChildLayoutPosition(view);
boolean first = itemPosition == 0;
// Leave this "paddingEnd" local variable here for clarity when converting from
// left/right to start/end for RTL friendly layout.
// noinspection UnnecessaryLocalVariable
int paddingEnd = horizontalPadding;
int paddingStart = first ? horizontalPaddingFirst : horizontalPadding;
int paddingLeft = isLtr ? paddingStart : paddingEnd;
int paddingRight = isLtr ? paddingEnd : paddingStart;
outRect.set(paddingLeft, 0, paddingRight, 0);
}
}
}

View File

@ -178,7 +178,9 @@ public class AvailableAppsFragment extends AppListFragment implements
return AppProvider.getRecentlyUpdatedUri(); return AppProvider.getRecentlyUpdatedUri();
} }
if (currentCategory.equals(CategoryProvider.Helper.getCategoryWhatsNew(getActivity()))) { if (currentCategory.equals(CategoryProvider.Helper.getCategoryWhatsNew(getActivity()))) {
return AppProvider.getNewlyAddedUri(); // Removed this feature in the new UI. this fragment will be gone soon so not implementing it again.
// return AppProvider.getNewlyAddedUri();
return AppProvider.getRecentlyUpdatedUri();
} }
return AppProvider.getCategoryUri(currentCategory); return AppProvider.getCategoryUri(currentCategory);
} }

View File

@ -0,0 +1,80 @@
package org.fdroid.fdroid.views.main;
import android.database.Cursor;
import android.os.Bundle;
import android.support.v4.app.LoaderManager;
import android.support.v4.content.CursorLoader;
import android.support.v4.content.Loader;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.view.View;
import android.widget.FrameLayout;
import org.fdroid.fdroid.R;
import org.fdroid.fdroid.data.CategoryProvider;
import org.fdroid.fdroid.data.Schema;
import org.fdroid.fdroid.views.categories.CategoryAdapter;
/**
* Responsible for ensuring that the categories view is inflated and then populated correctly.
* Will start a loader to get the list of categories from the database and populate a recycler
* view with relevant info about each.
*/
class CategoriesViewBinder implements LoaderManager.LoaderCallbacks<Cursor> {
private static final int LOADER_ID = 429820532;
private final CategoryAdapter categoryAdapter;
private final AppCompatActivity activity;
CategoriesViewBinder(AppCompatActivity activity, FrameLayout parent) {
this.activity = activity;
View categoriesView = activity.getLayoutInflater().inflate(R.layout.main_tab_categories, parent, true);
categoryAdapter = new CategoryAdapter(activity, activity.getSupportLoaderManager());
RecyclerView categoriesList = (RecyclerView) categoriesView.findViewById(R.id.category_list);
categoriesList.setHasFixedSize(true);
categoriesList.setLayoutManager(new LinearLayoutManager(activity));
categoriesList.setAdapter(categoryAdapter);
activity.getSupportLoaderManager().initLoader(LOADER_ID, null, this);
}
@Override
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
if (id != LOADER_ID) {
return null;
}
return new CursorLoader(
activity,
CategoryProvider.getAllCategories(),
Schema.CategoryTable.Cols.ALL,
null,
null,
null
);
}
@Override
public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) {
if (loader.getId() != LOADER_ID) {
return;
}
categoryAdapter.setCategoriesCursor(cursor);
}
@Override
public void onLoaderReset(Loader<Cursor> loader) {
if (loader.getId() != LOADER_ID) {
return;
}
categoryAdapter.setCategoriesCursor(null);
}
}

View File

@ -0,0 +1,69 @@
package org.fdroid.fdroid.views.main;
import android.content.Context;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.design.widget.BottomNavigationView;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.LinearLayoutManager;
import android.view.MenuItem;
import android.support.v7.widget.RecyclerView;
import org.fdroid.fdroid.R;
/**
* Main view shown to users upon starting F-Droid.
*
* Shows a bottom navigation bar, with the following entries:
* + Whats new
* + Categories list
* + App swap
* + My apps
* + Settings
*
* Users navigate between items by using the bottom navigation bar, or by swiping left and right.
* When switching from one screen to the next, we stay within this activity. The new screen will
* get inflated (if required)
*/
public class MainActivity extends AppCompatActivity implements BottomNavigationView.OnNavigationItemSelectedListener {
private RecyclerView pager;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
pager = (RecyclerView) findViewById(R.id.main_view_pager);
pager.setHasFixedSize(true);
pager.setLayoutManager(new NonScrollingHorizontalLayoutManager(this));
pager.setAdapter(new MainViewAdapter(this));
BottomNavigationView bottomNavigation = (BottomNavigationView) findViewById(R.id.bottom_navigation);
bottomNavigation.setOnNavigationItemSelectedListener(this);
}
@Override
public boolean onNavigationItemSelected(@NonNull MenuItem item) {
pager.scrollToPosition(((MainViewAdapter) pager.getAdapter()).adapterPositionFromItemId(item.getItemId()));
return true;
}
private static class NonScrollingHorizontalLayoutManager extends LinearLayoutManager {
NonScrollingHorizontalLayoutManager(Context context) {
super(context, LinearLayoutManager.HORIZONTAL, false);
}
@Override
public boolean canScrollHorizontally() {
return false;
}
@Override
public boolean canScrollVertically() {
return false;
}
}
}

View File

@ -0,0 +1,81 @@
package org.fdroid.fdroid.views.main;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.RecyclerView;
import android.util.SparseIntArray;
import android.view.ViewGroup;
import android.widget.FrameLayout;
import org.fdroid.fdroid.R;
/**
* Represents the five main views that are accessible from the main view. These are:
* + Whats new
* + Categories
* + Nearby
* + My Apps
* + Settings
*
* It is responsible for understanding the relationship between each main view that is reachable
* from the bottom navigation, and its position.
*
* It doesn't need to do very much other than redirect requests from the {@link MainActivity}s
* {@link RecyclerView} to the relevant "bind*()" method
* of the {@link MainViewController}.
*/
class MainViewAdapter extends RecyclerView.Adapter<MainViewController> {
private final SparseIntArray positionToId = new SparseIntArray();
private final AppCompatActivity activity;
MainViewAdapter(AppCompatActivity activity) {
this.activity = activity;
setHasStableIds(true);
positionToId.put(0, R.id.whats_new);
positionToId.put(1, R.id.categories);
positionToId.put(2, R.id.nearby);
positionToId.put(3, R.id.my_apps);
positionToId.put(4, R.id.settings);
}
@Override
public MainViewController onCreateViewHolder(ViewGroup parent, int viewType) {
FrameLayout frame = new FrameLayout(activity);
frame.setLayoutParams(new FrameLayout.LayoutParams(ViewGroup.LayoutParams.MATCH_PARENT, ViewGroup.LayoutParams.MATCH_PARENT));
return new MainViewController(activity, frame);
}
@Override
public void onBindViewHolder(MainViewController holder, int position) {
long menuId = getItemId(position);
if (menuId == R.id.whats_new) {
holder.bindWhatsNewView();
} else if (menuId == R.id.categories) {
holder.bindCategoriesView();
} else if (menuId == R.id.nearby) {
holder.bindSwapView();
} else if (menuId == R.id.my_apps) {
holder.bindMyApps();
} else if (menuId == R.id.settings) {
holder.bindSettingsView();
} else {
holder.clearViews();
}
}
@Override
public int getItemCount() {
return positionToId.size();
}
// The RecyclerViewPager and the BottomNavigationView both use menu item IDs to identify pages.
@Override
public long getItemId(int position) {
return positionToId.get(position);
}
public int adapterPositionFromItemId(int itemId) {
return positionToId.indexOfValue(itemId);
}
}

View File

@ -0,0 +1,93 @@
package org.fdroid.fdroid.views.main;
import android.content.Intent;
import android.support.v4.app.Fragment;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.RecyclerView;
import android.view.View;
import android.widget.Button;
import android.widget.FrameLayout;
import org.fdroid.fdroid.R;
import org.fdroid.fdroid.views.fragments.PreferencesFragment;
import org.fdroid.fdroid.views.myapps.MyAppsViewBinder;
import org.fdroid.fdroid.views.swap.SwapWorkflowActivity;
/**
* Decides which view on the main screen to attach to a given {@link FrameLayout}. This class
* doesn't know which view it will be rendering at the time it is constructed. Rather, at some
* point in the future the {@link MainViewAdapter} will have information about which view we
* are required to render, and will invoke the relevant "bind*()" method on this class.
*/
class MainViewController extends RecyclerView.ViewHolder {
private final AppCompatActivity activity;
private final FrameLayout frame;
MainViewController(AppCompatActivity activity, FrameLayout frame) {
super(frame);
this.activity = activity;
this.frame = frame;
}
public void clearViews() {
frame.removeAllViews();
}
/**
* @see WhatsNewViewBinder
*/
public void bindWhatsNewView() {
new WhatsNewViewBinder(activity, frame);
}
/**
* @see MyAppsViewBinder
*/
public void bindMyApps() {
new MyAppsViewBinder(activity, frame);
}
/**
* @see CategoriesViewBinder
*/
public void bindCategoriesView() {
new CategoriesViewBinder(activity, frame);
}
/**
* A splash screen encouraging people to start the swap process.
* The swap process is quite heavy duty in that it fires up Bluetooth and/or WiFi in
* order to scan for peers. As such, it is quite convenient to have a more lightweight view to show
* in the main navigation that doesn't automatically start doing things when the user touches the
* navigation menu in the bottom navigation.
*/
public void bindSwapView() {
View swapView = activity.getLayoutInflater().inflate(R.layout.main_tab_swap, frame, true);
Button startButton = (Button) swapView.findViewById(R.id.button);
startButton.setOnClickListener(new View.OnClickListener() {
@Override
public void onClick(View v) {
activity.startActivity(new Intent(activity, SwapWorkflowActivity.class));
}
});
}
/**
* Attaches a {@link PreferencesFragment} to the view. Everything else is managed by the
* fragment itself, so no further work needs to be done by this view binder.
*
* Note: It is tricky to attach a {@link Fragment} to a view from this view holder. This is due
* to the way in which the {@link RecyclerView} will reuse existing views and ask us to
* put a settings fragment in there at arbitrary times. Usually it wont be the same view we
* attached the fragment to last time, which causes weirdness. The solution is to use code from
* the com.lsjwzh.widget.recyclerviewpager.FragmentStatePagerAdapter which manages this.
* The code has been ported to {@link SettingsView}.
*
* @see SettingsView
*/
public void bindSettingsView() {
activity.getLayoutInflater().inflate(R.layout.main_tab_settings, frame, true);
}
}

View File

@ -0,0 +1,89 @@
package org.fdroid.fdroid.views.main;
import android.annotation.TargetApi;
import android.content.Context;
import android.support.v4.app.Fragment;
import android.support.v4.app.FragmentTransaction;
import android.support.v7.app.AppCompatActivity;
import android.util.AttributeSet;
import android.widget.FrameLayout;
import org.fdroid.fdroid.R;
import org.fdroid.fdroid.views.fragments.PreferencesFragment;
/**
* When attached to the window, the {@link PreferencesFragment} will be added. When detached from
* the window, the fragment will be removed.
*
* Based on code from https://github.com/lsjwzh/RecyclerViewPager/blob/master/lib/src/main/java/com/lsjwzh/widget/recyclerviewpager/FragmentStatePagerAdapter.java
* licensed under the Apache 2.0 license (https://github.com/lsjwzh/RecyclerViewPager/blob/master/LICENSE).
* @see android.support.v4.app.FragmentStatePagerAdapter Much of the code here was ported from this class.
*/
public class SettingsView extends FrameLayout {
private FragmentTransaction currentTransaction;
public SettingsView(Context context) {
super(context);
setId(R.id.preference_fragment_parent);
}
public SettingsView(Context context, AttributeSet attrs) {
super(context, attrs);
setId(R.id.preference_fragment_parent);
}
public SettingsView(Context context, AttributeSet attrs, int defStyleAttr) {
super(context, attrs, defStyleAttr);
setId(R.id.preference_fragment_parent);
}
@TargetApi(21)
public SettingsView(Context context, AttributeSet attrs, int defStyleAttr, int defStyleRes) {
super(context, attrs, defStyleAttr, defStyleRes);
setId(R.id.preference_fragment_parent);
}
@Override
protected void onAttachedToWindow() {
super.onAttachedToWindow();
AppCompatActivity activity = (AppCompatActivity) getContext();
if (activity == null) {
throw new IllegalArgumentException("Cannot add a SettingsView to activities which are not an AppCompatActivity");
}
if (currentTransaction == null) {
currentTransaction = activity.getSupportFragmentManager().beginTransaction();
}
currentTransaction.replace(getId(), new PreferencesFragment(), "preferences-fragment");
currentTransaction.commitAllowingStateLoss();
currentTransaction = null;
activity.getSupportFragmentManager().executePendingTransactions();
}
@Override
protected void onDetachedFromWindow() {
super.onDetachedFromWindow();
AppCompatActivity activity = (AppCompatActivity) getContext();
if (activity == null) {
throw new IllegalArgumentException("Cannot add a SettingsView to activities which are not an AppCompatActivity");
}
Fragment existingFragment = activity.getSupportFragmentManager().findFragmentByTag("preferences-fragment");
if (existingFragment == null) {
return;
}
if (currentTransaction == null) {
currentTransaction = activity.getSupportFragmentManager().beginTransaction();
}
currentTransaction.remove(existingFragment);
currentTransaction.commitAllowingStateLoss();
currentTransaction = null;
activity.getSupportFragmentManager().executePendingTransactions();
}
}

View File

@ -0,0 +1,91 @@
package org.fdroid.fdroid.views.main;
import android.database.Cursor;
import android.os.Bundle;
import android.support.v4.app.LoaderManager;
import android.support.v4.content.CursorLoader;
import android.support.v4.content.Loader;
import android.support.v4.widget.SwipeRefreshLayout;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.GridLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.view.View;
import android.widget.FrameLayout;
import org.fdroid.fdroid.R;
import org.fdroid.fdroid.UpdateService;
import org.fdroid.fdroid.data.AppProvider;
import org.fdroid.fdroid.data.Schema;
import org.fdroid.fdroid.views.whatsnew.WhatsNewAdapter;
/**
* Loads a list of newly added or recently updated apps and displays them to the user.
*/
class WhatsNewViewBinder implements LoaderManager.LoaderCallbacks<Cursor> {
private static final int LOADER_ID = 978015789;
private final WhatsNewAdapter whatsNewAdapter;
private final AppCompatActivity activity;
WhatsNewViewBinder(final AppCompatActivity activity, FrameLayout parent) {
this.activity = activity;
View whatsNewView = activity.getLayoutInflater().inflate(R.layout.main_tab_whats_new, parent, true);
whatsNewAdapter = new WhatsNewAdapter(activity);
GridLayoutManager layoutManager = new GridLayoutManager(activity, 2);
layoutManager.setSpanSizeLookup(new WhatsNewAdapter.SpanSizeLookup());
RecyclerView appList = (RecyclerView) whatsNewView.findViewById(R.id.app_list);
appList.setHasFixedSize(true);
appList.setLayoutManager(layoutManager);
appList.setAdapter(whatsNewAdapter);
final SwipeRefreshLayout swipeToRefresh = (SwipeRefreshLayout) whatsNewView.findViewById(R.id.swipe_to_refresh);
swipeToRefresh.setOnRefreshListener(new SwipeRefreshLayout.OnRefreshListener() {
@Override
public void onRefresh() {
swipeToRefresh.setRefreshing(false);
UpdateService.updateNow(activity);
}
});
activity.getSupportLoaderManager().initLoader(LOADER_ID, null, this);
}
@Override
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
if (id != LOADER_ID) {
return null;
}
return new CursorLoader(
activity,
AppProvider.getRecentlyUpdatedUri(),
Schema.AppMetadataTable.Cols.ALL,
null,
null,
null
);
}
@Override
public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) {
if (loader.getId() != LOADER_ID) {
return;
}
whatsNewAdapter.setAppsCursor(cursor);
}
@Override
public void onLoaderReset(Loader<Cursor> loader) {
if (loader.getId() != LOADER_ID) {
return;
}
whatsNewAdapter.setAppsCursor(null);
}
}

View File

@ -0,0 +1,10 @@
package org.fdroid.fdroid.views.myapps;
import android.support.v7.widget.RecyclerView;
import android.view.View;
public class InstalledHeaderController extends RecyclerView.ViewHolder {
public InstalledHeaderController(View itemView) {
super(itemView);
}
}

View File

@ -0,0 +1,92 @@
package org.fdroid.fdroid.views.myapps;
import android.app.Activity;
import android.database.Cursor;
import android.support.v7.widget.RecyclerView;
import android.view.LayoutInflater;
import android.view.ViewGroup;
import org.fdroid.fdroid.R;
import org.fdroid.fdroid.data.App;
import org.fdroid.fdroid.views.apps.AppListItemController;
import org.fdroid.fdroid.views.apps.AppListItemDivider;
/**
* Wraps a cursor which should have a list of "apps which can be updated". Also includes a header
* as the first element which allows for all items to be updated.
*/
public class MyAppsAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> {
private Cursor updatesCursor;
private final Activity activity;
private final AppListItemDivider divider;
public MyAppsAdapter(Activity activity) {
this.activity = activity;
divider = new AppListItemDivider(activity);
}
@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
LayoutInflater inflater = activity.getLayoutInflater();
switch (viewType) {
case R.id.my_apps__header:
return new UpdatesHeaderController(activity, inflater.inflate(R.layout.my_apps_updates_header, parent, false));
case R.id.my_apps__app:
return new AppListItemController(activity, inflater.inflate(R.layout.app_list_item, parent, false));
default:
throw new IllegalArgumentException();
}
}
@Override
public int getItemCount() {
return updatesCursor == null ? 0 : updatesCursor.getCount() + 1;
}
@Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
switch (getItemViewType(position)) {
case R.id.my_apps__header:
((UpdatesHeaderController) holder).bindModel(updatesCursor.getCount());
break;
case R.id.my_apps__app:
updatesCursor.moveToPosition(position - 1); // Subtract one to account for the header.
((AppListItemController) holder).bindModel(new App(updatesCursor));
break;
default:
throw new IllegalArgumentException();
}
}
@Override
public int getItemViewType(int position) {
if (position == 0) {
return R.id.my_apps__header;
} else {
return R.id.my_apps__app;
}
}
public void setApps(Cursor cursor) {
updatesCursor = cursor;
notifyDataSetChanged();
}
@Override
public void onAttachedToRecyclerView(RecyclerView recyclerView) {
super.onAttachedToRecyclerView(recyclerView);
recyclerView.addItemDecoration(divider);
}
@Override
public void onDetachedFromRecyclerView(RecyclerView recyclerView) {
recyclerView.removeItemDecoration(divider);
super.onDetachedFromRecyclerView(recyclerView);
}
}

View File

@ -0,0 +1,77 @@
package org.fdroid.fdroid.views.myapps;
import android.app.Activity;
import android.database.Cursor;
import android.os.Bundle;
import android.support.v4.app.LoaderManager;
import android.support.v4.content.CursorLoader;
import android.support.v4.content.Loader;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.view.View;
import android.widget.FrameLayout;
import org.fdroid.fdroid.R;
import org.fdroid.fdroid.data.AppProvider;
import org.fdroid.fdroid.data.Schema;
public class MyAppsViewBinder implements LoaderManager.LoaderCallbacks<Cursor> {
private final MyAppsAdapter adapter;
private final Activity activity;
public MyAppsViewBinder(AppCompatActivity activity, FrameLayout parent) {
this.activity = activity;
View myAppsView = activity.getLayoutInflater().inflate(R.layout.main_tabs, parent, true);
adapter = new MyAppsAdapter(activity);
RecyclerView list = (RecyclerView) myAppsView.findViewById(R.id.list);
list.setHasFixedSize(true);
list.setLayoutManager(new LinearLayoutManager(activity));
list.setAdapter(adapter);
LoaderManager loaderManager = activity.getSupportLoaderManager();
loaderManager.initLoader(0, null, this);
}
@Override
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
return new CursorLoader(
activity,
AppProvider.getCanUpdateUri(),
new String[]{
Schema.AppMetadataTable.Cols._ID, // Required for cursor loader to work.
Schema.AppMetadataTable.Cols.Package.PACKAGE_NAME,
Schema.AppMetadataTable.Cols.NAME,
Schema.AppMetadataTable.Cols.SUMMARY,
Schema.AppMetadataTable.Cols.IS_COMPATIBLE,
Schema.AppMetadataTable.Cols.LICENSE,
Schema.AppMetadataTable.Cols.ICON,
Schema.AppMetadataTable.Cols.ICON_URL,
Schema.AppMetadataTable.Cols.InstalledApp.VERSION_CODE,
Schema.AppMetadataTable.Cols.InstalledApp.VERSION_NAME,
Schema.AppMetadataTable.Cols.SuggestedApk.VERSION_NAME,
Schema.AppMetadataTable.Cols.SUGGESTED_VERSION_CODE,
Schema.AppMetadataTable.Cols.REQUIREMENTS, // Needed for filtering apps that require root.
Schema.AppMetadataTable.Cols.ANTI_FEATURES, // Needed for filtering apps that require anti-features.
},
null,
null,
null
);
}
@Override
public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) {
adapter.setApps(cursor);
}
@Override
public void onLoaderReset(Loader<Cursor> loader) {
adapter.setApps(null);
}
}

View File

@ -0,0 +1,38 @@
package org.fdroid.fdroid.views.myapps;
import android.app.Activity;
import android.support.v7.widget.RecyclerView;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;
import org.fdroid.fdroid.R;
import org.fdroid.fdroid.UpdateService;
public class UpdatesHeaderController extends RecyclerView.ViewHolder {
private final Activity activity;
private final TextView updatesHeading;
public UpdatesHeaderController(Activity activity, View itemView) {
super(itemView);
this.activity = activity;
Button updateAll = (Button) itemView.findViewById(R.id.update_all_button);
updateAll.setOnClickListener(onUpdateAll);
updatesHeading = (TextView) itemView.findViewById(R.id.updates_heading);
updatesHeading.setText(activity.getString(R.string.updates));
}
public void bindModel(int numAppsToUpdate) {
updatesHeading.setText(activity.getResources().getQuantityString(R.plurals.my_apps_header_number_of_updateable, numAppsToUpdate, numAppsToUpdate));
}
private final View.OnClickListener onUpdateAll = new View.OnClickListener() {
@Override
public void onClick(View v) {
UpdateService.autoDownloadUpdates(activity);
}
};
}

View File

@ -0,0 +1,143 @@
package org.fdroid.fdroid.views.whatsnew;
import android.app.Activity;
import android.content.Context;
import android.database.Cursor;
import android.graphics.Rect;
import android.support.v4.view.ViewCompat;
import android.support.v7.widget.GridLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.view.View;
import android.view.ViewGroup;
import org.fdroid.fdroid.R;
import org.fdroid.fdroid.data.App;
import org.fdroid.fdroid.views.categories.AppCardController;
public class WhatsNewAdapter extends RecyclerView.Adapter<AppCardController> {
private Cursor cursor;
private final Activity activity;
private final RecyclerView.ItemDecoration appListDecorator;
public WhatsNewAdapter(Activity activity) {
this.activity = activity;
appListDecorator = new WhatsNewAdapter.ItemDecorator(activity);
}
@Override
public void onAttachedToRecyclerView(RecyclerView recyclerView) {
super.onAttachedToRecyclerView(recyclerView);
recyclerView.addItemDecoration(appListDecorator);
}
@Override
public void onDetachedFromRecyclerView(RecyclerView recyclerView) {
recyclerView.removeItemDecoration(appListDecorator);
super.onDetachedFromRecyclerView(recyclerView);
}
@Override
public AppCardController onCreateViewHolder(ViewGroup parent, int viewType) {
int layout;
if (viewType == R.id.whats_new_feature) {
layout = R.layout.app_card_featured;
} else if (viewType == R.id.whats_new_large_tile) {
layout = R.layout.app_card_large;
} else if (viewType == R.id.whats_new_small_tile) {
layout = R.layout.app_card_horizontal;
} else if (viewType == R.id.whats_new_regular_list) {
layout = R.layout.app_card_list_item;
} else {
throw new IllegalArgumentException("Unknown view type when rendering \"Whats New\": " + viewType);
}
return new AppCardController(activity, activity.getLayoutInflater().inflate(layout, parent, false));
}
@Override
public int getItemViewType(int position) {
if (position == 0) {
return R.id.whats_new_feature;
} else if (position <= 2) {
return R.id.whats_new_large_tile;
} else if (position <= 4) {
return R.id.whats_new_small_tile;
} else {
return R.id.whats_new_regular_list;
}
}
@Override
public void onBindViewHolder(AppCardController holder, int position) {
cursor.moveToPosition(position);
holder.bindApp(new App(cursor));
}
@Override
public int getItemCount() {
return cursor == null ? 0 : cursor.getCount();
}
public void setAppsCursor(Cursor cursor) {
this.cursor = cursor;
notifyDataSetChanged();
}
// TODO: Replace with https://github.com/lucasr/twoway-view which looks really really cool, but
// no longer under active development (despite heaps of forks/stars on github).
public static class SpanSizeLookup extends GridLayoutManager.SpanSizeLookup {
@Override
public int getSpanSize(int position) {
if (position == 0) {
return 2;
} else if (position <= 4) {
return 1;
} else {
return 2;
}
}
}
/**
* Applies padding to items, ensuring that the spacing on the left, centre, and right all match.
* The vertical padding is slightly shorter than the horizontal padding also.
* @see org.fdroid.fdroid.R.dimen#whats_new__padding__app_card__horizontal
* @see org.fdroid.fdroid.R.dimen#whats_new__padding__app_card__vertical
*/
private class ItemDecorator extends RecyclerView.ItemDecoration {
private final Context context;
ItemDecorator(Context context) {
this.context = context.getApplicationContext();
}
@Override
public void getItemOffsets(Rect outRect, View view, RecyclerView parent, RecyclerView.State state) {
int position = parent.getChildAdapterPosition(view);
int horizontalPadding = (int) context.getResources().getDimension(R.dimen.whats_new__padding__app_card__horizontal);
int verticalPadding = (int) context.getResources().getDimension(R.dimen.whats_new__padding__app_card__vertical);
if (position == 0) {
// Don't set any padding for the first item as the FeatureImage behind it needs to butt right
// up against the left/top/right of the screen.
outRect.set(0, 0, 0, verticalPadding);
} else if (position <= 4) {
// Odd items are on the left, even on the right.
// The item on the left will have both left and right padding. The item on the right
// will only have padding on the right. This will allow the same amount of padding
// on the left, centre, and right of the grid, rather than double the padding in the
// middle (which would happen if both left+right paddings were set for both items).
boolean isLtr = ViewCompat.getLayoutDirection(parent) == ViewCompat.LAYOUT_DIRECTION_LTR;
boolean isAtStart = (position % 2) == 1;
int paddingStart = isAtStart ? horizontalPadding : 0;
int paddingLeft = isLtr ? paddingStart : horizontalPadding;
int paddingRight = isLtr ? horizontalPadding : paddingStart;
outRect.set(paddingLeft, 0, paddingRight, verticalPadding);
} else {
outRect.set(horizontalPadding, 0, horizontalPadding, verticalPadding);
}
}
}
}

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Used to separate two sepparate R.layout.app_list_item views in a list.
As these are not cards, they don't have their own drop shadow or other features that help
separate different list items.
-->
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<size android:width="1dp" android:height="1dp" />
<solid android:color="#ffe3e3e3" />
</shape>

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
A category chip displays an icon on the left and the category label on the right. It follows the
material design specification at https://material.google.com/components/chips.html#chips-contact-chips.
Most of the actual drawing is done in the Java code rather than here.
-->
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<corners android:radius="16dp" />
<solid android:color="#FFCCCCCC" />
</shape>

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Visually different from other app cards because it doesn't have a drop shadow, and has a larger
corner radius.
-->
<shape xmlns:android="http://schemas.android.com/apk/res/android">
<corners android:radius="4dp" />
<solid android:color="#faf8ef" />
</shape>

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<!--
Shown in the app list item as a shortcut for the user to be able to download/install an app.
-->
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@drawable/ic_download_button" />
</selector>

View File

@ -0,0 +1,5 @@
<vector android:autoMirrored="true" android:height="24dp"
android:viewportHeight="24.0" android:viewportWidth="24.0"
android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#FF000000" android:pathData="M20,11H7.83l5.59,-5.59L12,4l-8,8 8,8 1.41,-1.41L7.83,13H20v-2z"/>
</vector>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M20,6h-8l-2,-2L4,4c-1.1,0 -1.99,0.9 -1.99,2L2,18c0,1.1 0.9,2 2,2h16c1.1,0 2,-0.9 2,-2L22,8c0,-1.1 -0.9,-2 -2,-2zM20,18L4,18L4,8h16v10z"/>
</vector>

View File

@ -0,0 +1,9 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0">
<path
android:fillColor="#FF000000"
android:pathData="M19,6.41L17.59,5 12,10.59 6.41,5 5,6.41 10.59,12 5,17.59 6.41,19 12,13.41 17.59,19 19,17.59 13.41,12z"/>
</vector>

View File

@ -0,0 +1,13 @@
<vector android:height="32dp" android:viewportHeight="74.53289"
android:viewportWidth="74.53289" android:width="32dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillAlpha="1" android:fillColor="#0066cc"
android:pathData="m37.27,0c-20.57,0 -37.27,16.7 -37.27,37.27 0,20.57 16.7,37.27 37.27,37.27 20.57,0 37.27,-16.7 37.27,-37.27 0,-20.57 -16.7,-37.27 -37.27,-37.27zM37.27,2c19.49,0 35.27,15.78 35.27,35.27 0,19.49 -15.78,35.27 -35.27,35.27 -19.49,0 -35.27,-15.78 -35.27,-35.27 0,-19.49 15.78,-35.27 35.27,-35.27z"
android:strokeAlpha="1" android:strokeColor="#00000000"
android:strokeLineCap="butt" android:strokeLineJoin="miter" android:strokeWidth="2"/>
<path android:fillAlpha="1" android:fillColor="#0066cc"
android:pathData="M23.05,49.12h27.97v4.04h-27.97z"
android:strokeAlpha="1" android:strokeColor="#00000000" android:strokeWidth="2"/>
<path android:fillAlpha="1" android:fillColor="#0066cc"
android:pathData="m31.07,19.19 l0,12.18 -7.71,0 13.86,13.57 13.86,-13.57 -7.83,0 0,-12.18z"
android:strokeAlpha="1" android:strokeColor="#00000000" android:strokeWidth="2"/>
</vector>

View File

@ -0,0 +1,6 @@
<vector android:height="24dp" android:viewportHeight="64.0"
android:viewportWidth="64.0" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillAlpha="1" android:fillColor="#ffffff"
android:pathData="m32,0a32,32 0,0 0,-32 32,32 32,0 0,0 32,32 32,32 0,0 0,32 -32,32 32,0 0,0 -32,-32zM32.56,10.63a9.46,9.46 0,0 1,9.46 9.46,9.46 9.46,0 0,1 -9.46,9.46 9.46,9.46 0,0 1,-9.46 -9.46,9.46 9.46,0 0,1 9.46,-9.46zM32,36.45a33.39,33.39 0,0 1,20.37 6.98,23.37 23.37,0 0,1 -20.37,11.94 23.37,23.37 0,0 1,-20.37 -11.96,33.39 33.39,0 0,1 20.37,-6.96z"
android:strokeAlpha="1" android:strokeColor="#00000000" android:strokeWidth="2"/>
</vector>

View File

@ -0,0 +1,12 @@
<vector android:height="24dp" android:viewportHeight="64.0"
android:viewportWidth="64.0" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillAlpha="1" android:fillColor="#ffffff"
android:pathData="m31.52,0.48a31.52,31.52 45.69,0 0,-31.52 31.52,31.52 31.52,45.64 0,0 0.45,5.11 16.53,16.53 53.72,0 1,4.52 -2.34,26.71 26.71,48.01 0,1 -0.18,-2.78 26.71,26.71 49.01,0 1,26.72 -26.71,26.71 26.71,48.46 0,1 23.41,13.91 14.8,14.8 67.3,0 1,2.73 -0.26,14.8 14.8,67.32 0,1 2.62,0.24 31.52,31.52 0,0 0,-28.76 -18.69zM31.52,14.32a17.68,17.68 99.47,0 0,-17.68 17.68,17.68 17.68,99.47 0,0 0.18,2.39 16.53,16.53 53.72,0 1,12.62 14.6,17.68 17.68,99.47 0,0 4.88,0.69 17.68,17.68 99.2,0 0,14.22 -7.21,14.8 14.8,66.35 0,1 -2.88,-8.75 14.8,14.8 67.3,0 1,4.14 -10.27,17.68 17.68,99.2 0,0 -15.48,-9.15zM31.52,21.62a10.38,10.38 75.21,0 1,10.38 10.38,10.38 10.38,75.3 0,1 -10.38,10.38 10.38,10.38 76.55,0 1,-10.38 -10.38,10.38 10.38,76.21 0,1 10.38,-10.38zM53.04,47.77a26.71,26.71 48.42,0 1,-21.52 10.94,26.71 26.71,48.96 0,1 -6.56,-0.85 16.53,16.53 54.54,0 1,-2.96 4.15,31.52 31.52,45.67 0,0 9.52,1.51 31.52,31.52 47.06,0 0,26.83 -15.03,14.8 14.8,67.3 0,1 -0.69,0.04 14.8,14.8 67.32,0 1,-4.61 -0.76z"
android:strokeAlpha="1" android:strokeColor="#00000000" android:strokeWidth="2"/>
<path android:fillAlpha="1" android:fillColor="#ffffff"
android:pathData="M10.19,50.45m-7.88,0a7.88,7.88 53.72,1 1,15.76 0a7.88,7.88 53.29,1 1,-15.76 0"
android:strokeAlpha="1" android:strokeColor="#00000000" android:strokeWidth="2"/>
<path android:fillAlpha="1" android:fillColor="#ffffff"
android:pathData="M57.66,33.73m-6.34,0a6.34,6.34 114.83,1 1,12.68 0a6.34,6.34 114.83,1 1,-12.68 0"
android:strokeAlpha="1" android:strokeColor="#00000000" android:strokeWidth="2"/>
</vector>

View File

@ -0,0 +1,6 @@
<vector android:height="24dp" android:viewportHeight="64.0"
android:viewportWidth="64.0" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillAlpha="1" android:fillColor="#ffffff"
android:pathData="m40.15,0.71 l-9.57,5.97 -10.14,-4.94 -4.23,10.45 -11.11,1.96 2.72,10.95 -7.83,8.11 8.63,7.25 -1.56,11.17 11.25,0.79 5.3,9.95 9.57,-5.97 10.14,4.94 4.23,-10.45 11.11,-1.96 -2.72,-10.94 7.83,-8.12 -8.63,-7.25 1.56,-11.17 -11.25,-0.79 -5.3,-9.95zM28.45,15.95 L34.06,15.95 34.06,32.52 28.45,32.52 28.45,15.95zM28.45,38 L34.06,38 34.06,43.6 28.45,43.6 28.45,38z"
android:strokeAlpha="1" android:strokeColor="#00000000" android:strokeWidth="2"/>
</vector>

View File

@ -0,0 +1,6 @@
<vector android:height="24dp" android:viewportHeight="64.0"
android:viewportWidth="64.0" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillAlpha="1" android:fillColor="#ffffff"
android:pathData="m23.4,0.01 l0,8.54c0.02,1.79 -5.02,5.64 -7.4,4.27l-7.4,-4.27 -8.6,14.9 7.4,4.27c2.65,1.48 2.93,6.85 0,8.54l-7.4,4.27 8.6,14.9 7.4,-4.27c2.29,-1.36 7.4,0.81 7.4,4.27l0,8.55 17.2,0 0,-8.55c0,-3.23 4.16,-6.14 7.4,-4.27l7.4,4.27 8.6,-14.9 -7.4,-4.27c-2.47,-1.43 -2.98,-6.82 0,-8.54l7.4,-4.27 -8.6,-14.9 -7.4,4.27c-2.16,1.29 -7.4,-0.67 -7.4,-4.27l0,-8.54 -17.2,0zM32.14,20.4a11.76,11.76 0,0 1,11.76 11.76,11.76 11.76,0 0,1 -11.76,11.76 11.76,11.76 0,0 1,-11.75 -11.76,11.76 11.76,0 0,1 11.75,-11.76z"
android:strokeAlpha="1" android:strokeColor="#00000000" android:strokeWidth="2"/>
</vector>

View File

@ -0,0 +1,69 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<android.support.v7.widget.CardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="12dp">
<android.support.constraint.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="16dp"
android:layout_marginLeft="16dp"
android:layout_marginEnd="16dp"
android:layout_marginRight="16dp">
<ImageView
android:id="@+id/back"
android:contentDescription="@string/back"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:srcCompat="@drawable/ic_back_black_24dp" />
<EditText
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toEndOf="@+id/back"
app:layout_constraintEnd_toStartOf="@+id/clear"
android:padding="12dp"
android:id="@+id/search"
android:hint="@string/search_hint"
android:background="@android:color/transparent"
android:textSize="18sp" />
<ImageView
android:id="@+id/clear"
android:contentDescription="@string/clear_search"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:srcCompat="@drawable/ic_close_black_24dp" />
</android.support.constraint.ConstraintLayout>
</android.support.v7.widget.CardView>
<android.support.v7.widget.RecyclerView
android:id="@+id/app_list"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:listitem="@layout/app_list_item"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
android:scrollbars="vertical" />
</LinearLayout>

View File

@ -0,0 +1,27 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<android.support.design.widget.BottomNavigationView
android:id="@+id/bottom_navigation"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentBottom="true"
app:menu="@menu/main_activity_screens"
android:background="@color/fdroid_blue"
app:itemBackground="@color/fdroid_blue"
app:itemIconTint="@android:color/white"
app:itemTextColor="@android:color/white" />
<android.support.v7.widget.RecyclerView
android:id="@+id/main_view_pager"
android:layout_alignParentTop="true"
android:layout_above="@+id/bottom_navigation"
android:layout_width="match_parent"
android:layout_height="match_parent" />
</RelativeLayout>

View File

@ -0,0 +1,86 @@
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingBottom="2dp"
android:clipToPadding="false">
<ImageView
android:id="@+id/featured_image"
android:layout_width="0dp"
android:layout_height="120dp"
tools:src="@color/fdroid_green"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<android.support.constraint.Guideline
android:id="@+id/header_height"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
app:layout_constraintGuide_begin="80dp" />
<android.support.v7.widget.CardView
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginLeft="@dimen/whats_new__padding__app_card__horizontal"
android:layout_marginStart="@dimen/whats_new__padding__app_card__horizontal"
android:layout_marginRight="@dimen/whats_new__padding__app_card__horizontal"
android:layout_marginEnd="@dimen/whats_new__padding__app_card__horizontal"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@+id/header_height"
>
<android.support.constraint.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="12dp">
<ImageView
android:id="@+id/icon"
android:contentDescription="@string/app_icon"
android:layout_width="48dp"
android:layout_height="48dp"
tools:src="@drawable/ic_launcher"
android:scaleType="fitCenter"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
tools:text="F-Droid An application summary which takes up too much space and must ellipsize, perhaps after wrapping to a new line"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:id="@+id/summary"
android:lines="2"
android:textSize="14sp"
android:ellipsize="end"
android:layout_marginLeft="16dp"
android:layout_marginStart="16dp"
app:layout_constraintStart_toEndOf="@+id/icon"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
tools:text="Recently added"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/status"
android:lines="1"
android:textSize="12sp"
android:ellipsize="end"
android:layout_marginLeft="16dp"
android:layout_marginStart="16dp"
app:layout_constraintTop_toBottomOf="@+id/summary"
app:layout_constraintStart_toEndOf="@+id/icon"
android:textStyle="italic"
android:layout_marginTop="8dp" />
</android.support.constraint.ConstraintLayout>
</android.support.v7.widget.CardView>
</android.support.constraint.ConstraintLayout>

View File

@ -0,0 +1,52 @@
<?xml version="1.0" encoding="utf-8"?>
<android.support.v7.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<android.support.constraint.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_margin="8dp">
<ImageView
android:id="@+id/icon"
android:contentDescription="@string/app_icon"
android:layout_width="48dp"
android:layout_height="48dp"
tools:src="@drawable/ic_launcher"
android:scaleType="fitCenter"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
tools:text="F-Droid An application summary which takes up too much space and must ellipsize"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:id="@+id/summary"
android:lines="4"
android:textSize="13sp"
android:ellipsize="end"
android:layout_marginLeft="8dp"
android:layout_marginStart="8dp"
app:layout_constraintStart_toEndOf="@+id/icon"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
tools:text="Recently added"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/status"
android:lines="1"
android:textSize="12sp"
android:ellipsize="end"
app:layout_constraintTop_toBottomOf="@+id/summary"
app:layout_constraintStart_toStartOf="parent"
android:textStyle="italic"
android:layout_marginTop="4dp" />
</android.support.constraint.ConstraintLayout>
</android.support.v7.widget.CardView>

View File

@ -0,0 +1,58 @@
<?xml version="1.0" encoding="utf-8"?>
<android.support.v7.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="220dp">
<android.support.constraint.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_margin="8dp">
<ImageView
android:id="@+id/icon"
android:contentDescription="@string/app_icon"
android:layout_width="96dip"
android:layout_height="96dip"
tools:src="@drawable/ic_launcher"
android:scaleType="fitCenter"
android:layout_marginEnd="16dp"
android:layout_marginRight="16dp"
android:layout_marginStart="16dp"
android:layout_marginLeft="16dp"
android:layout_marginTop="8dp"
android:layout_marginBottom="8dp"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
tools:text="F-Droid An application summary which takes up too much space and must ellipsize"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/summary"
android:maxLines="4"
android:textSize="14sp"
android:ellipsize="end"
app:layout_constraintTop_toBottomOf="@+id/icon"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
android:layout_marginTop="8dp" />
<TextView
tools:text="Recently added"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/status"
android:lines="1"
android:textSize="12sp"
android:ellipsize="end"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintStart_toStartOf="parent"
android:textStyle="italic"
android:layout_marginTop="8dp" />
</android.support.constraint.ConstraintLayout>
</android.support.v7.widget.CardView>

View File

@ -0,0 +1,55 @@
<?xml version="1.0" encoding="utf-8"?>
<android.support.v7.widget.CardView xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<android.support.constraint.ConstraintLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_margin="8dp">
<ImageView
android:id="@+id/icon"
android:contentDescription="@string/app_icon"
android:layout_width="48dp"
android:layout_height="48dp"
tools:src="@drawable/ic_launcher"
android:scaleType="fitCenter"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent" />
<TextView
tools:text="F-Droid An application summary which takes up too much space and must ellipsize"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:id="@+id/summary"
android:lines="2"
android:textSize="14sp"
android:ellipsize="end"
android:layout_marginLeft="16dp"
android:layout_marginStart="16dp"
app:layout_constraintStart_toEndOf="@+id/icon"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:layout_editor_absoluteY="8dp" />
<TextView
tools:text="Recently added"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/status"
android:lines="1"
android:textSize="12sp"
android:ellipsize="end"
android:layout_marginLeft="16dp"
android:layout_marginStart="16dp"
app:layout_constraintTop_toBottomOf="@+id/summary"
app:layout_constraintStart_toEndOf="@+id/icon"
android:textStyle="italic"
android:layout_marginTop="8dp" />
</android.support.constraint.ConstraintLayout>
</android.support.v7.widget.CardView>

View File

@ -0,0 +1,41 @@
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="100dp"
android:layout_height="130dp"
android:background="@drawable/category_preview_app_card_background"
android:padding="8dp">
<!-- Ignore ContentDescription because it is kind of meaningless to have TTS read out "App icon"
when it will inevitably read out the name of the app straight after. -->
<ImageView
android:id="@+id/icon"
android:layout_width="48dip"
android:layout_height="48dip"
tools:src="@drawable/ic_launcher"
android:scaleType="fitCenter"
android:layout_marginEnd="16dp"
android:layout_marginRight="16dp"
android:layout_marginStart="16dp"
android:layout_marginLeft="16dp"
android:layout_marginTop="8dp"
android:layout_marginBottom="8dp"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:ignore="ContentDescription" />
<TextView
tools:text="F-Droid An application summary which takes up too much space and must ellipsize"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:id="@+id/summary"
android:maxLines="3"
android:ellipsize="end"
app:layout_constraintTop_toBottomOf="@+id/icon"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
android:layout_marginTop="8dp" />
</android.support.constraint.ConstraintLayout>

View File

@ -0,0 +1,67 @@
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content">
<ImageView
android:id="@+id/icon"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
android:contentDescription="@string/app_icon"
android:layout_width="48dp"
android:layout_height="48dp"
tools:src="@drawable/ic_launcher"
android:scaleType="fitCenter"
android:layout_marginStart="16dp"
android:layout_marginLeft="16dp"
android:layout_marginTop="8dp" />
<TextView
android:id="@+id/app_name"
android:layout_width="0dp"
android:layout_height="wrap_content"
tools:text="F-Droid Application manager with a long name that will wrap and then ellipsize"
android:textSize="18sp"
android:textColor="#424242"
android:lines="2"
android:ellipsize="end"
app:layout_constraintStart_toEndOf="@+id/icon"
app:layout_constraintTop_toTopOf="@+id/icon"
android:layout_marginLeft="8dp"
android:layout_marginStart="8dp"
app:layout_constraintEnd_toStartOf="@+id/install"
android:layout_marginEnd="8dp"
android:layout_marginRight="8dp" />
<TextView
android:id="@+id/status"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
tools:text="Installed"
android:textStyle="italic"
android:textSize="14sp"
android:textColor="#424242"
android:maxLines="1"
android:ellipsize="end"
android:fontFamily="sans-serif-light"
app:layout_constraintTop_toBottomOf="@+id/app_name"
app:layout_constraintStart_toEndOf="@+id/icon"
android:layout_marginStart="8dp"
android:layout_marginLeft="8dp" />
<Button
android:id="@+id/install"
android:background="@drawable/download_button"
android:layout_width="48dp"
android:layout_height="48dp"
android:padding="4dp"
android:layout_marginEnd="16dp"
android:layout_marginRight="16dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@+id/icon" />
</android.support.constraint.ConstraintLayout>

View File

@ -0,0 +1,80 @@
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:orientation="vertical" android:layout_width="match_parent"
android:layout_height="wrap_content">
<TextView
android:id="@+id/name"
tools:text="Business"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintBaseline_toBaselineOf="@+id/button"
android:textAppearance="@style/TextAppearance.AppCompat.Headline"
android:textSize="18sp"
android:textColor="#4a4a4a"
android:paddingLeft="18dp"
android:paddingStart="18dp"
android:paddingRight="18dp"
android:paddingEnd="18dp"
tools:layout_editor_absoluteX="0dp" />
<Button
android:id="@+id/button"
tools:text="View all 10"
android:layout_width="wrap_content"
android:layout_height="0dp"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintEnd_toEndOf="parent"
android:background="@android:color/transparent"
android:paddingLeft="18dp"
android:paddingStart="18dp"
android:paddingRight="18dp"
android:paddingEnd="18dp"
android:paddingTop="24dp"
android:paddingBottom="12dp"
android:textSize="14sp"
android:textAllCaps="true"
android:textColor="@color/fdroid_blue"
tools:layout_editor_absoluteX="268dp" />
<FrameLayout
android:id="@+id/category_background"
android:layout_width="0dp"
android:layout_height="0dp"
app:layout_constraintTop_toTopOf="@+id/app_cards"
app:layout_constraintBottom_toBottomOf="@+id/app_cards"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
tools:background="#ffffbbbb"/>
<ImageView
android:id="@+id/category_image"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/button"
android:layout_width="100dp"
android:layout_height="100dp"
tools:ignore="ContentDescription" />
<android.support.v7.widget.RecyclerView
android:id="@+id/app_cards"
tools:listitem="@layout/app_card_normal"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@+id/button"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layoutManager="LinearLayoutManager"
android:orientation="horizontal"
android:paddingTop="@dimen/category_preview__app_list__padding__vertical"
android:paddingBottom="@dimen/category_preview__app_list__padding__vertical"
android:clipToPadding="false"
android:layout_marginLeft="8dp"
android:layout_marginRight="8dp"/>
</android.support.constraint.ConstraintLayout>

View File

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<android.support.v7.widget.RecyclerView
android:id="@+id/category_list"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:listitem="@layout/category_item"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
android:scrollbars="vertical" />
</LinearLayout>

View File

@ -0,0 +1,7 @@
<?xml version="1.0" encoding="utf-8"?>
<org.fdroid.fdroid.views.main.SettingsView
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent">
</org.fdroid.fdroid.views.main.SettingsView>

View File

@ -0,0 +1,62 @@
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<ImageView
android:id="@+id/image"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:srcCompat="@drawable/swap_start_header"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintLeft_toLeftOf="parent" />
<TextView
android:id="@+id/text1"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:text="Download apps from people near you."
android:textSize="20sp"
android:textAlignment="center"
app:layout_constraintTop_toBottomOf="@+id/image"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
android:layout_marginTop="32dp"
android:layout_marginEnd="48dp"
android:layout_marginRight="48dp"
android:layout_marginStart="48dp"
android:layout_marginLeft="48dp" />
<!-- TODO: Use @string/app_name instead of "F-Droid". -->
<!-- TODO: The swap process helps to get F-Droid to the other user. That should probably be made a bit clearer here. -->
<TextView
android:id="@+id/text2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Participants must have F-Droid installed."
android:textSize="14sp"
android:textAlignment="center"
app:layout_constraintTop_toBottomOf="@+id/text1"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
android:layout_marginTop="16dp"
android:layout_marginEnd="48dp"
android:layout_marginRight="48dp"
android:layout_marginStart="48dp"
android:layout_marginLeft="48dp" />
<Button
android:id="@+id/button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="Find people near me"
app:layout_constraintTop_toBottomOf="@+id/text2"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
android:layout_marginTop="16dp" />
</android.support.constraint.ConstraintLayout>

View File

@ -0,0 +1,26 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<android.support.v4.widget.SwipeRefreshLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/swipe_to_refresh">
<android.support.v7.widget.RecyclerView
android:id="@+id/app_list"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:listitem="@layout/app_card_normal"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toRightOf="parent"
app:layout_constraintTop_toTopOf="parent"
android:scrollbars="vertical" />
</android.support.v4.widget.SwipeRefreshLayout>
</LinearLayout>

View File

@ -0,0 +1,19 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent">
<android.support.v7.widget.RecyclerView
android:id="@+id/list"
tools:listitem="@layout/app_list_item"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scrollbars="vertical"
app:layout_constraintTop_toBottomOf="@+id/update_all_button"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent" />
</LinearLayout>

View File

@ -0,0 +1,32 @@
<?xml version="1.0" encoding="utf-8"?>
<RelativeLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:paddingLeft="16dp"
android:paddingStart="16dp"
android:paddingRight="16dp"
android:paddingEnd="16dp"
android:layout_marginTop="16dp"
android:paddingBottom="8dp"
android:clipToPadding="false">
<Button
android:id="@+id/update_all_button"
android:text="@string/my_apps_btn_update_all"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentRight="true"
android:layout_alignParentEnd="true" />
<TextView
android:id="@+id/updates_heading"
tools:text="2 Updates"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_alignBaseline="@+id/update_all_button" />
</RelativeLayout>

View File

@ -0,0 +1,24 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical"
android:layout_width="match_parent"
android:layout_height="match_parent">
<android.support.v7.widget.Toolbar
android:id="@+id/toolbar"
android:title="@string/menu_manage"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:theme="?attr/actionBarTheme"
app:popupTheme="?attr/actionBarPopupTheme" />
<ListView
android:id="@+id/list"
android:layout_width="match_parent"
android:layout_height="match_parent"
tools:listitem="@layout/repo_item"
android:scrollbars="vertical" />
</LinearLayout>

View File

@ -1,5 +1,19 @@
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android" <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="match_parent"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:orientation="vertical">
<android.support.v7.widget.Toolbar
android:id="@+id/toolbar"
android:title="@string/repo_details"
android:layout_width="match_parent"
android:layout_height="wrap_content"
app:theme="?attr/actionBarTheme"
app:popupTheme="?attr/actionBarPopupTheme" />
<ScrollView
android:layout_width="match_parent" android:layout_width="match_parent"
android:layout_height="match_parent"> android:layout_height="match_parent">
@ -100,3 +114,5 @@
</LinearLayout> </LinearLayout>
</ScrollView> </ScrollView>
</LinearLayout>

View File

@ -0,0 +1,29 @@
<?xml version="1.0" encoding="utf-8"?>
<menu xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:title="@string/main_menu__latest_apps"
android:icon="@drawable/ic_overview"
app:showAsAction="ifRoom"
android:id="@+id/whats_new" />
<item
android:title="@string/main_menu__categories"
android:icon="@drawable/ic_category"
app:showAsAction="ifRoom"
android:id="@+id/categories" />
<item
android:title="@string/main_menu__swap_nearby"
android:icon="@drawable/ic_nearby"
app:showAsAction="ifRoom"
android:id="@+id/nearby" />
<item
android:title="@string/main_menu__my_apps"
android:icon="@drawable/ic_my_apps"
app:showAsAction="ifRoom"
android:id="@+id/my_apps" />
<item
android:title="@string/menu_settings"
android:icon="@drawable/ic_settings"
app:showAsAction="ifRoom"
android:id="@+id/settings" />
</menu>

View File

@ -18,4 +18,13 @@
<!-- The selected item stands out from the background by this elevation --> <!-- The selected item stands out from the background by this elevation -->
<dimen name="details_screenshot_selected_elevation">3dp</dimen> <dimen name="details_screenshot_selected_elevation">3dp</dimen>
<dimen name="whats_new__padding__app_card__horizontal">12dp</dimen>
<dimen name="whats_new__padding__app_card__vertical">10dp</dimen>
<dimen name="category_preview__app_list__padding__horizontal">4dp</dimen>
<dimen name="category_preview__app_list__padding__horizontal__first">72dp</dimen>
<dimen name="category_preview__app_list__padding__vertical">18dp</dimen>
<dimen name="category_preview__padding__recycler_view__top">12dp</dimen>
<dimen name="category_preview__padding__app_card__horizontal">3dp</dimen>
<dimen name="category_preview__padding__app_card__vertical">4dp</dimen>
</resources> </resources>

View File

@ -2,4 +2,13 @@
<resources> <resources>
<item type="id" name="category_spinner" /> <item type="id" name="category_spinner" />
<item type="id" name="appDetailsSummaryHeader" /> <item type="id" name="appDetailsSummaryHeader" />
<item type="id" name="preference_fragment_parent" />
<item type="id" name="whats_new_feature" />
<item type="id" name="whats_new_large_tile" />
<item type="id" name="whats_new_small_tile" />
<item type="id" name="whats_new_regular_list" />
<item type="id" name="my_apps__header" />
<item type="id" name="my_apps__app" />
</resources> </resources>

View File

@ -63,9 +63,17 @@
<string name="app_not_installed">Not Installed</string> <string name="app_not_installed">Not Installed</string>
<string name="app_inst_known_source">Installed (from %s)</string> <string name="app_inst_known_source">Installed (from %s)</string>
<string name="app_inst_unknown_source">Installed (from unknown source)</string> <string name="app_inst_unknown_source">Installed (from unknown source)</string>
<string name="app_version_x_available">Version %1$s available</string>
<string name="app_version_x_installed">Version %1$s</string>
<string name="added_on">Added on %s</string> <string name="added_on">Added on %s</string>
<string name="my_apps_btn_update_all">Update all</string>
<plurals name="my_apps_header_number_of_updateable">
<item quantity="one">%1$d Update</item>
<item quantity="other">%1$d Updates</item>
</plurals>
<string name="ok">OK</string> <string name="ok">OK</string>
<string name="yes">Yes</string> <string name="yes">Yes</string>
@ -82,6 +90,7 @@
<string name="enable">Enable</string> <string name="enable">Enable</string>
<string name="add_key">Add Key</string> <string name="add_key">Add Key</string>
<string name="overwrite">Overwrite</string> <string name="overwrite">Overwrite</string>
<string name="clear_search">Clear search</string>
<string name="tab_available_apps">Available</string> <string name="tab_available_apps">Available</string>
<string name="tab_installed_apps">Installed</string> <string name="tab_installed_apps">Installed</string>
@ -130,6 +139,11 @@
<string name="menu_litecoin">Litecoin</string> <string name="menu_litecoin">Litecoin</string>
<string name="menu_flattr">Flattr</string> <string name="menu_flattr">Flattr</string>
<string name="main_menu__latest_apps">Latest</string>
<string name="main_menu__categories">Categories</string>
<string name="main_menu__swap_nearby">Nearby</string>
<string name="main_menu__my_apps">My Apps</string>
<string name="details_installed">Version %s installed</string> <string name="details_installed">Version %s installed</string>
<string name="details_notinstalled">Not installed</string> <string name="details_notinstalled">Not installed</string>
@ -264,6 +278,13 @@
<string name="category_Time">Time</string> <string name="category_Time">Time</string>
<string name="category_Writing">Writing</string> <string name="category_Writing">Writing</string>
<plurals name="button_view_all_apps_in_category">
<!-- Even though these are the same as eachother, Android docs suggest always specifying at
least "one" and "other": https://developer.android.com/guide/topics/resources/string-resource.html#Plurals -->
<item quantity="one">View all %d</item>
<item quantity="other">View all %d</item>
</plurals>
<string name="empty_installed_app_list">No apps installed.\n\nThere are apps on your device, but they are not available from F-Droid. This could be because you need to update your repositories, or the repositories genuinely don\'t have your apps available.</string> <string name="empty_installed_app_list">No apps installed.\n\nThere are apps on your device, but they are not available from F-Droid. This could be because you need to update your repositories, or the repositories genuinely don\'t have your apps available.</string>
<string name="empty_available_app_list">No apps in this category.\n\nTry selecting a different category or updating your repositories to get a fresh list of apps.</string> <string name="empty_available_app_list">No apps in this category.\n\nTry selecting a different category or updating your repositories to get a fresh list of apps.</string>
<string name="empty_can_update_app_list">All apps up to date.\n\nCongratulations! All of your apps are up to date (or your repositories are out of date).</string> <string name="empty_can_update_app_list">All apps up to date.\n\nCongratulations! All of your apps are up to date (or your repositories are out of date).</string>
@ -423,4 +444,5 @@
<string name="notification_action_cancel">Cancel</string> <string name="notification_action_cancel">Cancel</string>
<string name="notification_action_install">Install</string> <string name="notification_action_install">Install</string>
<string name="category">Category</string>
</resources> </resources>

View File

@ -1,7 +1,7 @@
<?xml version='1.0' encoding='utf-8'?> <?xml version='1.0' encoding='utf-8'?>
<resources xmlns:android="http://schemas.android.com/apk/res/android"> <resources xmlns:android="http://schemas.android.com/apk/res/android">
<style name="AppBaseThemeDark" parent="Theme.AppCompat"> <style name="AppBaseThemeDark" parent="Theme.AppCompat.NoActionBar">
<!-- backward-compatibility theme options go here --> <!-- backward-compatibility theme options go here -->
<item name="colorPrimary">@color/fdroid_blue_dark</item> <item name="colorPrimary">@color/fdroid_blue_dark</item>
@ -10,9 +10,11 @@
<item name="android:textColorLink">@color/fdroid_green</item> <item name="android:textColorLink">@color/fdroid_green</item>
<item name="alertDialogTheme">@style/AlertDialogThemeDark</item> <item name="alertDialogTheme">@style/AlertDialogThemeDark</item>
<item name="android:textViewStyle">@style/TextViewStyle</item> <item name="android:textViewStyle">@style/TextViewStyle</item>
<item name="actionBarTheme">@style/AppThemeLight.Toolbar</item>
<item name="actionBarPopupTheme">@style/AppThemeLight.Toolbar</item>
</style> </style>
<style name="AppBaseThemeLight" parent="Theme.AppCompat.Light.DarkActionBar"> <style name="AppBaseThemeLight" parent="Theme.AppCompat.Light.NoActionBar">
<!-- backward-compatibility theme options go here --> <!-- backward-compatibility theme options go here -->
<item name="colorPrimary">@color/fdroid_blue</item> <item name="colorPrimary">@color/fdroid_blue</item>
@ -21,6 +23,8 @@
<item name="android:textColorLink">@color/fdroid_green</item> <item name="android:textColorLink">@color/fdroid_green</item>
<item name="alertDialogTheme">@style/AlertDialogThemeLight</item> <item name="alertDialogTheme">@style/AlertDialogThemeLight</item>
<item name="android:textViewStyle">@style/TextViewStyle</item> <item name="android:textViewStyle">@style/TextViewStyle</item>
<item name="actionBarTheme">@style/AppThemeLight.Toolbar</item>
<item name="actionBarPopupTheme">@style/AppThemeLight.Toolbar</item>
</style> </style>
<style name="AppThemeDark" parent="AppBaseThemeDark"> <style name="AppThemeDark" parent="AppBaseThemeDark">
@ -62,6 +66,10 @@
<item name="windowNoTitle">true</item> <item name="windowNoTitle">true</item>
</style> </style>
<style name="AppThemeLight.Toolbar" parent="ThemeOverlay.AppCompat.Dark.ActionBar">
<item name="android:background">@color/fdroid_blue</item>
</style>
<style name="TextViewStyle" parent="android:Widget.TextView"> <style name="TextViewStyle" parent="android:Widget.TextView">
<item name="android:textColor">?android:attr/textColorPrimary</item> <item name="android:textColor">?android:attr/textColorPrimary</item>
</style> </style>

View File

@ -114,6 +114,9 @@ public class AppProviderTest extends FDroidProviderTest {
App notInstalled = AppProvider.Helper.findSpecificApp(r, "not installed", 1, Cols.ALL); App notInstalled = AppProvider.Helper.findSpecificApp(r, "not installed", 1, Cols.ALL);
assertFalse(notInstalled.canAndWantToUpdate(context)); assertFalse(notInstalled.canAndWantToUpdate(context));
assertResultCount(contentResolver, 2, AppProvider.getCanUpdateUri(), PROJ);
assertResultCount(contentResolver, 7, AppProvider.getInstalledUri(), PROJ);
App installedOnlyOneVersionAvailable = AppProvider.Helper.findSpecificApp(r, "installed, only one version available", 1, Cols.ALL); App installedOnlyOneVersionAvailable = AppProvider.Helper.findSpecificApp(r, "installed, only one version available", 1, Cols.ALL);
App installedAlreadyLatestNoIgnore = AppProvider.Helper.findSpecificApp(r, "installed, already latest, no ignore", 1, Cols.ALL); App installedAlreadyLatestNoIgnore = AppProvider.Helper.findSpecificApp(r, "installed, already latest, no ignore", 1, Cols.ALL);
App installedAlreadyLatestIgnoreAll = AppProvider.Helper.findSpecificApp(r, "installed, already latest, ignore all", 1, Cols.ALL); App installedAlreadyLatestIgnoreAll = AppProvider.Helper.findSpecificApp(r, "installed, already latest, ignore all", 1, Cols.ALL);
@ -206,12 +209,14 @@ public class AppProviderTest extends FDroidProviderTest {
insertApps(100); insertApps(100);
assertResultCount(contentResolver, 100, AppProvider.getContentUri(), PROJ); assertResultCount(contentResolver, 100, AppProvider.getContentUri(), PROJ);
assertResultCount(contentResolver, 0, AppProvider.getCanUpdateUri(), PROJ);
assertResultCount(contentResolver, 0, AppProvider.getInstalledUri(), PROJ); assertResultCount(contentResolver, 0, AppProvider.getInstalledUri(), PROJ);
for (int i = 10; i < 20; i++) { for (int i = 10; i < 20; i++) {
InstalledAppTestUtils.install(context, "com.example.test." + i, i, "v1"); InstalledAppTestUtils.install(context, "com.example.test." + i, i, "v1");
} }
assertResultCount(contentResolver, 0, AppProvider.getCanUpdateUri(), PROJ);
assertResultCount(contentResolver, 10, AppProvider.getInstalledUri(), PROJ); assertResultCount(contentResolver, 10, AppProvider.getInstalledUri(), PROJ);
} }