Merge branch 'new-ui--main-screens--v1' into 'master'
Initial UI for new main screens See merge request !433
This commit is contained in:
commit
d67f23b60c
@ -1,4 +1,4 @@
|
||||
image: fdroid/ci:client-20161223
|
||||
image: registry.gitlab.com/fdroid/ci-images:client
|
||||
|
||||
cache:
|
||||
paths:
|
||||
|
@ -17,14 +17,21 @@ repositories {
|
||||
jcenter()
|
||||
}
|
||||
|
||||
ext {
|
||||
supportLibVersion = '25.0.1'
|
||||
}
|
||||
|
||||
dependencies {
|
||||
compile 'com.android.support:support-v4:24.2.1'
|
||||
compile 'com.android.support:appcompat-v7:24.2.1'
|
||||
compile 'com.android.support:gridlayout-v7:24.2.1'
|
||||
compile 'com.android.support:support-annotations:24.2.1'
|
||||
compile 'com.android.support:design:24.2.1'
|
||||
compile 'com.android.support:cardview-v7:24.2.1'
|
||||
compile "com.android.support:recyclerview-v7:24.2.1"
|
||||
compile "com.android.support:support-v4:${supportLibVersion}"
|
||||
compile "com.android.support:appcompat-v7:${supportLibVersion}"
|
||||
compile "com.android.support:gridlayout-v7:${supportLibVersion}"
|
||||
compile "com.android.support:support-annotations:${supportLibVersion}"
|
||||
compile "com.android.support:recyclerview-v7:${supportLibVersion}"
|
||||
compile "com.android.support:cardview-v7:${supportLibVersion}"
|
||||
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.google.zxing:core:3.2.1'
|
||||
@ -50,7 +57,7 @@ dependencies {
|
||||
|
||||
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:rules:0.5'
|
||||
}
|
||||
@ -94,20 +101,24 @@ if (!hasProperty('sourceDeps')) {
|
||||
'ch.acra:acra:d2762968c448757a7d6acc9f141881d9632f664988e9723ece33b5f7c79f3bc9',
|
||||
'commons-io:commons-io:a10418348d234968600ccb1d988efcbbd08716e1d96936ccc1880e7d22513474',
|
||||
'commons-net:commons-net:c25b0da668b3c5649f002d504def22d1b4cb30d206f05428d2fe168fa1a901c2',
|
||||
'com.android.support:animated-vector-drawable:5aa30f578e1daefb26bef0ce06414266fbb4cdf5d4259f42a92c7bd83dcd81b4',
|
||||
'com.android.support:appcompat-v7:ead7ac8011fb40676df8adc2856cae934edab55fc4444654c0ac6ea443736088',
|
||||
'com.android.support:support-annotations:1e4d471c5378b283d95abfb128e7ed3c6b3cb19bb6f0c317a9b75e48e99365ff',
|
||||
'com.android.support:support-compat:8e4fe0078b68073e8f5bcb52aa5b6407fd456d47c51aa0f8e8d1e23c69da06c1',
|
||||
'com.android.support:support-core-ui:ecc9184b7f438980e1c4a08b089d62dbc53ff90091f442d91fec27322a02c73c',
|
||||
'com.android.support:support-core-utils:0fbc508e41dd6e8c634f310ee88452aaf8f48b6a843a369b115130b80d2fc05f',
|
||||
'com.android.support:support-fragment:d8030f0bf0f64214a29dc4e14d5ccd225e59f66ed15eb37f3a5022e773dd1fda',
|
||||
'com.android.support:support-media-compat:fa29a23eadd685631584b2c0c624a36e3bb79a33e257b00304501ad682fa2be3',
|
||||
'com.android.support:support-v4:cac2956f5c4bb363cc0ba824ac16ea2a687d1c305d434416a34772a5f9375ed7',
|
||||
'com.android.support:support-vector-drawable:6ee37a7f7b93c1df1294e6f6f97df3724ac989fcda0549faf677001085330548',
|
||||
'com.android.support:design:89842bb1243507fe3079066ea4ea58795effe69cdf9a819e05274d21760adfc2',
|
||||
'com.android.support:cardview-v7:2303b351686d1db060b5fcf1a9c709c79b4a54a85bfda0fb3c4849e244606ee1',
|
||||
'com.android.support:gridlayout-v7:1a31c248d69faa815cc155883ddcb0ccc7ba8e14e69ec58dd18d8017e23d76f5',
|
||||
'com.android.support:recyclerview-v7:9077766a1a0f4e89528fbf9dcdf6d5880a8686f0266fa852d58d803beeef18fa',
|
||||
'com.android.support.constraint:constraint-layout-solver:d03a406eb505dfa673b0087bf17e16d5a4d6bf8afdf452ee175e346207948cdf',
|
||||
'com.android.support.constraint:constraint-layout:df1add69d11063eebba521818d63537b22207376b65f30cc35feea172b84e300',
|
||||
'com.android.support:animated-vector-drawable:70443a2857f9968c4e2c27c107657ce2291d774f8a50f03444e12ab637451175',
|
||||
'com.android.support:appcompat-v7:7fead560a22ea4b15848ce3000f312ef611fac0953bf90ca8710a72a1f6e36ea',
|
||||
'com.android.support:cardview-v7:50d88fae8cd1076cb90504d36ca5ee9df4018555c8f041bd28f43274c0fc9e1f',
|
||||
'com.android.support:design:07a72eb68c888b38d7b78e450e1af8a84e571406e0cf911889e0645d5a41f1e4',
|
||||
'com.android.support:gridlayout-v7:cc11d2a3ee484e078c358a51d23a37e4bfbc542de410cacf275eafc5624bb888',
|
||||
'com.android.support:palette-v7:89700afeedd988b471f0ce528ba916f368f549b47889b86b84d68eee42ea487c',
|
||||
'com.android.support:recyclerview-v7:803baba7be537ace8c5cb8a775e37547c22a04c4b028833796c45c26ec1deca2',
|
||||
'com.android.support:support-annotations:bd94ab42c841db16fb480f4c65d33d297e544655ecc498b37c5cf33a0c5f1968',
|
||||
'com.android.support:support-compat:d04f15aa5f2ae9e8cb7d025bf02dfd4fd6f6800628ceb107e0589634c9e4e537',
|
||||
'com.android.support:support-core-ui:29205ac978a1839d92be3d32db2385dac10f8688bba649e51650023c76de2f00',
|
||||
'com.android.support:support-core-utils:632c3750bd991da8b591f24a8916e74ca6063ae7f525f005c96981725c9bf491',
|
||||
'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.madgag.spongycastle:core:9b6b7ac856b91bcda2ede694eccd26cefb0bf0b09b89f13cda05b5da5ff68c6b',
|
||||
'com.madgag.spongycastle:pkix:6aba9b2210907a3d46dd3dcac782bb3424185290468d102d5207ebdc9796a905',
|
||||
|
@ -137,12 +137,6 @@
|
||||
android:configChanges="layoutDirection|locale|keyboardHidden|orientation|screenSize" >
|
||||
|
||||
<!-- App URLs -->
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.MAIN" />
|
||||
|
||||
<category android:name="android.intent.category.LAUNCHER" />
|
||||
</intent-filter>
|
||||
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
|
||||
@ -407,7 +401,6 @@
|
||||
android:label="@string/app_details"
|
||||
android:exported="true"
|
||||
android:parentActivityName=".FDroid"
|
||||
android:theme="@style/AppThemeLight.NoActionBar"
|
||||
android:configChanges="layoutDirection|locale" >
|
||||
<meta-data
|
||||
android:name="android.support.PARENT_ACTIVITY"
|
||||
@ -506,6 +499,17 @@
|
||||
<service
|
||||
android:name=".data.InstalledAppProviderService"
|
||||
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>
|
||||
|
||||
</manifest>
|
||||
|
@ -16,7 +16,6 @@ import android.support.design.widget.CoordinatorLayout;
|
||||
import android.support.v4.content.LocalBroadcastManager;
|
||||
import android.support.v7.app.AlertDialog;
|
||||
import android.support.v7.app.AppCompatActivity;
|
||||
import android.support.v7.app.AppCompatDelegate;
|
||||
import android.support.v7.widget.LinearLayoutManager;
|
||||
import android.support.v7.widget.RecyclerView;
|
||||
import android.support.v7.widget.Toolbar;
|
||||
@ -47,9 +46,6 @@ import org.fdroid.fdroid.views.AppDetailsRecyclerViewAdapter;
|
||||
import org.fdroid.fdroid.views.ShareChooserDialog;
|
||||
|
||||
public class AppDetails2 extends AppCompatActivity implements ShareChooserDialog.ShareChooserDialogListener, AppDetailsRecyclerViewAdapter.AppDetailsRecyclerViewAdapterCallbacks {
|
||||
static {
|
||||
AppCompatDelegate.setCompatVectorFromResourcesEnabled(true);
|
||||
}
|
||||
|
||||
private static final String TAG = "AppDetails2";
|
||||
|
||||
|
@ -242,7 +242,7 @@ public class FDroid extends AppCompatActivity implements SearchView.OnQueryTextL
|
||||
|
||||
if (!TextUtils.isEmpty(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);
|
||||
startActivity(intentToInvoke);
|
||||
finish();
|
||||
|
@ -410,7 +410,7 @@ public class UpdateService extends IntentService {
|
||||
|
||||
// now that downloading the index is done, start downloading updates
|
||||
if (changes && fdroidPrefs.isAutoDownloadEnabled()) {
|
||||
autoDownloadUpdates();
|
||||
autoDownloadUpdates(this);
|
||||
}
|
||||
}
|
||||
|
||||
@ -469,8 +469,8 @@ public class UpdateService extends IntentService {
|
||||
}
|
||||
}
|
||||
|
||||
private void autoDownloadUpdates() {
|
||||
Cursor cursor = getContentResolver().query(
|
||||
public static void autoDownloadUpdates(Context context) {
|
||||
Cursor cursor = context.getContentResolver().query(
|
||||
AppProvider.getCanUpdateUri(),
|
||||
Schema.AppMetadataTable.Cols.ALL,
|
||||
null, null, null);
|
||||
@ -478,8 +478,8 @@ public class UpdateService extends IntentService {
|
||||
cursor.moveToFirst();
|
||||
for (int i = 0; i < cursor.getCount(); i++) {
|
||||
App app = new App(cursor);
|
||||
Apk apk = ApkProvider.Helper.findApkFromAnyRepo(this, app.packageName, app.suggestedVersionCode);
|
||||
InstallManagerService.queue(this, app, apk);
|
||||
Apk apk = ApkProvider.Helper.findApkFromAnyRepo(context, app.packageName, app.suggestedVersionCode);
|
||||
InstallManagerService.queue(context, app, apk);
|
||||
cursor.moveToNext();
|
||||
}
|
||||
cursor.close();
|
||||
|
@ -29,7 +29,11 @@ import android.support.annotation.Nullable;
|
||||
import android.support.annotation.RequiresApi;
|
||||
import android.text.Editable;
|
||||
import android.text.Html;
|
||||
import android.text.SpannableStringBuilder;
|
||||
import android.text.Spanned;
|
||||
import android.text.TextUtils;
|
||||
import android.text.style.CharacterStyle;
|
||||
import android.text.style.TypefaceSpan;
|
||||
import android.util.DisplayMetrics;
|
||||
import android.util.Log;
|
||||
import android.util.TypedValue;
|
||||
@ -487,6 +491,23 @@ public final class Utils {
|
||||
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
|
||||
// lists to Html.fromHtml().
|
||||
public static class HtmlTagHandler implements Html.TagHandler {
|
||||
|
@ -199,16 +199,26 @@ public class AppProvider extends FDroidProvider {
|
||||
public AppQuerySelection add(AppQuerySelection query) {
|
||||
QuerySelection both = super.add(query);
|
||||
AppQuerySelection bothWithJoin = new AppQuerySelection(both.getSelection(), both.getArgs());
|
||||
if (this.naturalJoinToInstalled() || query.naturalJoinToInstalled()) {
|
||||
bothWithJoin.requireNaturalInstalledTable();
|
||||
}
|
||||
|
||||
if (this.leftJoinToPrefs() || query.leftJoinToPrefs()) {
|
||||
bothWithJoin.requireLeftJoinPrefs();
|
||||
}
|
||||
ensureJoinsCopied(query, 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 {
|
||||
@ -373,7 +383,6 @@ public class AppProvider extends FDroidProvider {
|
||||
protected static final String PATH_APPS = "apps";
|
||||
protected static final String PATH_SPECIFIC_APP = "app";
|
||||
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_REPO = "repo";
|
||||
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_AND_CATEGORIES = SEARCH_TEXT + 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 = NEWLY_ADDED + 1;
|
||||
private static final int CATEGORY = RECENTLY_UPDATED + 1;
|
||||
private static final int CALC_SUGGESTED_APKS = CATEGORY + 1;
|
||||
private static final int REPO = CALC_SUGGESTED_APKS + 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(), PATH_CALC_SUGGESTED_APKS, CALC_SUGGESTED_APKS);
|
||||
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_SEARCH + "/*/*", SEARCH_TEXT_AND_CATEGORIES);
|
||||
MATCHER.addURI(getAuthority(), PATH_SEARCH + "/*", SEARCH_TEXT);
|
||||
@ -425,10 +432,6 @@ public class AppProvider extends FDroidProvider {
|
||||
return Uri.withAppendedPath(getContentUri(), PATH_RECENTLY_UPDATED);
|
||||
}
|
||||
|
||||
public static Uri getNewlyAddedUri() {
|
||||
return Uri.withAppendedPath(getContentUri(), PATH_NEWLY_ADDED);
|
||||
}
|
||||
|
||||
private static Uri calcSuggestedApksUri() {
|
||||
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 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();
|
||||
}
|
||||
@ -583,7 +587,7 @@ public class AppProvider extends FDroidProvider {
|
||||
}
|
||||
|
||||
private AppQuerySelection queryInstalled() {
|
||||
return new AppQuerySelection().requireNaturalInstalledTable();
|
||||
return new AppQuerySelection().requireNaturalInstalledTable().not(queryCanUpdate());
|
||||
}
|
||||
|
||||
private AppQuerySelection querySearch(String query) {
|
||||
@ -666,12 +670,6 @@ public class AppProvider extends FDroidProvider {
|
||||
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
|
||||
* 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() {
|
||||
final String app = getTableName();
|
||||
final String lastUpdated = app + "." + Cols.LAST_UPDATED;
|
||||
final String selection = app + "." + Cols.ADDED + " != " + lastUpdated + " AND " + lastUpdated + " > ?";
|
||||
final String selection = getTableName() + "." + Cols.LAST_UPDATED + " > ? ";
|
||||
final String[] args = {Utils.formatDate(Preferences.get().calcMaxHistory(), "")};
|
||||
return new AppQuerySelection(selection, args);
|
||||
}
|
||||
@ -810,12 +806,6 @@ public class AppProvider extends FDroidProvider {
|
||||
includeSwap = false;
|
||||
break;
|
||||
|
||||
case NEWLY_ADDED:
|
||||
sortOrder = getTableName() + "." + Cols.ADDED + " DESC";
|
||||
selection = selection.add(queryNewlyAdded());
|
||||
includeSwap = false;
|
||||
break;
|
||||
|
||||
case HIGHEST_PRIORITY:
|
||||
selection = selection.add(queryPackageName(uri.getLastPathSegment()));
|
||||
includeSwap = false;
|
||||
|
@ -78,4 +78,8 @@ public class QuerySelection {
|
||||
return new QuerySelection(s, a);
|
||||
}
|
||||
|
||||
public QuerySelection not(QuerySelection querySelection) {
|
||||
String where = " NOT (" + querySelection.getSelection() + ") ";
|
||||
return add(where, querySelection.getArgs());
|
||||
}
|
||||
}
|
||||
|
@ -29,17 +29,15 @@ import android.net.Uri;
|
||||
import android.net.wifi.WifiInfo;
|
||||
import android.net.wifi.WifiManager;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
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.NavUtils;
|
||||
import android.support.v4.content.CursorLoader;
|
||||
import android.support.v4.content.Loader;
|
||||
import android.support.v7.app.ActionBarActivity;
|
||||
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.TextUtils;
|
||||
import android.text.TextWatcher;
|
||||
@ -47,9 +45,9 @@ import android.util.Log;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.widget.AdapterView;
|
||||
import android.widget.Button;
|
||||
import android.widget.EditText;
|
||||
import android.widget.LinearLayout;
|
||||
import android.widget.ListView;
|
||||
import android.widget.TextView;
|
||||
import android.widget.Toast;
|
||||
@ -74,7 +72,7 @@ import java.net.URISyntaxException;
|
||||
import java.net.URL;
|
||||
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 DEFAULT_NEW_REPO_TEXT = "https://";
|
||||
@ -85,7 +83,7 @@ public class ManageReposActivity extends ActionBarActivity {
|
||||
IS_SWAP
|
||||
}
|
||||
|
||||
private RepoListFragment listFragment;
|
||||
private Toolbar toolbar;
|
||||
|
||||
/**
|
||||
* 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);
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
FragmentManager fm = getSupportFragmentManager();
|
||||
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();
|
||||
}
|
||||
setContentView(R.layout.repo_list_activity);
|
||||
|
||||
toolbar = (Toolbar) findViewById(R.id.toolbar);
|
||||
setSupportActionBar(toolbar);
|
||||
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
|
||||
@ -133,6 +123,9 @@ public class ManageReposActivity extends ActionBarActivity {
|
||||
|
||||
/* let's see if someone is trying to send us a new repo */
|
||||
addRepoFromIntent(getIntent());
|
||||
|
||||
// Starts a new or restarts an existing Loader in this manager
|
||||
getSupportLoaderManager().restartLoader(0, null, this);
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -149,7 +142,7 @@ public class ManageReposActivity extends ActionBarActivity {
|
||||
|
||||
@Override
|
||||
public boolean onCreateOptionsMenu(Menu menu) {
|
||||
getMenuInflater().inflate(R.menu.manage_repos, menu);
|
||||
toolbar.inflateMenu(R.menu.manage_repos);
|
||||
return super.onCreateOptionsMenu(menu);
|
||||
}
|
||||
|
||||
@ -649,7 +642,7 @@ public class ManageReposActivity extends ActionBarActivity {
|
||||
values.put(RepoTable.Cols.IN_USE, 1);
|
||||
values.put(RepoTable.Cols.FINGERPRINT, fingerprint);
|
||||
RepoProvider.Helper.update(context, repo, values);
|
||||
listFragment.notifyDataSetChanged();
|
||||
notifyDataSetChanged();
|
||||
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;
|
||||
|
||||
@Override
|
||||
public Loader<Cursor> onCreateLoader(int i, Bundle bundle) {
|
||||
Uri uri = RepoProvider.allExceptSwapUri();
|
||||
Utils.debugLog(TAG, "Creating repo loader '" + uri + "'.");
|
||||
final String[] projection = {
|
||||
RepoTable.Cols._ID,
|
||||
RepoTable.Cols.NAME,
|
||||
@ -719,7 +708,7 @@ public class ManageReposActivity extends ActionBarActivity {
|
||||
RepoTable.Cols.FINGERPRINT,
|
||||
RepoTable.Cols.IN_USE,
|
||||
};
|
||||
return new CursorLoader(getActivity(), uri, projection, null, null, null);
|
||||
return new CursorLoader(this, uri, projection, null, null, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
@ -752,51 +741,22 @@ public class ManageReposActivity extends ActionBarActivity {
|
||||
if (repo.inuse != isEnabled) {
|
||||
ContentValues values = new ContentValues(1);
|
||||
values.put(RepoTable.Cols.IN_USE, isEnabled ? 1 : 0);
|
||||
RepoProvider.Helper.update(getActivity(), repo, values);
|
||||
RepoProvider.Helper.update(this, repo, values);
|
||||
|
||||
if (isEnabled) {
|
||||
UpdateService.updateNow(getActivity());
|
||||
UpdateService.updateNow(this);
|
||||
} else {
|
||||
RepoProvider.Helper.purgeApps(getActivity(), repo);
|
||||
RepoProvider.Helper.purgeApps(this, repo);
|
||||
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 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());
|
||||
startActivityForResult(intent, SHOW_REPO_DETAILS);
|
||||
}
|
||||
@ -808,7 +768,6 @@ public class ManageReposActivity extends ActionBarActivity {
|
||||
* repo, and wanting the switch to be changed to on).
|
||||
*/
|
||||
private void notifyDataSetChanged() {
|
||||
getLoaderManager().restartLoader(0, null, this);
|
||||
}
|
||||
getSupportLoaderManager().restartLoader(0, null, this);
|
||||
}
|
||||
}
|
||||
|
@ -17,6 +17,7 @@ import android.support.v4.app.NavUtils;
|
||||
import android.support.v4.content.LocalBroadcastManager;
|
||||
import android.support.v7.app.ActionBarActivity;
|
||||
import android.support.v7.app.AlertDialog;
|
||||
import android.support.v7.widget.Toolbar;
|
||||
import android.text.TextUtils;
|
||||
import android.text.format.DateUtils;
|
||||
import android.view.Menu;
|
||||
@ -93,8 +94,12 @@ public class RepoDetailsActivity extends ActionBarActivity {
|
||||
((FDroidApp) getApplication()).applyTheme(this);
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
setContentView(R.layout.repodetails);
|
||||
|
||||
Toolbar toolbar = (Toolbar) findViewById(R.id.toolbar);
|
||||
setSupportActionBar(toolbar);
|
||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
|
||||
repoView = findViewById(R.id.repoView);
|
||||
|
||||
repoId = getIntent().getLongExtra(ARG_REPO_ID, 0);
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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));
|
||||
}
|
||||
};
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
133
app/src/main/java/org/fdroid/fdroid/views/apps/CategorySpan.java
Normal file
133
app/src/main/java/org/fdroid/fdroid/views/apps/CategorySpan.java
Normal 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();
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -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
|
||||
}
|
||||
|
||||
}
|
@ -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();
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -178,7 +178,9 @@ public class AvailableAppsFragment extends AppListFragment implements
|
||||
return AppProvider.getRecentlyUpdatedUri();
|
||||
}
|
||||
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);
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
};
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
10
app/src/main/res/drawable/app_list_item_divider.xml
Normal file
10
app/src/main/res/drawable/app_list_item_divider.xml
Normal 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>
|
10
app/src/main/res/drawable/category_chip_background.xml
Normal file
10
app/src/main/res/drawable/category_chip_background.xml
Normal 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>
|
@ -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>
|
7
app/src/main/res/drawable/download_button.xml
Normal file
7
app/src/main/res/drawable/download_button.xml
Normal 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>
|
5
app/src/main/res/drawable/ic_back_black_24dp.xml
Normal file
5
app/src/main/res/drawable/ic_back_black_24dp.xml
Normal 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>
|
9
app/src/main/res/drawable/ic_category.xml
Normal file
9
app/src/main/res/drawable/ic_category.xml
Normal 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>
|
9
app/src/main/res/drawable/ic_close_black_24dp.xml
Normal file
9
app/src/main/res/drawable/ic_close_black_24dp.xml
Normal 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>
|
13
app/src/main/res/drawable/ic_download_button.xml
Normal file
13
app/src/main/res/drawable/ic_download_button.xml
Normal 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>
|
6
app/src/main/res/drawable/ic_my_apps.xml
Normal file
6
app/src/main/res/drawable/ic_my_apps.xml
Normal 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>
|
12
app/src/main/res/drawable/ic_nearby.xml
Normal file
12
app/src/main/res/drawable/ic_nearby.xml
Normal 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>
|
6
app/src/main/res/drawable/ic_overview.xml
Normal file
6
app/src/main/res/drawable/ic_overview.xml
Normal 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>
|
6
app/src/main/res/drawable/ic_settings.xml
Normal file
6
app/src/main/res/drawable/ic_settings.xml
Normal 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>
|
69
app/src/main/res/layout/activity_app_list.xml
Normal file
69
app/src/main/res/layout/activity_app_list.xml
Normal 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>
|
27
app/src/main/res/layout/activity_main.xml
Normal file
27
app/src/main/res/layout/activity_main.xml
Normal 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>
|
86
app/src/main/res/layout/app_card_featured.xml
Normal file
86
app/src/main/res/layout/app_card_featured.xml
Normal 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>
|
52
app/src/main/res/layout/app_card_horizontal.xml
Normal file
52
app/src/main/res/layout/app_card_horizontal.xml
Normal 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>
|
58
app/src/main/res/layout/app_card_large.xml
Normal file
58
app/src/main/res/layout/app_card_large.xml
Normal 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>
|
55
app/src/main/res/layout/app_card_list_item.xml
Normal file
55
app/src/main/res/layout/app_card_list_item.xml
Normal 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>
|
41
app/src/main/res/layout/app_card_normal.xml
Normal file
41
app/src/main/res/layout/app_card_normal.xml
Normal 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>
|
67
app/src/main/res/layout/app_list_item.xml
Normal file
67
app/src/main/res/layout/app_list_item.xml
Normal 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>
|
80
app/src/main/res/layout/category_item.xml
Normal file
80
app/src/main/res/layout/category_item.xml
Normal 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>
|
19
app/src/main/res/layout/main_tab_categories.xml
Normal file
19
app/src/main/res/layout/main_tab_categories.xml
Normal 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>
|
7
app/src/main/res/layout/main_tab_settings.xml
Normal file
7
app/src/main/res/layout/main_tab_settings.xml
Normal 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>
|
62
app/src/main/res/layout/main_tab_swap.xml
Normal file
62
app/src/main/res/layout/main_tab_swap.xml
Normal 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>
|
26
app/src/main/res/layout/main_tab_whats_new.xml
Normal file
26
app/src/main/res/layout/main_tab_whats_new.xml
Normal 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>
|
19
app/src/main/res/layout/main_tabs.xml
Normal file
19
app/src/main/res/layout/main_tabs.xml
Normal 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>
|
32
app/src/main/res/layout/my_apps_updates_header.xml
Normal file
32
app/src/main/res/layout/my_apps_updates_header.xml
Normal 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>
|
24
app/src/main/res/layout/repo_list_activity.xml
Normal file
24
app/src/main/res/layout/repo_list_activity.xml
Normal 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>
|
@ -1,5 +1,19 @@
|
||||
<?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_height="match_parent">
|
||||
|
||||
@ -99,4 +113,6 @@
|
||||
|
||||
</LinearLayout>
|
||||
|
||||
</ScrollView>
|
||||
</ScrollView>
|
||||
|
||||
</LinearLayout>
|
||||
|
29
app/src/main/res/menu/main_activity_screens.xml
Normal file
29
app/src/main/res/menu/main_activity_screens.xml
Normal 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>
|
@ -18,4 +18,13 @@
|
||||
<!-- The selected item stands out from the background by this elevation -->
|
||||
<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>
|
||||
|
@ -2,4 +2,13 @@
|
||||
<resources>
|
||||
<item type="id" name="category_spinner" />
|
||||
<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>
|
@ -63,9 +63,17 @@
|
||||
<string name="app_not_installed">Not Installed</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_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="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="yes">Yes</string>
|
||||
@ -82,6 +90,7 @@
|
||||
<string name="enable">Enable</string>
|
||||
<string name="add_key">Add Key</string>
|
||||
<string name="overwrite">Overwrite</string>
|
||||
<string name="clear_search">Clear search</string>
|
||||
|
||||
<string name="tab_available_apps">Available</string>
|
||||
<string name="tab_installed_apps">Installed</string>
|
||||
@ -130,6 +139,11 @@
|
||||
<string name="menu_litecoin">Litecoin</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_notinstalled">Not installed</string>
|
||||
@ -264,6 +278,13 @@
|
||||
<string name="category_Time">Time</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_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>
|
||||
@ -423,4 +444,5 @@
|
||||
<string name="notification_action_cancel">Cancel</string>
|
||||
<string name="notification_action_install">Install</string>
|
||||
|
||||
<string name="category">Category</string>
|
||||
</resources>
|
||||
|
@ -1,7 +1,7 @@
|
||||
<?xml version='1.0' encoding='utf-8'?>
|
||||
<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 -->
|
||||
|
||||
<item name="colorPrimary">@color/fdroid_blue_dark</item>
|
||||
@ -10,9 +10,11 @@
|
||||
<item name="android:textColorLink">@color/fdroid_green</item>
|
||||
<item name="alertDialogTheme">@style/AlertDialogThemeDark</item>
|
||||
<item name="android:textViewStyle">@style/TextViewStyle</item>
|
||||
<item name="actionBarTheme">@style/AppThemeLight.Toolbar</item>
|
||||
<item name="actionBarPopupTheme">@style/AppThemeLight.Toolbar</item>
|
||||
</style>
|
||||
|
||||
<style name="AppBaseThemeLight" parent="Theme.AppCompat.Light.DarkActionBar">
|
||||
<style name="AppBaseThemeLight" parent="Theme.AppCompat.Light.NoActionBar">
|
||||
<!-- backward-compatibility theme options go here -->
|
||||
|
||||
<item name="colorPrimary">@color/fdroid_blue</item>
|
||||
@ -21,6 +23,8 @@
|
||||
<item name="android:textColorLink">@color/fdroid_green</item>
|
||||
<item name="alertDialogTheme">@style/AlertDialogThemeLight</item>
|
||||
<item name="android:textViewStyle">@style/TextViewStyle</item>
|
||||
<item name="actionBarTheme">@style/AppThemeLight.Toolbar</item>
|
||||
<item name="actionBarPopupTheme">@style/AppThemeLight.Toolbar</item>
|
||||
</style>
|
||||
|
||||
<style name="AppThemeDark" parent="AppBaseThemeDark">
|
||||
@ -62,6 +66,10 @@
|
||||
<item name="windowNoTitle">true</item>
|
||||
</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">
|
||||
<item name="android:textColor">?android:attr/textColorPrimary</item>
|
||||
</style>
|
||||
|
@ -114,6 +114,9 @@ public class AppProviderTest extends FDroidProviderTest {
|
||||
App notInstalled = AppProvider.Helper.findSpecificApp(r, "not installed", 1, Cols.ALL);
|
||||
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 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);
|
||||
@ -206,12 +209,14 @@ public class AppProviderTest extends FDroidProviderTest {
|
||||
insertApps(100);
|
||||
|
||||
assertResultCount(contentResolver, 100, AppProvider.getContentUri(), PROJ);
|
||||
assertResultCount(contentResolver, 0, AppProvider.getCanUpdateUri(), PROJ);
|
||||
assertResultCount(contentResolver, 0, AppProvider.getInstalledUri(), PROJ);
|
||||
|
||||
for (int i = 10; i < 20; i++) {
|
||||
InstalledAppTestUtils.install(context, "com.example.test." + i, i, "v1");
|
||||
}
|
||||
|
||||
assertResultCount(contentResolver, 0, AppProvider.getCanUpdateUri(), PROJ);
|
||||
assertResultCount(contentResolver, 10, AppProvider.getInstalledUri(), PROJ);
|
||||
}
|
||||
|
||||
|
Loading…
x
Reference in New Issue
Block a user