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

Updates tab + misc UI improvements.

Closes #840, #876, #838, and #892

See merge request !444
This commit is contained in:
Peter Serwylo 2017-03-22 00:20:44 +00:00
commit 343e91280a
49 changed files with 1576 additions and 404 deletions

View File

@ -15,10 +15,13 @@ def getVersionName = { ->
repositories {
jcenter()
maven {
url "https://jitpack.io"
}
}
ext {
supportLibVersion = '25.0.1'
supportLibVersion = '25.2.0'
}
dependencies {
@ -41,12 +44,19 @@ dependencies {
compile 'commons-io:commons-io:2.5'
compile 'commons-net:commons-net:3.5'
compile 'org.openhab.jmdns:jmdns:3.4.2'
compile('ch.acra:acra:4.9.1') {
exclude module: 'support-v4'
exclude module: 'support-annotations'
}
compile 'ch.acra:acra:4.9.1'
compile 'io.reactivex:rxjava:1.1.0'
compile 'io.reactivex:rxandroid:0.23.0'
compile 'com.hannesdorfmann:adapterdelegates3:3.0.1'
// Migrate this to upstream https://github.com/Ashok-Varma/BottomNavigation if PR #110 gets
// accepted to drop the minSdk to 10.
compile('com.github.pserwylo:BottomNavigation:1.5.0') {
// These pull our explicit dependency on 25.2.0 up to 25.3.0 which is broken
// (https://code.google.com/p/android/issues/detail?id=251302)
exclude module: 'appcompat-v7'
exclude module: 'design'
}
testCompile 'junit:junit:4.12'
@ -103,23 +113,25 @@ if (!hasProperty('sourceDeps')) {
'commons-net:commons-net:c25b0da668b3c5649f002d504def22d1b4cb30d206f05428d2fe168fa1a901c2',
'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.android.support:animated-vector-drawable:d2d59a11809abe3e64535346f05c22437b458de115f06ea35021fd0714960213',
'com.android.support:appcompat-v7:120f3ce6cac682d69e53d80ccfa9cee076f0f11ccbe56d4ccd72099a745e81f9',
'com.android.support:cardview-v7:c8610b0c334e4438d7e1ac58fcf2ac891fb26bac662c8351cd6b345c8d7b7076',
'com.android.support:design:bf92337c5d0931df50a0dcec81682186dc1fbcf14c2fa1c6d51976963379b64d',
'com.android.support:gridlayout-v7:257ac1280f2b3cc3c0afca1cd4d4d2e0b923b92a76b61a9c09fc57e892da7360',
'com.android.support:palette-v7:e0050715e0d06fabcc8721b0c2893545fb00be9d761a6ef59ae69101d2368551',
'com.android.support:recyclerview-v7:d6ba2c3a6196cc464eb4d69756229523a46eef7804991e5a8cf2d6306dbff10c',
'com.android.support:support-annotations:47a2a30eab487a490a8a8f16678007c3d2b6dcae1e09b0485a12bbf921200ec3',
'com.android.support:support-compat:5a7b6e18903458e3a561df24033476518f998cd7ae1ed747c2874e0685b999c7',
'com.android.support:support-core-ui:cf3c75fd9a1b1dcbb6042d610515cd79cd0d65d3efd272d2250727187e8ca2ed',
'com.android.support:support-core-utils:e0561cc9d00ae125d9e1ad8985d4639e68ce8399ae973e91674e97faaf658243',
'com.android.support:support-fragment:f12633dd4d418a4edeb5ecf3bf4393edd0770b1eaa5d1ee3078c5e7c174868fd',
'com.android.support:support-media-compat:e9f820d08e6a5735cfdb2a7d81d3c86b4a31897ac1edaeb55c7de06bcb370343',
'com.android.support:support-v4:cd030f875dc7ee73b58e17598f368a2e12824fb3ceb4ed515ed815a47160228c',
'com.android.support:support-vector-drawable:d79752fd68db5a8f5c18125517dafb9e4d7b593c755d188986010e15edd62454',
'com.android.support:transition:5a4adefb1b410b23ad62b4477bc612edc47d3dfc8efed488deb8223b70b510d7',
'com.github.pserwylo:BottomNavigation:83d7941a7a8d21ba1a8a708cd683b1bb07c6cf898044dc92eadf18a7a7d54f90',
'com.google.zxing:core:b4d82452e7a6bf6ec2698904b332431717ed8f9a850224f295aec89de80f2259',
'com.hannesdorfmann:adapterdelegates3:1b20d099d6e7afe57aceca13b713b386959d94a247c3c06a7aeb65b866ece02f',
'com.madgag.spongycastle:core:9b6b7ac856b91bcda2ede694eccd26cefb0bf0b09b89f13cda05b5da5ff68c6b',
'com.madgag.spongycastle:pkix:6aba9b2210907a3d46dd3dcac782bb3424185290468d102d5207ebdc9796a905',
'com.madgag.spongycastle:prov:029f26cd6b67c06ffa05702d426d472c141789001bcb15b7262ed86c868e5643',

View File

@ -297,8 +297,14 @@
<service
android:name=".data.InstalledAppProviderService"
android:exported="false" />
<service
android:name=".AppUpdateStatusService"
android:exported="false" />
<activity android:name=".views.main.MainActivity">
<activity
android:name=".views.main.MainActivity"
android:launchMode="singleTop"
android:windowSoftInputMode="adjustResize">
<intent-filter>
<action android:name="android.intent.action.MAIN" />

View File

@ -35,12 +35,47 @@ import java.util.Map;
*/
public final class AppUpdateStatusManager {
static final String BROADCAST_APPSTATUS_LIST_CHANGED = "org.fdroid.fdroid.installer.appstatus.listchange";
static final String BROADCAST_APPSTATUS_ADDED = "org.fdroid.fdroid.installer.appstatus.appchange.add";
static final String BROADCAST_APPSTATUS_CHANGED = "org.fdroid.fdroid.installer.appstatus.appchange.change";
static final String BROADCAST_APPSTATUS_REMOVED = "org.fdroid.fdroid.installer.appstatus.appchange.remove";
static final String EXTRA_APK_URL = "urlstring";
static final String EXTRA_IS_STATUS_UPDATE = "isstatusupdate";
/**
* Broadcast when:
* * The user clears the list of installed apps from notification manager.
* * The user clears the list of apps available to update from the notification manager.
* * A repo update is completed and a bunch of new apps are ready to be updated.
* * F-Droid is opened, and it finds a bunch of .apk files downloaded and ready to install.
*/
public static final String BROADCAST_APPSTATUS_LIST_CHANGED = "org.fdroid.fdroid.installer.appstatus.listchange";
/**
* Broadcast when an app begins the download/install process (either manually or via an automatic download).
*/
public static final String BROADCAST_APPSTATUS_ADDED = "org.fdroid.fdroid.installer.appstatus.appchange.add";
/**
* When the {@link AppUpdateStatus#status} of an app changes or the download progress for an app advances.
*/
public static final String BROADCAST_APPSTATUS_CHANGED = "org.fdroid.fdroid.installer.appstatus.appchange.change";
/**
* Broadcast when:
* * The associated app has the {@link Status#Installed} status, and the user either visits
* that apps details page or clears the individual notification for the app.
* * The download for an app is cancelled.
*/
public static final String BROADCAST_APPSTATUS_REMOVED = "org.fdroid.fdroid.installer.appstatus.appchange.remove";
public static final String EXTRA_APK_URL = "urlstring";
public static final String EXTRA_REASON_FOR_CHANGE = "reason";
public static final String REASON_READY_TO_INSTALL = "readytoinstall";
public static final String REASON_UPDATES_AVAILABLE = "updatesavailable";
public static final String REASON_CLEAR_ALL_UPDATES = "clearallupdates";
public static final String REASON_CLEAR_ALL_INSTALLED = "clearallinstalled";
/**
* If this is present and true, then the broadcast has been sent in response to the {@link AppUpdateStatus#status}
* changing. In comparison, if it is just the download progress of an app then this should not be true.
*/
public static final String EXTRA_IS_STATUS_UPDATE = "isstatusupdate";
private static final String LOGTAG = "AppUpdateStatusManager";
@ -147,9 +182,11 @@ public final class AppUpdateStatusManager {
notifyAdd(entry);
}
private void notifyChange() {
private void notifyChange(String reason) {
if (!isBatchUpdating) {
localBroadcastManager.sendBroadcast(new Intent(BROADCAST_APPSTATUS_LIST_CHANGED));
Intent intent = new Intent(BROADCAST_APPSTATUS_LIST_CHANGED);
intent.putExtra(EXTRA_REASON_FOR_CHANGE, reason);
localBroadcastManager.sendBroadcast(intent);
}
}
@ -193,7 +230,7 @@ public final class AppUpdateStatusManager {
for (Apk apk : apksToUpdate) {
addApk(apk, status, null);
}
endBatchUpdates();
endBatchUpdates(status);
}
/**
@ -291,10 +328,17 @@ public final class AppUpdateStatusManager {
}
}
private void endBatchUpdates() {
private void endBatchUpdates(Status status) {
synchronized (appMapping) {
isBatchUpdating = false;
notifyChange();
String reason = null;
if (status == Status.ReadyToInstall) {
reason = REASON_READY_TO_INSTALL;
} else if (status == Status.UpdateAvailable) {
reason = REASON_UPDATES_AVAILABLE;
}
notifyChange(reason);
}
}
@ -306,7 +350,7 @@ public final class AppUpdateStatusManager {
it.remove();
}
}
notifyChange();
notifyChange(REASON_CLEAR_ALL_UPDATES);
}
}
@ -318,7 +362,7 @@ public final class AppUpdateStatusManager {
it.remove();
}
}
notifyChange();
notifyChange(REASON_CLEAR_ALL_INSTALLED);
}
}

View File

@ -0,0 +1,57 @@
package org.fdroid.fdroid;
import android.app.IntentService;
import android.content.Context;
import android.content.Intent;
import android.net.Uri;
import android.support.annotation.Nullable;
import org.fdroid.fdroid.data.Apk;
import org.fdroid.fdroid.data.ApkProvider;
import org.fdroid.fdroid.data.App;
import org.fdroid.fdroid.data.AppProvider;
import org.fdroid.fdroid.data.Schema;
import org.fdroid.fdroid.installer.ApkCache;
import java.util.ArrayList;
import java.util.List;
/**
* Scans the list of downloaded .apk files in the cache for each app which can be updated.
* If a valid .apk file is found then it will tell the {@link AppUpdateStatusManager} that it is
* {@link AppUpdateStatusManager.Status#ReadyToInstall}. This is an {@link IntentService} so as to
* run on a background thread, as it hits the disk a bit to figure out the hash of each downloaded
* file.
*
* TODO: Deal with more than just the suggested version. It should also work for people downloading earlier versions (but still newer than their current)
* TODO: Identify new apps which have not been installed before, but which have been downloading. Currently only works for updates.
*/
public class AppUpdateStatusService extends IntentService {
/**
* Queue up a background scan of all downloaded apk files to see if we should notify the user
* that they are ready to install.
*/
public static void scanDownloadedApks(Context context) {
context.startService(new Intent(context, AppUpdateStatusService.class));
}
public AppUpdateStatusService() {
super("AppUpdateStatusService");
}
@Override
protected void onHandleIntent(@Nullable Intent intent) {
List<App> apps = AppProvider.Helper.findCanUpdate(this, Schema.AppMetadataTable.Cols.ALL);
List<Apk> apksReadyToInstall = new ArrayList<>();
for (App app : apps) {
Apk apk = ApkProvider.Helper.findApkFromAnyRepo(this, app.packageName, app.suggestedVersionCode);
Uri downloadUri = Uri.parse(apk.getUrl());
if (ApkCache.apkIsCached(ApkCache.getApkDownloadPath(this, downloadUri), apk)) {
apksReadyToInstall.add(apk);
}
}
AppUpdateStatusManager.getInstance(this).addApks(apksReadyToInstall, AppUpdateStatusManager.Status.ReadyToInstall);
}
}

View File

@ -234,6 +234,7 @@ public class FDroidApp extends Application {
Preferences.get().configureProxy();
InstalledAppProviderService.compareToPackageManager(this);
AppUpdateStatusService.scanDownloadedApks(this);
// If the user changes the preference to do with filtering rooted apps,
// it is easier to just notify a change in the app provider,

View File

@ -410,7 +410,8 @@ class NotificationHelper {
// Intent to open main app list
Intent intentObject = new Intent(context, MainActivity.class);
PendingIntent piAction = PendingIntent.getActivity(context, 0, intentObject, 0);
intentObject.putExtra(MainActivity.EXTRA_VIEW_UPDATES, true);
PendingIntent piAction = PendingIntent.getActivity(context, 0, intentObject, PendingIntent.FLAG_UPDATE_CURRENT);
NotificationCompat.Builder builder =
new NotificationCompat.Builder(context)

View File

@ -20,7 +20,10 @@ import android.support.v7.widget.RecyclerView;
import android.text.TextUtils;
import android.view.View;
import android.view.ViewOutlineProvider;
import android.widget.Button;
import android.widget.ImageButton;
import android.widget.ImageView;
import android.widget.ProgressBar;
import android.widget.TextView;
import com.nostra13.universalimageloader.core.DisplayImageOptions;
@ -70,6 +73,18 @@ public class AppListItemController extends RecyclerView.ViewHolder {
@Nullable
private final TextView ignoredStatus;
@Nullable
private final ProgressBar progressBar;
@Nullable
private final ImageButton cancelButton;
/**
* Will operate as the "Download is complete, click to (install|update)" button.
*/
@Nullable
private final Button actionButton;
private final DisplayImageOptions displayImageOptions;
private App currentApp;
@ -107,6 +122,17 @@ public class AppListItemController extends RecyclerView.ViewHolder {
status = (TextView) itemView.findViewById(R.id.status);
installedVersion = (TextView) itemView.findViewById(R.id.installed_version);
ignoredStatus = (TextView) itemView.findViewById(R.id.ignored_status);
progressBar = (ProgressBar) itemView.findViewById(R.id.progress_bar);
cancelButton = (ImageButton) itemView.findViewById(R.id.cancel_button);
actionButton = (Button) itemView.findViewById(R.id.action_button);
if (actionButton != null) {
actionButton.setOnClickListener(onInstallClicked);
}
if (cancelButton != null) {
cancelButton.setOnClickListener(onCancelDownload);
}
displayImageOptions = Utils.getImageLoadingOptions().build();
@ -115,7 +141,6 @@ public class AppListItemController extends RecyclerView.ViewHolder {
public void bindModel(@NonNull App app) {
currentApp = app;
name.setText(Utils.formatAppNameAndSummary(app.name, app.summary));
ImageLoader.getInstance().displayImage(app.iconUrl, icon, displayImageOptions);
@ -129,10 +154,12 @@ public class AppListItemController extends RecyclerView.ViewHolder {
broadcastManager.registerReceiver(onDownloadProgress, DownloaderService.getIntentFilter(currentAppDownloadUrl));
broadcastManager.registerReceiver(onInstallAction, Installer.getInstallIntentFilter(Uri.parse(currentAppDownloadUrl)));
configureAppName(app);
configureStatusText(app);
configureInstalledVersion(app);
configureIgnoredStatus(app);
configureInstallButton(app);
configureActionButton(app);
}
/**
@ -140,8 +167,6 @@ public class AppListItemController extends RecyclerView.ViewHolder {
* * 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) {
@ -203,6 +228,10 @@ public class AppListItemController extends RecyclerView.ViewHolder {
}
}
/**
* Queries the {@link AppUpdateStatusManager} to find out if there are any apks corresponding to
* `app` which are ready to install.
*/
private boolean isReadyToInstall(@NonNull App app) {
for (AppUpdateStatusManager.AppUpdateStatus appStatus : AppUpdateStatusManager.getInstance(activity).getByPackageName(app.packageName)) {
if (appStatus.status == AppUpdateStatusManager.Status.ReadyToInstall) {
@ -212,13 +241,74 @@ public class AppListItemController extends RecyclerView.ViewHolder {
return false;
}
/**
* Queries the {@link AppUpdateStatusManager} to find out if there are any apks corresponding to
* `app` which are in the process of being downloaded.
*/
private boolean isDownloading(@NonNull App app) {
for (AppUpdateStatusManager.AppUpdateStatus appStatus : AppUpdateStatusManager.getInstance(activity).getByPackageName(app.packageName)) {
if (appStatus.status == AppUpdateStatusManager.Status.Downloading) {
return true;
}
}
return false;
}
/**
* The app name {@link TextView} is used for a few reasons:
* * Display name + summary of the app (most common).
* * If downloading, mention that it is downloading instead of showing the summary.
* * If downloaded and ready to install, mention that it is ready to update/install.
*/
private void configureAppName(@NonNull App app) {
if (isReadyToInstall(app)) {
if (app.isInstalled()) {
String appName = activity.getString(R.string.app_list__name__downloaded_and_ready_to_update, app.name);
if (app.lastUpdated != null) {
long ageInMillis = System.currentTimeMillis() - app.lastUpdated.getTime();
int ageInDays = (int) (ageInMillis / 1000 / 60 / 60 / 24);
String age = activity.getResources().getQuantityString(R.plurals.app_list__age__released_x_days_ago, ageInDays, ageInDays);
name.setText(appName + "\n" + age);
} else {
name.setText(appName);
}
} else {
name.setText(activity.getString(R.string.app_list__name__downloaded_and_ready_to_install, app.name));
}
} else if (isDownloading(app)) {
name.setText(activity.getString(R.string.app_list__name__downloading_in_progress, app.name));
} else {
name.setText(Utils.formatAppNameAndSummary(app.name, app.summary));
}
}
/**
* The action button will either tell the user to "Update" or "Install" the app. Both actually do
* the same thing (launch the package manager). It depends on whether the app has a previous
* version installed or not as to the chosen terminology.
*/
private void configureActionButton(@NonNull App app) {
if (actionButton == null) {
return;
}
if (!isReadyToInstall(app)) {
actionButton.setVisibility(View.GONE);
} else {
actionButton.setVisibility(View.VISIBLE);
if (app.isInstalled()) {
actionButton.setText(R.string.app__install_downloaded_update);
} else {
actionButton.setText(R.string.menu_install);
}
}
}
/**
* 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) {
@ -242,6 +332,61 @@ public class AppListItemController extends RecyclerView.ViewHolder {
}
}
private void onDownloadStarted() {
if (installButton != null) {
installButton.setImageDrawable(ContextCompat.getDrawable(activity, R.drawable.ic_download_progress));
installButton.setImageLevel(0);
}
if (progressBar != null) {
progressBar.setVisibility(View.VISIBLE);
progressBar.setIndeterminate(true);
}
if (cancelButton != null) {
cancelButton.setVisibility(View.VISIBLE);
}
}
private void onDownloadProgressUpdated(int bytesRead, int totalBytes) {
if (installButton != null) {
installButton.setImageDrawable(ContextCompat.getDrawable(activity, R.drawable.ic_download_progress));
int progressAsDegrees = totalBytes <= 0 ? 0 : (int) (((float) bytesRead / totalBytes) * 360);
installButton.setImageLevel(progressAsDegrees);
}
if (progressBar != null) {
progressBar.setVisibility(View.VISIBLE);
if (totalBytes <= 0) {
progressBar.setIndeterminate(true);
} else {
progressBar.setIndeterminate(false);
progressBar.setMax(totalBytes);
progressBar.setProgress(bytesRead);
}
}
if (cancelButton != null) {
cancelButton.setVisibility(View.VISIBLE);
}
}
private void onDownloadComplete() {
if (installButton != null) {
installButton.setImageDrawable(ContextCompat.getDrawable(activity, R.drawable.ic_download_complete));
}
if (progressBar != null) {
progressBar.setVisibility(View.GONE);
}
if (cancelButton != null) {
cancelButton.setVisibility(View.GONE);
}
configureActionButton(currentApp);
}
@SuppressWarnings("FieldCanBeLocal")
private final View.OnClickListener onAppClicked = new View.OnClickListener() {
@Override
@ -262,22 +407,27 @@ public class AppListItemController extends RecyclerView.ViewHolder {
}
};
/**
* Updates both the progress bar and the circular install button (which shows progress around the outside of the circle).
* Also updates the app label to indicate that the app is being downloaded.
*/
private final BroadcastReceiver onDownloadProgress = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
if (installButton == null || currentApp == null || !TextUtils.equals(currentAppDownloadUrl, intent.getDataString())) {
if (currentApp == null || !TextUtils.equals(currentAppDownloadUrl, intent.getDataString()) || (installButton == null && progressBar == null)) {
return;
}
if (Downloader.ACTION_PROGRESS.equals(intent.getAction())) {
installButton.setImageDrawable(ContextCompat.getDrawable(activity, R.drawable.ic_download_progress));
int bytesRead = intent.getIntExtra(Downloader.EXTRA_BYTES_READ, 0);
int totalBytes = intent.getIntExtra(Downloader.EXTRA_TOTAL_BYTES, 100);
configureAppName(currentApp);
int progressAsDegrees = (int) (((float) bytesRead / totalBytes) * 360);
installButton.setImageLevel(progressAsDegrees);
if (Downloader.ACTION_STARTED.equals(intent.getAction())) {
onDownloadStarted();
} else if (Downloader.ACTION_PROGRESS.equals(intent.getAction())) {
int bytesRead = intent.getIntExtra(Downloader.EXTRA_BYTES_READ, 0);
int totalBytes = intent.getIntExtra(Downloader.EXTRA_TOTAL_BYTES, 0);
onDownloadProgressUpdated(bytesRead, totalBytes);
} else if (Downloader.ACTION_COMPLETE.equals(intent.getAction())) {
installButton.setImageDrawable(ContextCompat.getDrawable(activity, R.drawable.ic_download_complete));
onDownloadComplete();
}
}
};
@ -289,6 +439,8 @@ public class AppListItemController extends RecyclerView.ViewHolder {
return;
}
configureAppName(currentApp);
Apk apk = intent.getParcelableExtra(Installer.EXTRA_APK);
if (!TextUtils.equals(apk.packageName, currentApp.packageName)) {
return;
@ -347,4 +499,16 @@ public class AppListItemController extends RecyclerView.ViewHolder {
}
}
};
@SuppressWarnings("FieldCanBeLocal")
private final View.OnClickListener onCancelDownload = new View.OnClickListener() {
@Override
public void onClick(View v) {
if (currentAppDownloadUrl == null) {
return;
}
InstallManagerService.cancel(activity, currentAppDownloadUrl);
}
};
}

View File

@ -119,7 +119,7 @@ public class CategorySpan extends ReplacementSpan {
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);
Drawable icon = ContextCompat.getDrawable(context, R.drawable.ic_categories);
icon.setBounds(iconPadding, iconPadding, iconPadding + iconSize, iconPadding + iconSize);
icon.draw(canvas);

View File

@ -1,21 +1,31 @@
package org.fdroid.fdroid.views.main;
import android.app.SearchManager;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.database.Cursor;
import android.net.Uri;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.design.widget.BottomNavigationView;
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.v4.content.LocalBroadcastManager;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.LinearLayoutManager;
import android.text.TextUtils;
import android.view.MenuItem;
import android.widget.Toast;
import android.support.v7.widget.RecyclerView;
import com.ashokvarma.bottomnavigation.BadgeItem;
import com.ashokvarma.bottomnavigation.BottomNavigationBar;
import com.ashokvarma.bottomnavigation.BottomNavigationItem;
import org.fdroid.fdroid.AppDetails;
import org.fdroid.fdroid.AppDetails2;
import org.fdroid.fdroid.AppUpdateStatusManager;
import org.fdroid.fdroid.FDroidApp;
import org.fdroid.fdroid.NfcHelper;
import org.fdroid.fdroid.Preferences;
@ -23,7 +33,9 @@ import org.fdroid.fdroid.R;
import org.fdroid.fdroid.UpdateService;
import org.fdroid.fdroid.Utils;
import org.fdroid.fdroid.compat.UriCompat;
import org.fdroid.fdroid.data.AppProvider;
import org.fdroid.fdroid.data.NewRepoConfig;
import org.fdroid.fdroid.data.Schema;
import org.fdroid.fdroid.views.ManageReposActivity;
import org.fdroid.fdroid.views.apps.AppListActivity;
import org.fdroid.fdroid.views.swap.SwapWorkflowActivity;
@ -35,30 +47,38 @@ import org.fdroid.fdroid.views.swap.SwapWorkflowActivity;
* + Whats new
* + Categories list
* + App swap
* + My apps
* + Updates
* + 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 {
public class MainActivity extends AppCompatActivity implements BottomNavigationBar.OnTabSelectedListener,
LoaderManager.LoaderCallbacks<Cursor> {
private static final String TAG = "MainActivity";
public static final String EXTRA_VIEW_MY_APPS = "org.fdroid.fdroid.views.main.MainActivity.VIEW_MY_APPS";
public static final String EXTRA_VIEW_UPDATES = "org.fdroid.fdroid.views.main.MainActivity.VIEW_UPDATES";
private static final String ADD_REPO_INTENT_HANDLED = "addRepoIntentHandled";
private static final String ACTION_ADD_REPO = "org.fdroid.fdroid.MainActivity.ACTION_ADD_REPO";
private static final String STATE_SELECTED_MENU_ID = "selectedMenuId";
private static final int LOADER_NUM_UPDATES = 1;
private static final int REQUEST_SWAP = 3;
private RecyclerView pager;
private MainViewAdapter adapter;
private BottomNavigationBar bottomNavigation;
private int selectedMenuId = R.id.whats_new;
private BadgeItem updatesBadge;
@Override
protected void onCreate(Bundle savedInstanceState) {
protected void onCreate(@Nullable Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
@ -70,8 +90,29 @@ public class MainActivity extends AppCompatActivity implements BottomNavigationV
pager.setLayoutManager(new NonScrollingHorizontalLayoutManager(this));
pager.setAdapter(adapter);
BottomNavigationView bottomNavigation = (BottomNavigationView) findViewById(R.id.bottom_navigation);
bottomNavigation.setOnNavigationItemSelectedListener(this);
updatesBadge = new BadgeItem();
bottomNavigation = (BottomNavigationBar) findViewById(R.id.bottom_navigation);
bottomNavigation.setTabSelectedListener(this)
.setBarBackgroundColor(R.color.fdroid_blue)
.setInActiveColor(R.color.bottom_nav_items)
.setActiveColor(android.R.color.white)
.setMode(BottomNavigationBar.MODE_FIXED)
.addItem(new BottomNavigationItem(R.drawable.ic_latest, R.string.main_menu__latest_apps))
.addItem(new BottomNavigationItem(R.drawable.ic_categories, R.string.main_menu__categories))
.addItem(new BottomNavigationItem(R.drawable.ic_nearby, R.string.main_menu__swap_nearby))
.addItem(new BottomNavigationItem(R.drawable.ic_updates, R.string.updates).setBadgeItem(updatesBadge))
.addItem(new BottomNavigationItem(R.drawable.ic_settings, R.string.menu_settings))
.initialise();
IntentFilter updateableAppsFilter = new IntentFilter(AppUpdateStatusManager.BROADCAST_APPSTATUS_LIST_CHANGED);
LocalBroadcastManager.getInstance(this).registerReceiver(onUpdateableAppsChanged, updateableAppsFilter);
getSupportLoaderManager().initLoader(LOADER_NUM_UPDATES, null, this);
if (savedInstanceState != null) {
selectedMenuId = savedInstanceState.getInt(STATE_SELECTED_MENU_ID, R.id.whats_new);
}
setSelectedMenuInNav();
initialRepoUpdateIfRequired();
@ -79,6 +120,16 @@ public class MainActivity extends AppCompatActivity implements BottomNavigationV
handleSearchOrAppViewIntent(intent);
}
@Override
protected void onSaveInstanceState(Bundle outState) {
outState.putInt(STATE_SELECTED_MENU_ID, selectedMenuId);
super.onSaveInstanceState(outState);
}
private void setSelectedMenuInNav() {
bottomNavigation.selectTab(adapter.adapterPositionFromItemId(selectedMenuId));
}
/**
* The first time the app is run, we will have an empty app list. To deal with this, we will
* attempt to update with the default repo. However, if we have tried this at least once, then
@ -99,9 +150,11 @@ public class MainActivity extends AppCompatActivity implements BottomNavigationV
FDroidApp.checkStartTor(this);
if (getIntent().hasExtra(EXTRA_VIEW_MY_APPS)) {
getIntent().removeExtra(EXTRA_VIEW_MY_APPS);
pager.scrollToPosition(adapter.adapterPositionFromItemId(R.id.my_apps));
if (getIntent().hasExtra(EXTRA_VIEW_UPDATES)) {
getIntent().removeExtra(EXTRA_VIEW_UPDATES);
pager.scrollToPosition(adapter.adapterPositionFromItemId(R.id.updates));
selectedMenuId = R.id.updates;
setSelectedMenuInNav();
}
// AppDetails 2 and RepoDetailsActivity set different NFC actions, so reset here
@ -127,9 +180,19 @@ public class MainActivity extends AppCompatActivity implements BottomNavigationV
}
@Override
public boolean onNavigationItemSelected(@NonNull MenuItem item) {
pager.scrollToPosition(((MainViewAdapter) pager.getAdapter()).adapterPositionFromItemId(item.getItemId()));
return true;
public void onTabSelected(int position) {
pager.scrollToPosition(position);
selectedMenuId = (int) adapter.getItemId(position);
}
@Override
public void onTabUnselected(int position) {
}
@Override
public void onTabReselected(int position) {
}
private void handleSearchOrAppViewIntent(Intent intent) {
@ -257,6 +320,36 @@ public class MainActivity extends AppCompatActivity implements BottomNavigationV
}
}
@Override
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
Uri uri = AppProvider.getCanUpdateUri();
String[] projection = new String[]{Schema.AppMetadataTable.Cols._COUNT};
return new CursorLoader(this, uri, projection, null, null, null);
}
@Override
public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) {
cursor.moveToFirst();
int canUpdateCount = cursor.getInt(cursor.getColumnIndex(Schema.AppMetadataTable.Cols._COUNT));
cursor.close();
refreshUpdatesBadge(canUpdateCount);
}
private void refreshUpdatesBadge(int canUpdateCount) {
if (canUpdateCount == 0) {
updatesBadge.hide(true);
} else {
updatesBadge.setText(Integer.toString(canUpdateCount));
updatesBadge.show(true);
}
}
@Override
public void onLoaderReset(Loader<Cursor> loader) {
}
private static class NonScrollingHorizontalLayoutManager extends LinearLayoutManager {
NonScrollingHorizontalLayoutManager(Context context) {
super(context, LinearLayoutManager.HORIZONTAL, false);
@ -273,4 +366,13 @@ public class MainActivity extends AppCompatActivity implements BottomNavigationV
}
}
private final BroadcastReceiver onUpdateableAppsChanged = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
if (AppUpdateStatusManager.REASON_UPDATES_AVAILABLE.equals(intent.getStringExtra(AppUpdateStatusManager.EXTRA_REASON_FOR_CHANGE))) {
getSupportLoaderManager().restartLoader(LOADER_NUM_UPDATES, null, MainActivity.this);
}
}
};
}

View File

@ -13,7 +13,7 @@ import org.fdroid.fdroid.R;
* + Whats new
* + Categories
* + Nearby
* + My Apps
* + Updates
* + Settings
*
* It is responsible for understanding the relationship between each main view that is reachable
@ -35,10 +35,26 @@ class MainViewAdapter extends RecyclerView.Adapter<MainViewController> {
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(3, R.id.updates);
positionToId.put(4, R.id.settings);
}
@Override
public void onViewDetachedFromWindow(MainViewController holder) {
long viewType = getItemId(holder.getAdapterPosition());
if (viewType == R.id.updates) {
holder.unbindUpdates();
}
}
@Override
public void onViewAttachedToWindow(MainViewController holder) {
long viewType = getItemId(holder.getAdapterPosition());
if (viewType == R.id.updates) {
holder.bindUpdates();
}
}
@Override
public MainViewController onCreateViewHolder(ViewGroup parent, int viewType) {
MainViewController holder = createEmptyView();
@ -52,8 +68,8 @@ class MainViewAdapter extends RecyclerView.Adapter<MainViewController> {
case R.id.nearby:
holder.bindSwapView();
break;
case R.id.my_apps:
holder.bindMyApps();
case R.id.updates:
holder.bindUpdates();
break;
case R.id.settings:
holder.bindSettingsView();

View File

@ -1,6 +1,7 @@
package org.fdroid.fdroid.views.main;
import android.content.Intent;
import android.support.annotation.Nullable;
import android.support.v4.app.Fragment;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.RecyclerView;
@ -10,7 +11,7 @@ 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.updates.UpdatesViewBinder;
import org.fdroid.fdroid.views.swap.SwapWorkflowActivity;
/**
@ -24,6 +25,9 @@ class MainViewController extends RecyclerView.ViewHolder {
private final AppCompatActivity activity;
private final FrameLayout frame;
@Nullable
private UpdatesViewBinder updatesView = null;
MainViewController(AppCompatActivity activity, FrameLayout frame) {
super(frame);
this.activity = activity;
@ -38,10 +42,20 @@ class MainViewController extends RecyclerView.ViewHolder {
}
/**
* @see MyAppsViewBinder
* @see UpdatesViewBinder
*/
public void bindMyApps() {
new MyAppsViewBinder(activity, frame);
public void bindUpdates() {
if (updatesView == null) {
updatesView = new UpdatesViewBinder(activity, frame);
}
updatesView.bind();
}
public void unbindUpdates() {
if (updatesView != null) {
updatesView.unbind();
}
}
/**

View File

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

View File

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

View File

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

View File

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

View File

@ -0,0 +1,365 @@
package org.fdroid.fdroid.views.updates;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.database.Cursor;
import android.os.Bundle;
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.v4.content.LocalBroadcastManager;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.text.TextUtils;
import android.view.ViewGroup;
import com.hannesdorfmann.adapterdelegates3.AdapterDelegatesManager;
import org.fdroid.fdroid.AppUpdateStatusManager;
import org.fdroid.fdroid.data.App;
import org.fdroid.fdroid.data.AppProvider;
import org.fdroid.fdroid.data.Schema;
import org.fdroid.fdroid.views.updates.items.AppNotification;
import org.fdroid.fdroid.views.updates.items.AppStatus;
import org.fdroid.fdroid.views.updates.items.AppUpdateData;
import org.fdroid.fdroid.views.updates.items.DonationPrompt;
import org.fdroid.fdroid.views.updates.items.UpdateableApp;
import org.fdroid.fdroid.views.updates.items.UpdateableAppsHeader;
import java.util.ArrayList;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
/**
* Manages the following types of information:
* * Apps marked for downloading (while the user is offline)
* * Currently downloading apps
* * Apps which have been downloaded (and need further action to install). This includes new installs and updates.
* * Reminders to users that they can donate to apps (only shown infrequently after several updates)
* * A list of apps which are eligible to be updated (for when the "Automatic Updates" option is disabled), including:
* + A summary of all apps to update including an "Update all" button and a "Show apps" button.
* + Once "Show apps" is expanded then each app is shown along with its own download button.
*
* It does this by maintaining several different lists of interesting apps. Each list contains wrappers
* around the piece of data it wants to render ({@link AppStatus}, {@link DonationPrompt},
* {@link AppNotification}, {@link UpdateableApp}). Instead of juggling the various viewTypes
* to find out which position in the adapter corresponds to which view type, this is handled by
* the {@link UpdatesAdapter#delegatesManager}.
*
* There are a series of type-safe lists which hold the specific data this adapter is interested in.
* This data is then collated into a single list (see {@link UpdatesAdapter#populateItems()}) which
* is the actual thing the adapter binds too. At any point it is safe to clear the single list and
* repopulate it from the original source lists of data. When this is done, the adapter will notify
* the recycler view that its data has changed. Sometimes it will also ask the recycler view to
* scroll to the newly added item (if attached to the recycler view).
*/
public class UpdatesAdapter extends RecyclerView.Adapter<RecyclerView.ViewHolder> implements LoaderManager.LoaderCallbacks<Cursor> {
private final AdapterDelegatesManager<List<AppUpdateData>> delegatesManager = new AdapterDelegatesManager<>();
private final List<AppUpdateData> items = new ArrayList<>();
private final AppCompatActivity activity;
@Nullable
private RecyclerView recyclerView;
private final List<AppStatus> appsToShowStatus = new ArrayList<>();
private final List<DonationPrompt> appsToPromptForDonation = new ArrayList<>();
private final List<AppNotification> appsToNotifyAbout = new ArrayList<>();
private final List<UpdateableApp> updateableApps = new ArrayList<>();
private boolean showAllUpdateableApps = false;
public UpdatesAdapter(AppCompatActivity activity) {
this.activity = activity;
delegatesManager.addDelegate(new AppStatus.Delegate(activity))
.addDelegate(new AppNotification.Delegate())
.addDelegate(new DonationPrompt.Delegate())
.addDelegate(new UpdateableApp.Delegate(activity))
.addDelegate(new UpdateableAppsHeader.Delegate(activity));
populateAppStatuses();
notifyDataSetChanged();
activity.getSupportLoaderManager().initLoader(0, null, this);
}
/**
* There are some statuses managed by {@link AppUpdateStatusManager} which we don't care about
* for the "Updates" view. For example {@link org.fdroid.fdroid.AppUpdateStatusManager.Status#Installed}
* apps are not interesting in the Updates" view at this point in time. Also, although this
* adapter does know about apps with updates availble, it does so by querying the database not
* by querying the app update status manager. As such, apps with the status
* {@link org.fdroid.fdroid.AppUpdateStatusManager.Status#UpdateAvailable} are not interesting here.
*/
private boolean shouldShowStatus(AppUpdateStatusManager.AppUpdateStatus status) {
return status.status == AppUpdateStatusManager.Status.Unknown ||
status.status == AppUpdateStatusManager.Status.Downloading ||
status.status == AppUpdateStatusManager.Status.ReadyToInstall;
}
/**
* Adds items from the {@link AppUpdateStatusManager} to {@link UpdatesAdapter#appsToShowStatus}.
* Note that this will then subsequently rebuild the underlying adapter data structure by
* invoking {@link UpdatesAdapter#populateItems}. However as per the populateItems method, it
* does not know how best to notify the recycler view of any changes. That is up to the caller
* of this method.
*/
private void populateAppStatuses() {
for (AppUpdateStatusManager.AppUpdateStatus status : AppUpdateStatusManager.getInstance(activity).getAll()) {
if (shouldShowStatus(status)) {
appsToShowStatus.add(new AppStatus(activity, status));
}
}
Collections.sort(appsToShowStatus, new Comparator<AppStatus>() {
@Override
public int compare(AppStatus o1, AppStatus o2) {
return o1.status.app.name.compareTo(o2.status.app.name);
}
});
populateItems();
}
public boolean canViewAllUpdateableApps() {
return showAllUpdateableApps;
}
public void toggleAllUpdateableApps() {
showAllUpdateableApps = !showAllUpdateableApps;
populateItems();
if (showAllUpdateableApps) {
notifyItemRangeInserted(appsToShowStatus.size() + 1, updateableApps.size());
if (recyclerView != null) {
// Scroll so that the "Update X apps" header is at the top of the page, and the
// list of apps takes up the rest of the screen.
((LinearLayoutManager) recyclerView.getLayoutManager()).scrollToPositionWithOffset(appsToShowStatus.size(), 0);
}
} else {
notifyItemRangeRemoved(appsToShowStatus.size() + 1, updateableApps.size());
}
}
/**
* Completely rebuilds the underlying data structure used by this adapter. Note however, that
* this does not notify the recycler view of any changes. Thus, it is up to other methods which
* initiate a call to this method to make sure they appropriately notify the recyler view.
*/
private void populateItems() {
items.clear();
items.addAll(appsToShowStatus);
if (updateableApps != null && updateableApps.size() > 0) {
items.add(new UpdateableAppsHeader(activity, this, updateableApps));
if (showAllUpdateableApps) {
items.addAll(updateableApps);
}
}
items.addAll(appsToPromptForDonation);
items.addAll(appsToNotifyAbout);
}
@Override
public int getItemCount() {
return items.size();
}
@Override
public int getItemViewType(int position) {
return delegatesManager.getItemViewType(items, position);
}
@Override
public RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent, int viewType) {
return delegatesManager.onCreateViewHolder(parent, viewType);
}
@Override
public void onBindViewHolder(RecyclerView.ViewHolder holder, int position) {
delegatesManager.onBindViewHolder(items, position, holder);
}
@Override
public void onAttachedToRecyclerView(RecyclerView recyclerView) {
super.onAttachedToRecyclerView(recyclerView);
this.recyclerView = recyclerView;
}
@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,
Schema.AppMetadataTable.Cols.NAME
);
}
@Override
public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) {
int numberRemoved = updateableApps.size();
boolean hadHeader = updateableApps.size() > 0;
boolean willHaveHeader = cursor.getCount() > 0;
updateableApps.clear();
notifyItemRangeRemoved(appsToShowStatus.size(), numberRemoved + (hadHeader ? 1 : 0));
cursor.moveToFirst();
while (!cursor.isAfterLast()) {
updateableApps.add(new UpdateableApp(activity, new App(cursor)));
cursor.moveToNext();
}
populateItems();
notifyItemRangeInserted(appsToShowStatus.size(), updateableApps.size() + (willHaveHeader ? 1 : 0));
}
@Override
public void onLoaderReset(Loader<Cursor> loader) { }
/**
* Doesn't listen for {@link AppUpdateStatusManager#BROADCAST_APPSTATUS_CHANGED} because the
* individual items in the recycler view will listen for the appropriate changes in state and
* update themselves accordingly (if they are displayed).
*/
public void listenForStatusUpdates() {
IntentFilter filter = new IntentFilter();
filter.addAction(AppUpdateStatusManager.BROADCAST_APPSTATUS_ADDED);
filter.addAction(AppUpdateStatusManager.BROADCAST_APPSTATUS_REMOVED);
filter.addAction(AppUpdateStatusManager.BROADCAST_APPSTATUS_LIST_CHANGED);
LocalBroadcastManager.getInstance(activity).registerReceiver(receiverAppStatusChanges, filter);
}
public void stopListeningForStatusUpdates() {
LocalBroadcastManager.getInstance(activity).unregisterReceiver(receiverAppStatusChanges);
}
private void onManyAppStatusesChanged(String reasonForChange) {
switch (reasonForChange) {
case AppUpdateStatusManager.REASON_UPDATES_AVAILABLE:
onUpdateableAppsChanged();
break;
case AppUpdateStatusManager.REASON_READY_TO_INSTALL:
onFoundAppsReadyToInstall();
break;
}
}
/**
* Apps have been made available for update which were not available for update before.
* We need to rerun our database query to get a list of apps to update.
*/
private void onUpdateableAppsChanged() {
activity.getSupportLoaderManager().initLoader(0, null, this);
}
/**
* We have completed a scan of .apk files in the cache, and identified there are
* some which are ready to install.
*/
private void onFoundAppsReadyToInstall() {
if (appsToShowStatus.size() > 0) {
int size = appsToShowStatus.size();
appsToShowStatus.clear();
notifyItemRangeRemoved(0, size);
}
populateAppStatuses();
notifyItemRangeInserted(0, appsToShowStatus.size());
if (recyclerView != null) {
recyclerView.smoothScrollToPosition(0);
}
}
private void onAppStatusAdded(String apkUrl) {
// We could try and find the specific place where we need to add our new item, but it is
// far simpler to clear the list and rebuild it (sorting it in the process).
appsToShowStatus.clear();
populateAppStatuses();
// After adding the new item to our list (somewhere) we can then look it back up again in
// order to notify the recycler view and scroll to that item.
int positionOfNewApp = 0;
for (int i = 0; i < appsToShowStatus.size(); i++) {
if (TextUtils.equals(appsToShowStatus.get(i).status.getUniqueKey(), apkUrl)) {
positionOfNewApp = i;
break;
}
}
notifyItemInserted(positionOfNewApp);
if (recyclerView != null) {
recyclerView.smoothScrollToPosition(positionOfNewApp);
}
}
private void onAppStatusRemoved(String apkUrl) {
// Find out where the item is in our internal data structure, so that we can remove it and
// also notify the recycler view appropriately.
int positionOfOldApp = 0;
for (int i = 0; i < appsToShowStatus.size(); i++) {
if (TextUtils.equals(appsToShowStatus.get(i).status.getUniqueKey(), apkUrl)) {
positionOfOldApp = i;
break;
}
}
appsToShowStatus.remove(positionOfOldApp);
populateItems();
notifyItemRemoved(positionOfOldApp);
}
private final BroadcastReceiver receiverAppStatusChanges = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
String apkUrl = intent.getStringExtra(AppUpdateStatusManager.EXTRA_APK_URL);
switch (intent.getAction()) {
case AppUpdateStatusManager.BROADCAST_APPSTATUS_LIST_CHANGED:
onManyAppStatusesChanged(intent.getStringExtra(AppUpdateStatusManager.EXTRA_REASON_FOR_CHANGE));
break;
case AppUpdateStatusManager.BROADCAST_APPSTATUS_ADDED:
onAppStatusAdded(apkUrl);
break;
case AppUpdateStatusManager.BROADCAST_APPSTATUS_REMOVED:
onAppStatusRemoved(apkUrl);
break;
}
}
};
}

View File

@ -0,0 +1,33 @@
package org.fdroid.fdroid.views.updates;
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;
public class UpdatesViewBinder {
private final UpdatesAdapter adapter;
public UpdatesViewBinder(AppCompatActivity activity, FrameLayout parent) {
View view = activity.getLayoutInflater().inflate(R.layout.main_tab_updates, parent, true);
adapter = new UpdatesAdapter(activity);
RecyclerView list = (RecyclerView) view.findViewById(R.id.list);
list.setHasFixedSize(true);
list.setLayoutManager(new LinearLayoutManager(activity));
list.setAdapter(adapter);
}
public void bind() {
adapter.listenForStatusUpdates();
}
public void unbind() {
adapter.stopListeningForStatusUpdates();
}
}

View File

@ -0,0 +1,56 @@
package org.fdroid.fdroid.views.updates.items;
import android.app.Activity;
import android.support.annotation.NonNull;
import android.support.v7.widget.RecyclerView;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import com.hannesdorfmann.adapterdelegates3.AdapterDelegate;
import java.util.List;
/**
* Each of these apps has a notification to display to the user.
* The notification will have come from the apps metadata, provided by its maintainer. It may be
* something about the app being removed from the repository, or perhaps security problems that
* were identified in the app.
*/
public class AppNotification extends AppUpdateData {
public AppNotification(Activity activity) {
super(activity);
}
public static class Delegate extends AdapterDelegate<List<AppUpdateData>> {
@Override
protected boolean isForViewType(@NonNull List<AppUpdateData> items, int position) {
return items.get(position) instanceof AppNotification;
}
@NonNull
@Override
protected RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent) {
return new ViewHolder(new TextView(parent.getContext()));
}
@Override
protected void onBindViewHolder(@NonNull List<AppUpdateData> items, int position, @NonNull RecyclerView.ViewHolder holder, @NonNull List<Object> payloads) {
AppNotification app = (AppNotification) items.get(position);
((ViewHolder) holder).bindApp(app);
}
}
public static class ViewHolder extends RecyclerView.ViewHolder {
public ViewHolder(View itemView) {
super(itemView);
}
public void bindApp(AppNotification app) {
((TextView) itemView).setText("Notification for app");
}
}
}

View File

@ -0,0 +1,57 @@
package org.fdroid.fdroid.views.updates.items;
import android.app.Activity;
import android.support.annotation.NonNull;
import android.support.v7.widget.RecyclerView;
import android.view.ViewGroup;
import com.hannesdorfmann.adapterdelegates3.AdapterDelegate;
import org.fdroid.fdroid.AppUpdateStatusManager;
import org.fdroid.fdroid.R;
import org.fdroid.fdroid.data.App;
import org.fdroid.fdroid.views.apps.AppListItemController;
import java.util.List;
/**
* Apps which we want to show some more substantial information about.
* @see R.layout#updateable_app_status_item The view that this binds to
* @see AppListItemController Used for binding the {@link App} to the {@link R.layout#updateable_app_status_item}
*/
public class AppStatus extends AppUpdateData {
public final AppUpdateStatusManager.AppUpdateStatus status;
public AppStatus(Activity activity, AppUpdateStatusManager.AppUpdateStatus status) {
super(activity);
this.status = status;
}
public static class Delegate extends AdapterDelegate<List<AppUpdateData>> {
private final Activity activity;
public Delegate(Activity activity) {
this.activity = activity;
}
@Override
protected boolean isForViewType(@NonNull List<AppUpdateData> items, int position) {
return items.get(position) instanceof AppStatus;
}
@NonNull
@Override
protected RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent) {
return new AppListItemController(activity, activity.getLayoutInflater().inflate(R.layout.updateable_app_status_item, parent, false));
}
@Override
protected void onBindViewHolder(@NonNull List<AppUpdateData> items, int position, @NonNull RecyclerView.ViewHolder holder, @NonNull List<Object> payloads) {
AppStatus app = (AppStatus) items.get(position);
((AppListItemController) holder).bindModel(app.status.app);
}
}
}

View File

@ -0,0 +1,16 @@
package org.fdroid.fdroid.views.updates.items;
import android.app.Activity;
/**
* Used as a common base class for all data types in the {@link org.fdroid.fdroid.views.updates.UpdatesAdapter}.
* Doesn't have any functionality of its own, but allows the {@link org.fdroid.fdroid.views.updates.UpdatesAdapter#delegatesManager}
* to specify a data type more specific than just {@link Object}.
*/
public abstract class AppUpdateData {
public final Activity activity;
public AppUpdateData(Activity activity) {
this.activity = activity;
}
}

View File

@ -0,0 +1,54 @@
package org.fdroid.fdroid.views.updates.items;
import android.app.Activity;
import android.support.annotation.NonNull;
import android.support.v7.widget.RecyclerView;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import com.hannesdorfmann.adapterdelegates3.AdapterDelegate;
import java.util.List;
/**
* The app (if any) which we should prompt the user about potentially donating to (due to having
* updated several times).
*/
public class DonationPrompt extends AppUpdateData {
public DonationPrompt(Activity activity) {
super(activity);
}
public static class Delegate extends AdapterDelegate<List<AppUpdateData>> {
@Override
protected boolean isForViewType(@NonNull List<AppUpdateData> items, int position) {
return items.get(position) instanceof DonationPrompt;
}
@NonNull
@Override
protected RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent) {
return new ViewHolder(new TextView(parent.getContext()));
}
@Override
protected void onBindViewHolder(@NonNull List<AppUpdateData> items, int position, @NonNull RecyclerView.ViewHolder holder, @NonNull List<Object> payloads) {
DonationPrompt app = (DonationPrompt) items.get(position);
((ViewHolder) holder).bindApp(app);
}
}
public static class ViewHolder extends RecyclerView.ViewHolder {
public ViewHolder(View itemView) {
super(itemView);
}
public void bindApp(DonationPrompt app) {
((TextView) itemView).setText("Donation prompt for app");
}
}
}

View File

@ -0,0 +1,57 @@
package org.fdroid.fdroid.views.updates.items;
import android.app.Activity;
import android.support.annotation.NonNull;
import android.support.v7.widget.RecyclerView;
import android.view.ViewGroup;
import com.hannesdorfmann.adapterdelegates3.AdapterDelegate;
import org.fdroid.fdroid.R;
import org.fdroid.fdroid.data.App;
import org.fdroid.fdroid.views.apps.AppListItemController;
import java.util.List;
/**
* List of all apps which can be updated, but have not yet been downloaded.
* @see UpdateableApp The data that is bound to this view.
* @see R.layout#updateable_app_list_item The view that this binds to.
* @see AppListItemController Used for binding the {@link App} to the {@link R.layout#updateable_app_list_item}
*/
public class UpdateableApp extends AppUpdateData {
public final App app;
public UpdateableApp(Activity activity, App app) {
super(activity);
this.app = app;
}
public static class Delegate extends AdapterDelegate<List<AppUpdateData>> {
private final Activity activity;
public Delegate(Activity activity) {
this.activity = activity;
}
@Override
protected boolean isForViewType(@NonNull List<AppUpdateData> items, int position) {
return items.get(position) instanceof UpdateableApp;
}
@NonNull
@Override
protected RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent) {
return new AppListItemController(activity, activity.getLayoutInflater().inflate(R.layout.updateable_app_list_item, parent, false));
}
@Override
protected void onBindViewHolder(@NonNull List<AppUpdateData> items, int position, @NonNull RecyclerView.ViewHolder holder, @NonNull List<Object> payloads) {
UpdateableApp app = (UpdateableApp) items.get(position);
((AppListItemController) holder).bindModel(app.app);
}
}
}

View File

@ -0,0 +1,120 @@
package org.fdroid.fdroid.views.updates.items;
import android.app.Activity;
import android.support.annotation.NonNull;
import android.support.v7.widget.RecyclerView;
import android.text.TextUtils;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.Button;
import android.widget.ImageView;
import android.widget.TextView;
import com.hannesdorfmann.adapterdelegates3.AdapterDelegate;
import org.fdroid.fdroid.R;
import org.fdroid.fdroid.UpdateService;
import org.fdroid.fdroid.views.updates.UpdatesAdapter;
import java.util.ArrayList;
import java.util.List;
/**
* Summary of all apps that can be downloaded. Includes a button to download all of them and also
* a toggle to show or hide the list of each individual item.
* @see R.layout#updates_header The view that this binds to.
* @see UpdateableAppsHeader The data that is bound to this view.
*/
public class UpdateableAppsHeader extends AppUpdateData {
public final List<UpdateableApp> apps;
public final UpdatesAdapter adapter;
public UpdateableAppsHeader(Activity activity, UpdatesAdapter updatesAdapter, List<UpdateableApp> updateableApps) {
super(activity);
apps = updateableApps;
adapter = updatesAdapter;
}
public static class Delegate extends AdapterDelegate<List<AppUpdateData>> {
private final LayoutInflater inflater;
public Delegate(Activity activity) {
inflater = activity.getLayoutInflater();
}
@Override
protected boolean isForViewType(@NonNull List<AppUpdateData> items, int position) {
return items.get(position) instanceof UpdateableAppsHeader;
}
@NonNull
@Override
protected RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent) {
return new ViewHolder(inflater.inflate(R.layout.updates_header, parent, false));
}
@Override
protected void onBindViewHolder(@NonNull List<AppUpdateData> items, int position, @NonNull RecyclerView.ViewHolder holder, @NonNull List<Object> payloads) {
UpdateableAppsHeader app = (UpdateableAppsHeader) items.get(position);
((ViewHolder) holder).bindHeader(app);
}
}
public static class ViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener {
private UpdateableAppsHeader header;
private final TextView updatesAvailable;
private final ImageView downloadAll;
private final TextView appsToUpdate;
private final Button toggleAppsToUpdate;
public ViewHolder(View itemView) {
super(itemView);
updatesAvailable = (TextView) itemView.findViewById(R.id.text_updates_available);
downloadAll = (ImageView) itemView.findViewById(R.id.button_download_all);
appsToUpdate = (TextView) itemView.findViewById(R.id.text_apps_to_update);
toggleAppsToUpdate = (Button) itemView.findViewById(R.id.button_toggle_apps_to_update);
toggleAppsToUpdate.setOnClickListener(this);
downloadAll.setOnClickListener(this);
}
public void bindHeader(UpdateableAppsHeader header) {
this.header = header;
updatesAvailable.setText(itemView.getResources().getQuantityString(R.plurals.updates__download_updates_for_apps, header.apps.size(), header.apps.size()));
List<String> appNames = new ArrayList<>(header.apps.size());
for (UpdateableApp app : header.apps) {
appNames.add(app.app.name);
}
appsToUpdate.setText(TextUtils.join(", ", appNames));
updateToggleButtonText();
}
@Override
public void onClick(View v) {
if (v == toggleAppsToUpdate) {
header.adapter.toggleAllUpdateableApps();
updateToggleButtonText();
} else if (v == downloadAll) {
UpdateService.autoDownloadUpdates(header.activity);
}
}
private void updateToggleButtonText() {
if (header.adapter.canViewAllUpdateableApps()) {
toggleAppsToUpdate.setText(R.string.updates__hide_updateable_apps);
} else {
toggleAppsToUpdate.setText(R.string.updates__show_updateable_apps);
}
}
}
}

View File

@ -0,0 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:color="@android:color/white" android:state_checked="true"/>
<item android:color="@android:color/white" android:state_pressed="true"/>
<item android:color="#B8D4F0"/>
</selector>

View File

@ -0,0 +1,9 @@
<?xml version="1.0" encoding="utf-8"?>
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item>
<shape android:shape="rectangle">
<corners android:radius="@dimen/badge_size" />
<solid android:color="#ffdd0000" />
</shape>
</item>
</selector>

View File

@ -2,25 +2,25 @@
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_pressed="true">
<shape android:shape="rectangle">
<corners android:radius="3dp" />
<corners android:radius="24dp" />
<solid android:color="@color/fdroid_blue_dark" />
</shape>
</item>
<item android:state_checked="true">
<shape android:shape="rectangle">
<corners android:radius="3dp" />
<corners android:radius="24dp" />
<solid android:color="@color/fdroid_blue_dark" />
</shape>
</item>
<item android:state_enabled="false">
<shape android:shape="rectangle">
<corners android:radius="3dp" />
<corners android:radius="24dp" />
<solid android:color="@color/fdroid_blue_dark" />
</shape>
</item>
<item>
<shape android:shape="rectangle">
<corners android:radius="3dp" />
<corners android:radius="24dp" />
<solid android:color="@color/fdroid_blue" />
</shape>
</item>

View File

@ -2,25 +2,25 @@
<selector xmlns:android="http://schemas.android.com/apk/res/android">
<item android:state_pressed="true">
<shape android:shape="rectangle">
<corners android:radius="3dp" />
<corners android:radius="24dp" />
<solid android:color="@color/fdroid_blue" />
</shape>
</item>
<item android:state_checked="true">
<shape android:shape="rectangle">
<corners android:radius="3dp" />
<corners android:radius="24dp" />
<solid android:color="@color/fdroid_blue" />
</shape>
</item>
<item android:state_enabled="false">
<shape android:shape="rectangle">
<corners android:radius="3dp" />
<corners android:radius="24dp" />
<solid android:color="@color/fdroid_blue" />
</shape>
</item>
<item>
<shape android:shape="rectangle">
<corners android:radius="3dp" />
<corners android:radius="24dp" />
<solid android:color="@android:color/white" />
<stroke android:color="@color/fdroid_blue" android:width="2dp" />
</shape>

View File

@ -0,0 +1,6 @@
<vector android:height="24dp" android:viewportHeight="42.0"
android:viewportWidth="42.0" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#ffffff"
android:pathData="M38,3H4C2.9,3 2,3.9 2,5v12c0,1.1 0.9,2 2,2h34c1.1,0 2,-0.9 2,-2V5C40,3.9 39.1,3 38,3m0,20H4c-1.1,0 -2,0.9 -2,2v12c0,1.1 0.9,2 2,2h34c1.1,0 2,-0.9 2,-2V25c0,-1.1 -0.9,-2 -2,-2"
android:strokeColor="#00000000" android:strokeWidth="1"/>
</vector>

View File

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

View File

@ -0,0 +1,9 @@
<vector android:height="24dp" android:viewportHeight="47.0"
android:viewportWidth="47.0" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#ffffff"
android:pathData="M23.9,10.2l-11.4,1.64l8.25,7.94l-1.95,11.22l10.2,-5.3l10.2,5.3l-1.95,-11.22l8.25,-7.94l-11.4,-1.64l-5.1,-10.2z"
android:strokeColor="#00000000" android:strokeWidth="1"/>
<path android:fillColor="#ffffff"
android:pathData="m17.07,35.44c-0.58,0 -1.15,-0.18 -1.64,-0.53 -0.86,-0.62 -1.29,-1.68 -1.11,-2.72L16.06,22.07 15.04,21.09L4.21,21.09c-1.5,0 -2.71,1.21 -2.71,2.71L1.5,44.29c0,1.5 1.21,2.71 2.71,2.71L24.7,47c1.5,0 2.71,-1.21 2.71,-2.71L27.41,30.36l-9.05,4.76c-0.41,0.21 -0.85,0.32 -1.3,0.32"
android:strokeColor="#00000000" android:strokeWidth="1"/>
</vector>

View File

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

View File

@ -1,12 +1,18 @@
<vector android:height="24dp" android:viewportHeight="64.0"
android:viewportWidth="64.0" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<vector android:height="24dp" android:viewportHeight="48.0"
android:viewportWidth="48.0" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#ffffff"
android:pathData="m22.75,12.78c-6.19,0 -11.22,5.03 -11.22,11.22 0,0.55 0.05,1.08 0.13,1.61 1.71,0.27 3.29,0.93 4.64,1.91 -0.57,-1.05 -0.9,-2.24 -0.9,-3.52 0,-4.05 3.3,-7.35 7.35,-7.35 4.05,0 7.35,3.3 7.35,7.35 0,4.05 -3.3,7.35 -7.35,7.35 -1.38,0 -2.66,-0.39 -3.77,-1.05 0.92,1.39 1.53,2.99 1.73,4.73 0.66,0.12 1.34,0.2 2.04,0.2 6.19,0 11.22,-5.03 11.22,-11.22 0,-6.19 -5.03,-11.22 -11.22,-11.22"
android:strokeColor="#00000000" android:strokeWidth="1"/>
<path android:fillColor="#ffffff"
android:pathData="m42.28,25.4c-0.36,0 -0.71,-0.02 -1.06,-0.06 -0.69,9.6 -8.71,17.2 -18.48,17.2 -1.28,0 -2.53,-0.13 -3.74,-0.38 -0.83,1.26 -1.91,2.33 -3.18,3.15 2.18,0.71 4.5,1.1 6.91,1.1 12.03,0 21.87,-9.54 22.38,-21.45 -0.9,0.29 -1.85,0.44 -2.84,0.44"
android:strokeColor="#00000000" android:strokeWidth="1"/>
<path android:fillColor="#ffffff"
android:pathData="m42.28,10.64c-2.96,0 -5.36,2.41 -5.36,5.36 0,2.96 2.41,5.36 5.36,5.36 2.96,0 5.36,-2.41 5.36,-5.36 0,-2.96 -2.41,-5.36 -5.36,-5.36"
android:strokeColor="#00000000" android:strokeWidth="1"/>
<path android:fillColor="#ffffff"
android:pathData="m10,29.5c-3.72,0 -6.75,3.03 -6.75,6.75 0,3.72 3.03,6.75 6.75,6.75 3.72,0 6.75,-3.03 6.75,-6.75 0,-3.72 -3.03,-6.75 -6.75,-6.75"
android:strokeColor="#00000000" android:strokeWidth="1"/>
<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"/>
android:pathData="m4.47,27.01c-0.16,-0.98 -0.25,-1.98 -0.25,-3.01 0,-10.22 8.31,-18.53 18.53,-18.53 4.68,0 8.97,1.75 12.23,4.63C35.81,9.08 36.85,8.24 38.03,7.64 34.02,3.9 28.65,1.6 22.75,1.6 10.4,1.6 0.35,11.65 0.35,24c0,2.1 0.3,4.13 0.84,6.05 0.87,-1.23 1.99,-2.27 3.28,-3.04"
android:strokeColor="#00000000" android:strokeWidth="1"/>
</vector>

View File

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

View File

@ -1,6 +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 android:height="24dp" android:viewportHeight="40.0"
android:viewportWidth="39.0" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#ffffff"
android:pathData="m34.32,21.96c0.08,-0.64 0.14,-1.28 0.14,-1.96 0,-0.68 -0.06,-1.32 -0.14,-1.96l4.22,-3.3c0.38,-0.3 0.48,-0.84 0.24,-1.28l-4,-6.92c-0.24,-0.44 -0.78,-0.6 -1.22,-0.44l-4.98,2c-1.04,-0.8 -2.16,-1.46 -3.38,-1.96l-0.76,-5.3c-0.06,-0.48 -0.48,-0.84 -0.98,-0.84h-8c-0.5,0 -0.92,0.36 -0.98,0.84l-0.76,5.3c-1.22,0.5 -2.34,1.18 -3.38,1.96l-4.98,-2C4.9,5.92 4.38,6.1 4.14,6.54l-4,6.92c-0.26,0.44 -0.14,0.98 0.24,1.28l4.22,3.3c-0.08,0.64 -0.14,1.3 -0.14,1.96 0,0.66 0.06,1.32 0.14,1.96l-4.22,3.3c-0.38,0.3 -0.48,0.84 -0.24,1.28l4,6.92c0.24,0.44 0.78,0.6 1.22,0.44l4.98,-2c1.04,0.8 2.16,1.46 3.38,1.96l0.76,5.3c0.06,0.48 0.48,0.84 0.98,0.84h8c0.5,0 0.92,-0.36 0.98,-0.84l0.76,-5.3c1.22,-0.5 2.34,-1.18 3.38,-1.96l4.98,2c0.46,0.18 0.98,0 1.22,-0.44l4,-6.92c0.24,-0.44 0.14,-0.98 -0.24,-1.28zM19.46,27c-3.86,0 -7,-3.14 -7,-7 0,-3.86 3.14,-7 7,-7 3.86,0 7,3.14 7,7 0,3.86 -3.14,7 -7,7z"
/>
</vector>

View File

@ -0,0 +1,6 @@
<vector android:height="24dp" android:viewportHeight="42.0"
android:viewportWidth="42.0" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#ffffff"
android:pathData="m21,42c2.37,0 4.31,-1.94 4.31,-4.31H16.69C16.69,40.06 18.61,42 21,42ZM33.92,29.08V18.31C33.92,11.7 30.39,6.16 24.23,4.7V3.23C24.23,1.44 22.79,0 21,0 19.21,0 17.77,1.44 17.77,3.23V4.7C11.59,6.16 8.08,11.67 8.08,18.31v10.77l-4.31,4.31v2.15H38.23v-2.15z"
android:strokeColor="#00000000" android:strokeWidth="1"/>
</vector>

View File

@ -6,22 +6,19 @@
android:layout_width="match_parent"
android:layout_height="match_parent">
<android.support.design.widget.BottomNavigationView
<com.ashokvarma.bottomnavigation.BottomNavigationBar
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:layout_alignParentBottom="true"
android:layout_width="match_parent"
android:layout_height="wrap_content" />
<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" />
android:layout_height="match_parent"
tools:listitem="@layout/main_tab_whats_new" />
</RelativeLayout>

View File

@ -114,6 +114,7 @@
android:layout_toRightOf="@id/icon"
android:layout_alignParentEnd="true"
android:layout_alignParentRight="true"
android:clipToPadding="false"
android:visibility="gone"
>
@ -125,7 +126,8 @@
android:layout_weight="1"
android:maxLines="1"
android:ellipsize="marquee"
android:text="THIS IS BUTTON 1" />
android:padding="12dp"
tools:text="THIS IS BUTTON 1" />
<Button
android:id="@+id/primaryButtonView"
@ -137,7 +139,8 @@
android:layout_weight="1"
android:maxLines="1"
android:ellipsize="marquee"
android:text="THIS IS 2" />
android:padding="12dp"
tools:text="THIS IS 2" />
</LinearLayout>
</RelativeLayout>

View File

@ -24,7 +24,6 @@
<Button
android:id="@+id/button"
tools:text="View all 10"
style="@style/DetailsSecondaryButtonStyle"
android:layout_width="wrap_content"
android:layout_height="0dp"
app:layout_constraintTop_toTopOf="parent"

View File

@ -8,11 +8,11 @@
<android.support.v7.widget.Toolbar
android:id="@+id/toolbar"
android:layout_width="match_parent"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:theme="?attr/actionBarTheme"
app:popupTheme="?attr/actionBarPopupTheme" />
@ -22,8 +22,8 @@
android:layout_height="0dp"
tools:listitem="@layout/installed_app_list_item"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintLeft_toLeftOf="parent"
app:layout_constraintRight_toRightOf="parent"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@+id/toolbar"
android:scrollbars="vertical" />

View File

@ -1,7 +1,6 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
<FrameLayout
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">
@ -11,9 +10,6 @@
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" />
android:scrollbars="vertical" />
</LinearLayout>
</FrameLayout>

View File

@ -1,33 +0,0 @@
<?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"
style="@style/DetailsSecondaryButtonStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentRight="true"
android:layout_alignParentEnd="true" />
<TextView
android:id="@+id/updates_heading"
tools:text="2 Updates"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_alignParentLeft="true"
android:layout_alignParentStart="true"
android:layout_alignBaseline="@+id/update_all_button" />
</RelativeLayout>

View File

@ -0,0 +1,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="wrap_content"
android:paddingStart="24dp"
android:paddingLeft="24dp"
tools:ignore="RtlSymmetry">
<!-- 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 (via the @+id/app_name). -->
<ImageView
android:id="@+id/icon"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
android:layout_width="32dp"
android:layout_height="32dp"
tools:src="@drawable/ic_launcher"
android:scaleType="fitCenter"
android:layout_marginStart="16dp"
android:layout_marginLeft="16dp"
android:layout_marginTop="8dp"
android:layout_marginBottom="8dp"
tools:ignore="ContentDescription" />
<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="16sp"
android:textColor="#424242"
android:lines="1"
android:ellipsize="end"
android:layout_marginStart="8dp"
android:layout_marginLeft="8dp"
android:layout_marginEnd="8dp"
android:layout_marginRight="8dp"
app:layout_constraintStart_toEndOf="@+id/icon"
app:layout_constraintEnd_toStartOf="@+id/install"
app:layout_constraintTop_toTopOf="@+id/icon"
app:layout_constraintBottom_toBottomOf="@+id/icon" />
<ImageView
android:id="@+id/install"
tools:src="@drawable/ic_download"
android:scaleType="fitXY"
android:contentDescription="@string/updates__tts__download_app"
android:layout_width="40dp"
android:layout_height="40dp"
android:elevation="2dp"
android:layout_marginEnd="16dp"
android:layout_marginRight="16dp"
android:layout_marginTop="8dp"
android:layout_marginBottom="8dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@+id/icon"
app:layout_constraintBottom_toBottomOf="@+id/icon" />
</android.support.constraint.ConstraintLayout>

View File

@ -0,0 +1,87 @@
<?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">
<!-- 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"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
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"
tools:ignore="ContentDescription" />
<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="16sp"
android:textColor="#424242"
android:maxLines="2"
android:ellipsize="end"
app:layout_constraintStart_toEndOf="@+id/icon"
app:layout_constraintEnd_toStartOf="@+id/action_button"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintBottom_toBottomOf="parent"
android:layout_marginBottom="8dp"
android:layout_marginStart="8dp"
android:layout_marginEnd="8dp"
android:layout_marginTop="8dp"
app:layout_constraintVertical_bias="0.333" />
<ProgressBar
android:id="@+id/progress_bar"
android:layout_width="0dp"
android:layout_height="wrap_content"
style="@style/Widget.AppCompat.ProgressBar.Horizontal"
android:layout_marginStart="8dp"
android:layout_marginLeft="8dp"
android:layout_marginRight="8dp"
android:layout_marginEnd="8dp"
app:layout_constraintTop_toBottomOf="@+id/app_name"
app:layout_constraintStart_toEndOf="@+id/icon"
app:layout_constraintEnd_toStartOf="@+id/cancel_button"
android:visibility="gone"
tools:visibility="visible" />
<ImageButton
android:id="@+id/cancel_button"
android:contentDescription="@string/cancel"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:srcCompat="@drawable/ic_cancel"
android:background="@android:color/transparent"
android:layout_marginEnd="16dp"
android:layout_marginRight="16dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@+id/progress_bar"
app:layout_constraintBottom_toBottomOf="@+id/progress_bar"
android:visibility="gone"
tools:visibility="visible" />
<Button
android:id="@+id/action_button"
style="@style/DetailsPrimaryButtonStyle"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginEnd="16dp"
android:layout_marginRight="16dp"
android:layout_marginTop="8dp"
android:layout_marginBottom="8dp"
app:layout_constraintBottom_toBottomOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="parent"
tools:text="Update" />
</android.support.constraint.ConstraintLayout>

View File

@ -0,0 +1,63 @@
<?xml version="1.0" encoding="utf-8"?>
<android.support.constraint.ConstraintLayout
xmlns:android="http://schemas.android.com/apk/res/android"
android:layout_width="match_parent"
android:layout_height="wrap_content"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto">
<TextView
android:id="@+id/text_updates_available"
android:layout_width="0dp"
android:layout_height="wrap_content"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toTopOf="parent"
app:layout_constraintEnd_toStartOf="@+id/button_download_all"
android:layout_marginTop="8dp"
android:layout_marginStart="16dp"
android:layout_marginLeft="16dp"
android:layout_marginEnd="8dp"
android:layout_marginRight="8dp"
tools:text="Download updates for 3 apps" />
<ImageView
android:id="@+id/button_download_all"
android:layout_width="48dp"
android:layout_height="48dp"
app:srcCompat="@drawable/ic_download_progress_0"
android:layout_marginEnd="8dp"
android:layout_marginRight="8dp"
android:contentDescription="@string/updates__tts__download_updates_for_all_apps"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@+id/text_updates_available" />
<TextView
android:id="@+id/text_apps_to_update"
android:layout_width="0dp"
android:layout_height="wrap_content"
tools:text="SAnd, Birthday Droid, Dados D, Other app, Another app"
android:maxLines="1"
android:ellipsize="end"
android:layout_marginStart="16dp"
android:layout_marginLeft="16dp"
android:layout_marginEnd="8dp"
android:layout_marginRight="8dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toStartOf="@+id/button_download_all"
app:layout_constraintTop_toBottomOf="@+id/text_updates_available" />
<Button
android:id="@+id/button_toggle_apps_to_update"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintTop_toBottomOf="@+id/text_apps_to_update"
android:background="?attr/selectableItemBackground"
android:textColor="@color/fdroid_blue"
android:padding="0dp"
android:layout_marginStart="16dp"
android:layout_marginLeft="16dp"
tools:text="Show apps"
android:textAllCaps="true" />
</android.support.constraint.ConstraintLayout>

View File

@ -3,12 +3,12 @@
xmlns:app="http://schemas.android.com/apk/res-auto">
<item
android:title="@string/main_menu__latest_apps"
android:icon="@drawable/ic_overview"
android:icon="@drawable/ic_latest"
app:showAsAction="ifRoom|withText"
android:id="@+id/whats_new" />
<item
android:title="@string/main_menu__categories"
android:icon="@drawable/ic_category"
android:icon="@drawable/ic_categories"
app:showAsAction="ifRoom|withText"
android:id="@+id/categories" />
<item
@ -17,10 +17,10 @@
app:showAsAction="ifRoom|withText"
android:id="@+id/nearby" />
<item
android:title="@string/preference_category__my_apps"
android:icon="@drawable/ic_my_apps"
android:title="@string/updates"
android:icon="@drawable/ic_updates"
app:showAsAction="ifRoom|withText"
android:id="@+id/my_apps" />
android:id="@+id/updates" />
<item
android:title="@string/menu_settings"
android:icon="@drawable/ic_settings"

View File

@ -29,4 +29,6 @@
<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>
<dimen name="badge_size">18dp</dimen>
</resources>

View File

@ -8,7 +8,4 @@
<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>

View File

@ -67,17 +67,31 @@
<string name="app_version_x_installed">Version %1$s</string>
<string name="app_recommended_version_installed">Version %1$s (Recommended)</string>
<string name="app__newly_added">New</string>
<string name="added_on">Added on %s</string>
<string name="app__install_downloaded_update">Update</string>
<string name="app_list__name__downloaded_and_ready_to_update">Update %1$s</string>
<string name="app_list__name__downloaded_and_ready_to_install">Install %1$s</string>
<string name="app_list__name__downloading_in_progress">Downloading %1$s</string>
<plurals name="app_list__age__released_x_days_ago">
<item quantity="one">Released %1$d day ago</item>
<item quantity="other">Released %1$d days ago</item>
</plurals>
<string name="installed_apps__activity_title">Installed Apps</string>
<string name="installed_app__updates_ignored">Updates ignored</string>
<string name="installed_app__updates_ignored_for_suggested_version">Updates ignored for Version %1$s</string>
<!-- The inline download button shown in the "Updates" screen only uses an icon and so requires
some descriptive text for the TTS engine -->
<string name="updates__tts__download_app">Download</string>
<string name="updates__tts__download_updates_for_all_apps">Download all updates</string>
<string name="added_on">Added on %s</string>
<string name="updates__hide_updateable_apps">Hide apps</string>
<string name="updates__show_updateable_apps">Show apps</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 name="updates__download_updates_for_apps">
<item quantity="one">Download update for %1$d app.</item>
<item quantity="other">Download updates for %1$d apps.</item>
</plurals>
<string name="ok">OK</string>

View File

@ -14,11 +14,17 @@
<style name="DetailsPrimaryButtonStyle" parent="DetailsButtonStyle">
<item name="android:textColor">#ffffff</item>
<item name="android:background">@drawable/button_primary_background_selector</item>
<item name="android:padding">8dp</item>
<item name="android:minHeight">32dp</item>
<item name="android:minWidth">0dp</item>
</style>
<style name="DetailsSecondaryButtonStyle" parent="DetailsButtonStyle">
<item name="android:textColor">@color/fdroid_blue</item>
<item name="android:background">@drawable/button_secondary_background_selector</item>
<item name="android:padding">8dp</item>
<item name="android:minHeight">32dp</item>
<item name="android:minWidth">0dp</item>
</style>
<style name="DetailsMoreButtonStyle">