Updates: Implemented new UI for "Updates" screen.

Alows for more flexibility in what we are able to display, including:
 * Prompting users to donate to frequently updated apps
 * Showing messages from package maintainers to users
 * Marking apps for later installation when offline

Most of these are not yet implemented, but will be able to when
required, whereas they were not able to in the previous UI.
This commit is contained in:
Peter Serwylo 2017-03-14 08:32:56 +11:00
parent 7659c3a33f
commit 40cc328e98
21 changed files with 990 additions and 275 deletions

View File

@ -63,6 +63,13 @@ public final class AppUpdateStatusManager {
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.
@ -174,9 +181,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);
}
}
@ -220,7 +229,7 @@ public final class AppUpdateStatusManager {
for (Apk apk : apksToUpdate) {
addApk(apk, status, null);
}
endBatchUpdates();
endBatchUpdates(status);
}
/**
@ -318,10 +327,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);
}
}
@ -333,7 +349,7 @@ public final class AppUpdateStatusManager {
it.remove();
}
}
notifyChange();
notifyChange(REASON_CLEAR_ALL_UPDATES);
}
}
@ -345,7 +361,7 @@ public final class AppUpdateStatusManager {
it.remove();
}
}
notifyChange();
notifyChange(REASON_CLEAR_ALL_INSTALLED);
}
}

View File

@ -10,7 +10,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;
/**
@ -38,10 +38,10 @@ class MainViewController extends RecyclerView.ViewHolder {
}
/**
* @see MyAppsViewBinder
* @see UpdatesViewBinder
*/
public void bindUpdates() {
new MyAppsViewBinder(activity, frame);
new UpdatesViewBinder(activity, frame);
}
/**

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,355 @@
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.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());
} 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);
}
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,26 @@
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 {
public UpdatesViewBinder(AppCompatActivity activity, FrameLayout parent) {
View view = activity.getLayoutInflater().inflate(R.layout.main_tab_updates, parent, true);
UpdatesAdapter adapter = new UpdatesAdapter(activity);
// TODO: Find the right time to stop listening for status updates.
adapter.listenForStatusUpdates();
RecyclerView list = (RecyclerView) view.findViewById(R.id.list);
list.setHasFixedSize(true);
list.setLayoutManager(new LinearLayoutManager(activity));
list.setAdapter(adapter);
}
}

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

@ -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="18sp"
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="@+id/icon"
app:layout_constraintBottom_toBottomOf="@+id/icon"
android:layout_marginBottom="8dp"
android:layout_marginLeft="8dp"
android:layout_marginStart="8dp"
android:layout_marginRight="8dp"
android:layout_marginEnd="8dp"
tools:layout_editor_absoluteX="72dp" />
<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"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
android:layout_marginEnd="16dp"
android:layout_marginRight="16dp"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@+id/icon"
app:layout_constraintBottom_toBottomOf="@+id/icon"
style="@style/DetailsPrimaryButtonStyle"
tools:text="Update" />
</android.support.constraint.ConstraintLayout>

View File

@ -0,0 +1,61 @@
<?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"
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="@android:color/transparent"
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

@ -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,22 @@
<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="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>