Merge branch 'improve-updates-tab-stability' into 'master'

Refactor `AppListItemController` to improve updates tab stability

See merge request !549
This commit is contained in:
Hans-Christoph Steiner 2017-07-06 22:15:41 +00:00
commit 8f680bc1aa
17 changed files with 575 additions and 375 deletions

View File

@ -7,7 +7,7 @@ import android.view.ViewGroup;
import org.fdroid.fdroid.R;
import org.fdroid.fdroid.data.App;
class AppListAdapter extends RecyclerView.Adapter<AppListItemController> {
class AppListAdapter extends RecyclerView.Adapter<StandardAppListItemController> {
private Cursor cursor;
private final Activity activity;
@ -24,13 +24,13 @@ class AppListAdapter extends RecyclerView.Adapter<AppListItemController> {
}
@Override
public AppListItemController onCreateViewHolder(ViewGroup parent, int viewType) {
return new AppListItemController(activity, activity.getLayoutInflater()
public StandardAppListItemController onCreateViewHolder(ViewGroup parent, int viewType) {
return new StandardAppListItemController(activity, activity.getLayoutInflater()
.inflate(R.layout.app_list_item, parent, false));
}
@Override
public void onBindViewHolder(AppListItemController holder, int position) {
public void onBindViewHolder(StandardAppListItemController holder, int position) {
cursor.moveToPosition(position);
holder.bindModel(new App(cursor));
}

View File

@ -37,7 +37,6 @@ import org.fdroid.fdroid.Utils;
import org.fdroid.fdroid.data.Apk;
import org.fdroid.fdroid.data.ApkProvider;
import org.fdroid.fdroid.data.App;
import org.fdroid.fdroid.data.AppPrefs;
import org.fdroid.fdroid.installer.ApkCache;
import org.fdroid.fdroid.installer.InstallManagerService;
import org.fdroid.fdroid.installer.Installer;
@ -46,13 +45,25 @@ import org.fdroid.fdroid.installer.InstallerFactory;
import java.io.File;
import java.util.Iterator;
// TODO: Support cancelling of downloads by tapping the install button a second time.
@SuppressWarnings("LineLength")
public class AppListItemController extends RecyclerView.ViewHolder {
/**
* Supports the following layouts:
* <ul>
* <li>app_list_item (see {@link StandardAppListItemController}</li>
* <li>updateable_app_list_status_item (see
* {@link org.fdroid.fdroid.views.updates.items.AppStatusListItemController}</li>
* <li>updateable_app_list_item (see
* {@link org.fdroid.fdroid.views.updates.items.UpdateableAppListItemController}</li>
* <li>installed_app_list_item (see {@link StandardAppListItemController}</li>
* </ul>
*
* The state of the UI is defined in a dumb {@link AppListItemState} class, then applied to the UI
* in the {@link #refreshView(App, AppUpdateStatusManager.AppUpdateStatus)} method.
*/
public abstract class AppListItemController extends RecyclerView.ViewHolder {
private static final String TAG = "AppListItemController";
private final Activity activity;
protected final Activity activity;
@NonNull
private final ImageView icon;
@ -67,13 +78,7 @@ public class AppListItemController extends RecyclerView.ViewHolder {
private final TextView status;
@Nullable
private final TextView downloadReady;
@Nullable
private final TextView installedVersion;
@Nullable
private final TextView ignoredStatus;
private final TextView secondaryStatus;
@Nullable
private final ProgressBar progressBar;
@ -111,13 +116,15 @@ public class AppListItemController extends RecyclerView.ViewHolder {
public void getOutline(View view, Outline outline) {
float density = activity.getResources().getDisplayMetrics().density;
// TODO: This is a bit hacky/hardcoded/too-specific to the particular icons we're using.
// This is a bit hacky/hardcoded/too-specific to the particular icons we're using.
// This is because the default "download & install" and "downloaded & ready to install"
// icons are smaller than the "downloading progress" button. Hence, we can't just use
// the width/height of the view to calculate the outline size.
int xPadding = (int) (8 * density);
int yPadding = (int) (9 * density);
outline.setOval(xPadding, yPadding, installButton.getWidth() - xPadding, installButton.getHeight() - yPadding);
int right = installButton.getWidth() - xPadding;
int bottom = installButton.getHeight() - yPadding;
outline.setOval(xPadding, yPadding, right, bottom);
}
});
}
@ -126,9 +133,7 @@ public class AppListItemController extends RecyclerView.ViewHolder {
icon = (ImageView) itemView.findViewById(R.id.icon);
name = (TextView) itemView.findViewById(R.id.app_name);
status = (TextView) itemView.findViewById(R.id.status);
downloadReady = (TextView) itemView.findViewById(R.id.download_ready);
installedVersion = (TextView) itemView.findViewById(R.id.installed_version);
ignoredStatus = (TextView) itemView.findViewById(R.id.ignored_status);
secondaryStatus = (TextView) itemView.findViewById(R.id.secondary_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);
@ -146,247 +151,197 @@ public class AppListItemController extends RecyclerView.ViewHolder {
itemView.setOnClickListener(onAppClicked);
}
/**
* Figures out the current install/update/download/etc status for the app we are viewing.
* Then, asks the view to update itself to reflect this status.
*/
private void refreshStatus(@NonNull App app) {
Iterator<AppUpdateStatusManager.AppUpdateStatus> statuses = AppUpdateStatusManager.getInstance(activity).getByPackageName(app.packageName).iterator();
if (statuses.hasNext()) {
AppUpdateStatusManager.AppUpdateStatus status = statuses.next();
updateAppStatus(app, status);
} else {
currentStatus = null;
}
}
public void bindModel(@NonNull App app) {
currentApp = app;
ImageLoader.getInstance().displayImage(app.iconUrl, icon, displayImageOptions);
refreshStatus(app);
// Figures out the current install/update/download/etc status for the app we are viewing.
// Then, asks the view to update itself to reflect this status.
Iterator<AppUpdateStatusManager.AppUpdateStatus> statuses =
AppUpdateStatusManager.getInstance(activity).getByPackageName(app.packageName).iterator();
if (statuses.hasNext()) {
AppUpdateStatusManager.AppUpdateStatus status = statuses.next();
updateAppStatus(app, status);
} else {
updateAppStatus(app, null);
}
final LocalBroadcastManager broadcastManager = LocalBroadcastManager.getInstance(activity.getApplicationContext());
broadcastManager.unregisterReceiver(onInstallAction);
final LocalBroadcastManager broadcastManager =
LocalBroadcastManager.getInstance(activity.getApplicationContext());
broadcastManager.unregisterReceiver(onStatusChanged);
// broadcastManager.registerReceiver(onInstallAction, Installer.getInstallIntentFilter(Uri.parse(currentAppDownloadUrl)));
IntentFilter intentFilter = new IntentFilter();
intentFilter.addAction(AppUpdateStatusManager.BROADCAST_APPSTATUS_ADDED);
intentFilter.addAction(AppUpdateStatusManager.BROADCAST_APPSTATUS_REMOVED);
intentFilter.addAction(AppUpdateStatusManager.BROADCAST_APPSTATUS_CHANGED);
broadcastManager.registerReceiver(onStatusChanged, intentFilter);
configureAppName(app);
configureStatusText(app);
configureInstalledVersion(app);
configureIgnoredStatus(app);
configureInstallButton(app);
configureActionButton(app);
}
/**
* Sets the text/visibility of the {@link R.id#status} {@link TextView} based on whether the app:
* * Is compatible with the users device
* * Is installed
* * Can be updated
* 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 void configureStatusText(@NonNull App app) {
if (status == null) {
return;
}
if (!app.compatible) {
status.setText(activity.getString(R.string.app_incompatible));
status.setVisibility(View.VISIBLE);
} else if (app.isInstalled()) {
if (app.canAndWantToUpdate(activity)) {
String upgradeFromTo = activity.getString(R.string.app_version_x_available, app.getSuggestedVersionName());
status.setText(upgradeFromTo);
} else {
String installed = activity.getString(R.string.app_version_x_installed, app.installedVersionName);
status.setText(installed);
}
status.setVisibility(View.VISIBLE);
} else {
status.setVisibility(View.INVISIBLE);
}
private void updateAppStatus(@NonNull App app, @Nullable AppUpdateStatusManager.AppUpdateStatus status) {
currentStatus = status;
refreshView(app, status);
}
/**
* Shows the currently installed version name, and whether or not it is the recommended version.
* Binds to the {@link R.id#installed_version} {@link TextView}.
* Queries the current state via {@link #getCurrentViewState(App, AppUpdateStatusManager.AppUpdateStatus)}
* and then updates the relevant widgets depending on that state.
*
* Should contain little to no business logic, this all belongs to
* {@link #getCurrentViewState(App, AppUpdateStatusManager.AppUpdateStatus)}.
*
* @see AppListItemState
* @see #getCurrentViewState(App, AppUpdateStatusManager.AppUpdateStatus)
*/
private void configureInstalledVersion(@NonNull App app) {
if (installedVersion == null) {
return;
}
private void refreshView(@NonNull App app,
@Nullable AppUpdateStatusManager.AppUpdateStatus appStatus) {
int res = (app.suggestedVersionCode == app.installedVersionCode)
? R.string.app_recommended_version_installed : R.string.app_version_x_installed;
AppListItemState viewState = getCurrentViewState(app, appStatus);
installedVersion.setText(activity.getString(res, app.installedVersionName));
}
name.setText(viewState.getMainText());
/**
* Shows whether the user has previously asked to ignore updates for this app entirely, or for a
* specific version of this app. Binds to the {@link R.id#ignored_status} {@link TextView}.
*/
private void configureIgnoredStatus(@NonNull App app) {
if (ignoredStatus == null) {
return;
}
AppPrefs prefs = app.getPrefs(activity);
if (prefs.ignoreAllUpdates) {
ignoredStatus.setText(activity.getString(R.string.installed_app__updates_ignored));
ignoredStatus.setVisibility(View.VISIBLE);
} else if (prefs.ignoreThisUpdate > 0 && prefs.ignoreThisUpdate == app.suggestedVersionCode) {
ignoredStatus.setText(activity.getString(R.string.installed_app__updates_ignored_for_suggested_version, app.getSuggestedVersionName()));
ignoredStatus.setVisibility(View.VISIBLE);
} else {
ignoredStatus.setVisibility(View.GONE);
}
}
/**
* 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) {
return true;
}
}
return false;
}
/**
* The app name {@link TextView} is used for a few reasons:
* <li> Display name + summary of the app (most common).
* <li> If downloading, mention that it is downloading instead of showing the summary.
* <li> If downloaded and ready to install, mention that it is ready to update/install.
*/
private void configureAppName(@NonNull App app) {
if (downloadReady != null) {
downloadReady.setVisibility(View.GONE);
}
if (currentStatus != null && currentStatus.status == AppUpdateStatusManager.Status.ReadyToInstall) {
name.setText(app.name);
if (downloadReady != null) {
downloadReady.setVisibility(View.VISIBLE);
}
} else if (currentStatus != null && currentStatus.status == AppUpdateStatusManager.Status.Downloading) {
name.setText(activity.getString(R.string.app_list__name__downloading_in_progress, app.name));
} else if (currentStatus != null && currentStatus.status == AppUpdateStatusManager.Status.Installed) {
name.setText(activity.getString(R.string.app_list__name__successfully_installed, 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;
}
actionButton.setVisibility(View.VISIBLE);
if (currentStatus != null && currentStatus.status == AppUpdateStatusManager.Status.Installed) {
if (activity.getPackageManager().getLaunchIntentForPackage(app.packageName) != null) {
actionButton.setText(R.string.menu_launch);
if (actionButton != null) {
if (viewState.shouldShowActionButton()) {
actionButton.setVisibility(View.VISIBLE);
actionButton.setText(viewState.getActionButtonText());
} else {
actionButton.setVisibility(View.GONE);
}
} else if (currentStatus != null && currentStatus.status == AppUpdateStatusManager.Status.ReadyToInstall) {
if (app.isInstalled()) {
actionButton.setText(R.string.app__install_downloaded_update);
}
if (progressBar != null) {
if (viewState.showProgress()) {
progressBar.setVisibility(View.VISIBLE);
if (viewState.isProgressIndeterminate()) {
progressBar.setIndeterminate(true);
} else {
progressBar.setIndeterminate(false);
progressBar.setMax(viewState.getProgressMax());
progressBar.setProgress(viewState.getProgressCurrent());
}
} else {
actionButton.setText(R.string.menu_install);
progressBar.setVisibility(View.GONE);
}
} else {
actionButton.setVisibility(View.GONE);
}
}
/**
* 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.
*/
private void configureInstallButton(@NonNull App app) {
if (installButton == null) {
return;
}
if (isReadyToInstall(app)) {
installButton.setVisibility(View.GONE);
} else {
boolean installable = app.canAndWantToUpdate(activity) || !app.isInstalled();
boolean shouldAllow = app.compatible && !app.isFiltered();
if (cancelButton != null) {
if (viewState.showProgress()) {
cancelButton.setVisibility(View.VISIBLE);
} else {
cancelButton.setVisibility(View.GONE);
}
}
if (shouldAllow && installable) {
installButton.setImageDrawable(ContextCompat.getDrawable(activity, R.drawable.ic_download));
if (installButton != null) {
if (viewState.shouldShowActionButton()) {
installButton.setVisibility(View.GONE);
} else if (viewState.showProgress()) {
installButton.setVisibility(View.VISIBLE);
installButton.setImageDrawable(ContextCompat.getDrawable(activity, R.drawable.ic_download_progress));
int progressAsDegrees = viewState.getProgressMax() <= 0 ? 0 :
(int) (((float) viewState.getProgressCurrent() / viewState.getProgressMax()) * 360);
installButton.setImageLevel(progressAsDegrees);
} else if (viewState.shouldShowInstall()) {
installButton.setVisibility(View.VISIBLE);
installButton.setImageDrawable(ContextCompat.getDrawable(activity, R.drawable.ic_download));
} else {
installButton.setVisibility(View.GONE);
}
}
}
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);
if (status != null) {
CharSequence statusText = viewState.getStatusText();
if (statusText == null) {
status.setVisibility(View.GONE);
} else {
progressBar.setIndeterminate(false);
progressBar.setMax(totalBytes);
progressBar.setProgress(bytesRead);
status.setVisibility(View.VISIBLE);
status.setText(statusText);
}
}
if (cancelButton != null) {
cancelButton.setVisibility(View.VISIBLE);
if (secondaryStatus != null) {
CharSequence statusText = viewState.getSecondaryStatusText();
if (statusText == null) {
secondaryStatus.setVisibility(View.GONE);
} else {
secondaryStatus.setVisibility(View.VISIBLE);
secondaryStatus.setText(statusText);
}
}
}
private void onDownloadComplete() {
if (installButton != null) {
installButton.setVisibility(View.GONE);
}
@NonNull
protected AppListItemState getCurrentViewState(
@NonNull App app, @Nullable AppUpdateStatusManager.AppUpdateStatus appStatus) {
if (appStatus == null) {
return getViewStateDefault(app);
} else {
switch (appStatus.status) {
case ReadyToInstall:
return getViewStateReadyToInstall(app);
if (progressBar != null) {
progressBar.setVisibility(View.GONE);
}
case Downloading:
return getViewStateDownloading(app, appStatus);
if (cancelButton != null) {
cancelButton.setVisibility(View.GONE);
}
case Installed:
return getViewStateInstalled(app);
if (currentApp != null) {
configureActionButton(currentApp);
default:
return getViewStateDefault(app);
}
}
}
protected AppListItemState getViewStateInstalled(@NonNull App app) {
CharSequence mainText = activity.getString(
R.string.app_list__name__successfully_installed, app.name);
AppListItemState state = new AppListItemState(app)
.setMainText(mainText)
.setStatusText(activity.getString(R.string.notification_content_single_installed));
if (activity.getPackageManager().getLaunchIntentForPackage(app.packageName) != null) {
state.showActionButton(activity.getString(R.string.menu_launch));
}
return state;
}
protected AppListItemState getViewStateDownloading(
@NonNull App app, @NonNull AppUpdateStatusManager.AppUpdateStatus currentStatus) {
CharSequence mainText = activity.getString(
R.string.app_list__name__downloading_in_progress, app.name);
return new AppListItemState(app)
.setMainText(mainText)
.setProgress(currentStatus.progressCurrent, currentStatus.progressMax);
}
protected AppListItemState getViewStateReadyToInstall(@NonNull App app) {
int actionButtonLabel = app.isInstalled()
? R.string.app__install_downloaded_update
: R.string.menu_install;
return new AppListItemState(app)
.setMainText(app.name)
.showActionButton(activity.getString(actionButtonLabel))
.setStatusText(activity.getString(R.string.app_list_download_ready));
}
protected AppListItemState getViewStateDefault(@NonNull App app) {
return new AppListItemState(app);
}
/* =================================================================
* Various listeners for each different click/broadcast that we need
* to respond to.
* =================================================================
*/
@SuppressWarnings("FieldCanBeLocal")
private final View.OnClickListener onAppClicked = new View.OnClickListener() {
@Override
@ -398,8 +353,10 @@ public class AppListItemController extends RecyclerView.ViewHolder {
Intent intent = new Intent(activity, AppDetails2.class);
intent.putExtra(AppDetails2.EXTRA_APPID, currentApp.packageName);
if (Build.VERSION.SDK_INT >= 21) {
Pair<View, String> iconTransitionPair = Pair.create((View) icon, activity.getString(R.string.transition_app_item_icon));
Bundle bundle = ActivityOptionsCompat.makeSceneTransitionAnimation(activity, iconTransitionPair).toBundle();
String transitionAppIcon = activity.getString(R.string.transition_app_item_icon);
Pair<View, String> iconTransitionPair = Pair.create((View) icon, transitionAppIcon);
Bundle bundle = ActivityOptionsCompat
.makeSceneTransitionAnimation(activity, iconTransitionPair).toBundle();
activity.startActivity(intent, bundle);
} else {
activity.startActivity(intent);
@ -407,41 +364,15 @@ 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 void updateAppStatus(@NonNull App app, @NonNull AppUpdateStatusManager.AppUpdateStatus status) {
currentStatus = status;
configureAppName(app);
configureActionButton(app);
switch (status.status) {
case Downloading:
onDownloadProgressUpdated(status.progressCurrent, status.progressMax);
break;
case ReadyToInstall:
onDownloadComplete();
break;
case Installed:
case Installing:
case InstallError:
case UpdateAvailable:
case DownloadInterrupted:
break;
}
}
private final BroadcastReceiver onStatusChanged = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
AppUpdateStatusManager.AppUpdateStatus newStatus = intent.getParcelableExtra(AppUpdateStatusManager.EXTRA_STATUS);
AppUpdateStatusManager.AppUpdateStatus newStatus =
intent.getParcelableExtra(AppUpdateStatusManager.EXTRA_STATUS);
if (currentApp == null || !TextUtils.equals(newStatus.app.packageName, currentApp.packageName) || (installButton == null && progressBar == null)) {
if (currentApp == null
|| !TextUtils.equals(newStatus.app.packageName, currentApp.packageName)
|| (installButton == null && progressBar == null)) {
return;
}
@ -449,34 +380,6 @@ public class AppListItemController extends RecyclerView.ViewHolder {
}
};
private final BroadcastReceiver onInstallAction = new BroadcastReceiver() {
@Override
public void onReceive(Context context, Intent intent) {
Apk apk = intent.getParcelableExtra(Installer.EXTRA_APK);
if (currentApp == null || !TextUtils.equals(apk.packageName, currentApp.packageName)) {
return;
}
configureAppName(currentApp);
configureActionButton(currentApp);
if (installButton == null) {
return;
}
if (Installer.ACTION_INSTALL_STARTED.equals(intent.getAction())) {
installButton.setImageDrawable(ContextCompat.getDrawable(activity, R.drawable.ic_download_progress));
installButton.setImageLevel(0);
} else if (Installer.ACTION_INSTALL_COMPLETE.equals(intent.getAction())) {
installButton.setVisibility(View.GONE);
// TODO: It could've been a different version other than the current suggested version.
// In these cases, don't hide the button but rather set it back to the default install image.
} else if (Installer.ACTION_INSTALL_INTERRUPTED.equals(intent.getAction())) {
installButton.setImageDrawable(ContextCompat.getDrawable(activity, R.drawable.ic_download));
}
}
};
@SuppressWarnings("FieldCanBeLocal")
private final View.OnClickListener onActionClicked = new View.OnClickListener() {
@Override
@ -500,8 +403,10 @@ public class AppListItemController extends RecyclerView.ViewHolder {
}
if (currentStatus != null && currentStatus.status == AppUpdateStatusManager.Status.ReadyToInstall) {
File apkFilePath = ApkCache.getApkDownloadPath(activity, Uri.parse(currentStatus.apk.getUrl()));
Utils.debugLog(TAG, "skip download, we have already downloaded " + currentStatus.apk.getUrl() + " to " + apkFilePath);
Uri apkDownloadUri = Uri.parse(currentStatus.apk.getUrl());
File apkFilePath = ApkCache.getApkDownloadPath(activity, apkDownloadUri);
Utils.debugLog(TAG, "skip download, we have already downloaded " + currentStatus.apk.getUrl() +
" to " + apkFilePath);
// TODO: This seems like a bit of a hack. Is there a better way to do this by changing
// the Installer API so that we can ask it to install without having to get it to fire
@ -513,7 +418,8 @@ public class AppListItemController extends RecyclerView.ViewHolder {
broadcastManager.unregisterReceiver(this);
if (Installer.ACTION_INSTALL_USER_INTERACTION.equals(intent.getAction())) {
PendingIntent pendingIntent = intent.getParcelableExtra(Installer.EXTRA_USER_INTERACTION_PI);
PendingIntent pendingIntent =
intent.getParcelableExtra(Installer.EXTRA_USER_INTERACTION_PI);
try {
pendingIntent.send();
} catch (PendingIntent.CanceledException ignored) { }
@ -521,9 +427,9 @@ public class AppListItemController extends RecyclerView.ViewHolder {
}
};
broadcastManager.registerReceiver(receiver, Installer.getInstallIntentFilter(Uri.parse(currentStatus.apk.getUrl())));
broadcastManager.registerReceiver(receiver, Installer.getInstallIntentFilter(apkDownloadUri));
Installer installer = InstallerFactory.create(activity, currentStatus.apk);
installer.installPackage(Uri.parse(apkFilePath.toURI().toString()), Uri.parse(currentStatus.apk.getUrl()));
installer.installPackage(Uri.parse(apkFilePath.toURI().toString()), apkDownloadUri);
} else {
final Apk suggestedApk = ApkProvider.Helper.findSuggestedApk(activity, currentApp);
InstallManagerService.queue(activity, currentApp, suggestedApk);

View File

@ -0,0 +1,102 @@
package org.fdroid.fdroid.views.apps;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import org.fdroid.fdroid.Utils;
import org.fdroid.fdroid.data.App;
/**
* A dumb model which is used to specify what should/should not be shown in an {@link AppListItemController}.
* @see AppListItemController and its subclasses.
*/
public class AppListItemState {
private final App app;
private CharSequence mainText = null;
private CharSequence actionButtonText = null;
private CharSequence statusText = null;
private CharSequence secondaryStatusText = null;
private int progressCurrent = -1;
private int progressMax = -1;
private boolean showInstallButton;
public AppListItemState(@NonNull App app) {
this.app = app;
}
public AppListItemState setMainText(CharSequence mainText) {
this.mainText = mainText;
return this;
}
public AppListItemState showActionButton(CharSequence label) {
actionButtonText = label;
return this;
}
public AppListItemState setStatusText(CharSequence text) {
this.statusText = text;
return this;
}
public AppListItemState setSecondaryStatusText(CharSequence text) {
this.secondaryStatusText = text;
return this;
}
public AppListItemState setProgress(int progressCurrent, int progressMax) {
this.progressCurrent = progressCurrent;
this.progressMax = progressMax;
return this;
}
public AppListItemState setShowInstallButton(boolean show) {
this.showInstallButton = show;
return this;
}
@Nullable
public CharSequence getMainText() {
return mainText != null
? mainText
: Utils.formatAppNameAndSummary(app.name, app.summary);
}
public boolean shouldShowInstall() {
return showInstallButton;
}
public boolean shouldShowActionButton() {
return actionButtonText != null;
}
public CharSequence getActionButtonText() {
return actionButtonText;
}
public boolean showProgress() {
return progressCurrent >= 0;
}
public boolean isProgressIndeterminate() {
return progressMax <= 0;
}
public int getProgressCurrent() {
return progressCurrent;
}
public int getProgressMax() {
return progressMax;
}
@Nullable
public CharSequence getStatusText() {
return statusText;
}
@Nullable
public CharSequence getSecondaryStatusText() {
return secondaryStatusText;
}
}

View File

@ -0,0 +1,55 @@
package org.fdroid.fdroid.views.apps;
import android.app.Activity;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.view.View;
import org.fdroid.fdroid.AppUpdateStatusManager;
import org.fdroid.fdroid.R;
import org.fdroid.fdroid.data.App;
/**
* Used for search results or for category lists.
* Shows an inline download button, and also (if appropriate):
* * Whether the app is incompatible.
* * Version that app can be upgraded to.
* * Installed version.
*/
public class StandardAppListItemController extends AppListItemController {
public StandardAppListItemController(Activity activity, View itemView) {
super(activity, itemView);
}
@NonNull
@Override
protected AppListItemState getCurrentViewState(
@NonNull App app, @Nullable AppUpdateStatusManager.AppUpdateStatus appStatus) {
return super.getCurrentViewState(app, appStatus)
.setStatusText(getStatusText(app))
.setShowInstallButton(shouldShowInstall(app));
}
@Nullable
private CharSequence getStatusText(@NonNull App app) {
if (!app.compatible) {
return activity.getString(R.string.app_incompatible);
} else if (app.isInstalled()) {
if (app.canAndWantToUpdate(activity)) {
return activity.getString(R.string.app_version_x_available, app.getSuggestedVersionName());
} else {
return activity.getString(R.string.app_version_x_installed, app.installedVersionName);
}
}
return null;
}
private boolean shouldShowInstall(@NonNull App app) {
boolean installable = app.canAndWantToUpdate(activity) || !app.isInstalled();
boolean shouldAllow = app.compatible && !app.isFiltered();
return installable && shouldAllow;
}
}

View File

@ -0,0 +1,61 @@
package org.fdroid.fdroid.views.installed;
import android.app.Activity;
import android.database.Cursor;
import android.support.annotation.Nullable;
import android.support.v7.widget.RecyclerView;
import android.view.View;
import android.view.ViewGroup;
import org.fdroid.fdroid.R;
import org.fdroid.fdroid.data.App;
import org.fdroid.fdroid.data.Schema;
class InstalledAppListAdapter extends RecyclerView.Adapter<InstalledAppListItemController> {
private final Activity activity;
@Nullable
private Cursor cursor;
InstalledAppListAdapter(Activity activity) {
this.activity = activity;
setHasStableIds(true);
}
@Override
public long getItemId(int position) {
if (cursor == null) {
return 0;
}
cursor.moveToPosition(position);
return cursor.getLong(cursor.getColumnIndex(Schema.AppMetadataTable.Cols.ROW_ID));
}
@Override
public InstalledAppListItemController onCreateViewHolder(ViewGroup parent, int viewType) {
View view = activity.getLayoutInflater().inflate(R.layout.installed_app_list_item, parent, false);
return new InstalledAppListItemController(activity, view);
}
@Override
public void onBindViewHolder(InstalledAppListItemController holder, int position) {
if (cursor == null) {
return;
}
cursor.moveToPosition(position);
holder.bindModel(new App(cursor));
}
@Override
public int getItemCount() {
return cursor == null ? 0 : cursor.getCount();
}
public void setApps(@Nullable Cursor cursor) {
this.cursor = cursor;
notifyDataSetChanged();
}
}

View File

@ -0,0 +1,62 @@
package org.fdroid.fdroid.views.installed;
import android.app.Activity;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.view.View;
import org.fdroid.fdroid.AppUpdateStatusManager;
import org.fdroid.fdroid.R;
import org.fdroid.fdroid.data.App;
import org.fdroid.fdroid.data.AppPrefs;
import org.fdroid.fdroid.views.apps.AppListItemController;
import org.fdroid.fdroid.views.apps.AppListItemState;
/**
* Shows the currently installed version name, and whether or not it is the recommended version.
* Also shows whether the user has previously asked to ignore updates for this app entirely, or for
* a specific version of this app.
*/
public class InstalledAppListItemController extends AppListItemController {
public InstalledAppListItemController(Activity activity, View itemView) {
super(activity, itemView);
}
@NonNull
@Override
protected AppListItemState getCurrentViewState(
@NonNull App app, @Nullable AppUpdateStatusManager.AppUpdateStatus appStatus) {
return new AppListItemState(app)
.setStatusText(getInstalledVersion(app))
.setSecondaryStatusText(getIgnoreStatus(app));
}
/**
* Either "Version X" or "Version Y (Recommended)", depending on the installed version.
*/
private CharSequence getInstalledVersion(@NonNull App app) {
int statusStringRes = (app.suggestedVersionCode == app.installedVersionCode)
? R.string.app_recommended_version_installed
: R.string.app_version_x_installed;
return activity.getString(statusStringRes, app.installedVersionName);
}
/**
* Show whether the user has ignored a specific version ("Updates ignored for Version X"), or
* all versions ("Updates ignored").
*/
@Nullable
private CharSequence getIgnoreStatus(@NonNull App app) {
AppPrefs prefs = app.getPrefs(activity);
if (prefs.ignoreAllUpdates) {
return activity.getString(R.string.installed_app__updates_ignored);
} else if (prefs.ignoreThisUpdate > 0 && prefs.ignoreThisUpdate == app.suggestedVersionCode) {
return activity.getString(
R.string.installed_app__updates_ignored_for_suggested_version,
app.getSuggestedVersionName());
}
return null;
}
}

View File

@ -19,10 +19,8 @@
package org.fdroid.fdroid.views.installed;
import android.app.Activity;
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;
@ -31,15 +29,12 @@ import android.support.v7.widget.LinearLayoutManager;
import android.support.v7.widget.RecyclerView;
import android.support.v7.widget.Toolbar;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;
import org.fdroid.fdroid.FDroidApp;
import org.fdroid.fdroid.R;
import org.fdroid.fdroid.data.App;
import org.fdroid.fdroid.data.AppProvider;
import org.fdroid.fdroid.data.Schema;
import org.fdroid.fdroid.views.apps.AppListItemController;
public class InstalledAppsActivity extends AppCompatActivity implements LoaderManager.LoaderCallbacks<Cursor> {
@ -105,52 +100,4 @@ public class InstalledAppsActivity extends AppCompatActivity implements LoaderMa
adapter.setApps(null);
}
static class InstalledAppListAdapter extends RecyclerView.Adapter<AppListItemController> {
private final Activity activity;
@Nullable
private Cursor cursor;
InstalledAppListAdapter(Activity activity) {
this.activity = activity;
setHasStableIds(true);
}
@Override
public long getItemId(int position) {
if (cursor == null) {
return 0;
}
cursor.moveToPosition(position);
return cursor.getLong(cursor.getColumnIndex(Schema.AppMetadataTable.Cols.ROW_ID));
}
@Override
public AppListItemController onCreateViewHolder(ViewGroup parent, int viewType) {
View view = activity.getLayoutInflater().inflate(R.layout.installed_app_list_item, parent, false);
return new AppListItemController(activity, view);
}
@Override
public void onBindViewHolder(AppListItemController holder, int position) {
if (cursor == null) {
return;
}
cursor.moveToPosition(position);
holder.bindModel(new App(cursor));
}
@Override
public int getItemCount() {
return cursor == null ? 0 : cursor.getCount();
}
public void setApps(@Nullable Cursor cursor) {
this.cursor = cursor;
notifyDataSetChanged();
}
}
}

View File

@ -8,7 +8,6 @@ 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;
@ -16,8 +15,8 @@ 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}
* @see AppStatusListItemController Used for binding the {@link App} to the
* {@link R.layout#updateable_app_status_item}.
*/
public class AppStatus extends AppUpdateData {
@ -44,7 +43,7 @@ public class AppStatus extends AppUpdateData {
@NonNull
@Override
protected RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent) {
return new AppListItemController(activity, activity.getLayoutInflater()
return new AppStatusListItemController(activity, activity.getLayoutInflater()
.inflate(R.layout.updateable_app_status_item, parent, false));
}
@ -52,7 +51,7 @@ public class AppStatus extends AppUpdateData {
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);
((AppStatusListItemController) holder).bindModel(app.status.app);
}
}

View File

@ -0,0 +1,48 @@
package org.fdroid.fdroid.views.updates.items;
import android.app.Activity;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.view.View;
import org.fdroid.fdroid.AppUpdateStatusManager;
import org.fdroid.fdroid.R;
import org.fdroid.fdroid.data.App;
import org.fdroid.fdroid.views.apps.AppListItemController;
import org.fdroid.fdroid.views.apps.AppListItemState;
/**
* Shows apps which are:
* * In the process of being downloaded.
* * Downloaded and ready to install.
* * Recently installed and ready to run.
*/
public class AppStatusListItemController extends AppListItemController {
public AppStatusListItemController(Activity activity, View itemView) {
super(activity, itemView);
}
@NonNull
@Override
protected AppListItemState getCurrentViewState(
@NonNull App app, @Nullable AppUpdateStatusManager.AppUpdateStatus appStatus) {
return super.getCurrentViewState(app, appStatus)
.setStatusText(getStatusText(appStatus));
}
@Nullable
private CharSequence getStatusText(@Nullable AppUpdateStatusManager.AppUpdateStatus appStatus) {
if (appStatus != null) {
switch (appStatus.status) {
case ReadyToInstall:
return activity.getString(R.string.app_list_download_ready);
case Installed:
return activity.getString(R.string.notification_content_single_installed);
}
}
return null;
}
}

View File

@ -7,7 +7,6 @@ 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;
@ -16,7 +15,8 @@ import java.util.List;
*
* @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}
* @see UpdateableAppListItemController Used for binding the {@link App} to
* the {@link R.layout#updateable_app_list_item}
*/
public class UpdateableApp extends AppUpdateData {
@ -43,7 +43,7 @@ public class UpdateableApp extends AppUpdateData {
@NonNull
@Override
protected RecyclerView.ViewHolder onCreateViewHolder(ViewGroup parent) {
return new AppListItemController(activity, activity.getLayoutInflater()
return new UpdateableAppListItemController(activity, activity.getLayoutInflater()
.inflate(R.layout.updateable_app_list_item, parent, false));
}
@ -51,7 +51,7 @@ public class UpdateableApp extends AppUpdateData {
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);
((UpdateableAppListItemController) holder).bindModel(app.app);
}
}

View File

@ -0,0 +1,31 @@
package org.fdroid.fdroid.views.updates.items;
import android.app.Activity;
import android.support.annotation.NonNull;
import android.support.annotation.Nullable;
import android.view.View;
import org.fdroid.fdroid.AppUpdateStatusManager;
import org.fdroid.fdroid.data.App;
import org.fdroid.fdroid.views.apps.AppListItemController;
import org.fdroid.fdroid.views.apps.AppListItemState;
/**
* Very trimmed down list item. Only displays the app icon, name, and a download button.
* We don't even need to show download progress, because the intention is that as soon as
* we have started downloading the app, it is removed from the list (and replaced with an
* {@link AppStatusListItemController}.
*/
public class UpdateableAppListItemController extends AppListItemController {
public UpdateableAppListItemController(Activity activity, View itemView) {
super(activity, itemView);
}
@NonNull
@Override
protected AppListItemState getCurrentViewState(
@NonNull App app, @Nullable AppUpdateStatusManager.AppUpdateStatus appStatus) {
return new AppListItemState(app)
.setShowInstallButton(true);
}
}

View File

@ -46,12 +46,7 @@
android:layout_height="wrap_content"
android:layout_marginTop="8dp"
tools:text="Installed"
android:textStyle="italic"
android:textSize="14sp"
android:textColor="#424242"
android:maxLines="1"
android:ellipsize="end"
android:fontFamily="sans-serif-light"
style="@style/AppListItemStatusText"
app:layout_constraintTop_toBottomOf="@+id/app_name"
app:layout_constraintStart_toEndOf="@+id/icon"
android:layout_marginStart="8dp"

View File

@ -44,24 +44,20 @@
android:layout_marginRight="8dp" />
<TextView
android:id="@+id/installed_version"
android:id="@+id/status"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
tools:text="Version 4.7.3 (recommended)"
android:textStyle="italic"
android:textSize="14sp"
android:textColor="?attr/installedApps"
android:maxLines="1"
android:ellipsize="end"
android:fontFamily="sans-serif-light"
style="@style/AppListItemStatusText"
app:layout_constraintTop_toBottomOf="@+id/app_name"
app:layout_constraintStart_toEndOf="@+id/icon"
android:layout_marginStart="8dp"
android:layout_marginLeft="8dp" />
<TextView
android:id="@+id/ignored_status"
android:id="@+id/secondary_status"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
@ -69,7 +65,7 @@
android:textSize="14sp"
android:maxLines="1"
android:ellipsize="end"
app:layout_constraintTop_toBottomOf="@+id/installed_version"
app:layout_constraintTop_toBottomOf="@+id/status"
app:layout_constraintStart_toEndOf="@+id/icon"
android:layout_marginStart="8dp"
android:layout_marginLeft="8dp" />

View File

@ -55,22 +55,8 @@
android:layout_marginRight="16dp"
android:layout_marginTop="8dp"
android:layout_marginBottom="8dp"
app:layout_constraintEnd_toStartOf="@+id/action_button"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toTopOf="@+id/icon"
app:layout_constraintBottom_toBottomOf="@+id/icon" />
<Button
android:id="@+id/action_button"
style="@style/DetailsPrimaryButtonStyleSmall"
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

@ -36,20 +36,18 @@
app:layout_constraintBottom_toBottomOf="parent"
android:layout_marginBottom="8dp"
android:layout_marginStart="8dp"
android:layout_marginLeft="8dp"
android:layout_marginEnd="8dp"
android:layout_marginRight="8dp"
android:layout_marginTop="8dp"
app:layout_constraintVertical_bias="0.333" />
<TextView
android:id="@+id/download_ready"
android:id="@+id/status"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/app_list_download_ready"
android:textSize="14sp"
android:textColor="?attr/lightGrayTextColor"
android:maxLines="1"
android:ellipsize="end"
android:fontFamily="sans-serif-light"
tools:text="@string/app_list_download_ready"
style="@style/AppListItemStatusText"
app:layout_constraintTop_toBottomOf="@+id/app_name"
app:layout_constraintStart_toEndOf="@+id/icon"
android:layout_marginStart="8dp"

View File

@ -6,4 +6,7 @@
</style>
<style name="BodyText" parent="BodyTextV16" />
<style name="AppListItemStatusText" parent="AppListItemStatusTextBase">
<item name="android:fontFamily">sans-serif-light</item>
</style>
</resources>

View File

@ -121,6 +121,17 @@
<item name="android:textColor">?attr/lightGrayTextColor</item>
</style>
<!-- Used for supplementary information to show below an app name in app lists, such as
whether it is incompatible, what version is installed, etc -->
<style name="AppListItemStatusTextBase">
<item name="android:textStyle">italic</item>
<item name="android:textSize">14sp</item>
<item name="android:textColor">?attr/lightGrayTextColor</item>
<item name="android:maxLines">1</item>
<item name="android:ellipsize">end</item>
</style>
<style name="AppListItemStatusText" parent="AppListItemStatusTextBase" />
<style name="SwapTheme.Wizard" parent="Theme.AppCompat.Light.NoActionBar">
<item name="colorButtonNormal">@color/swap_bright_blue</item>
<item name="actionButtonStyle">@style/SwapTheme.Wizard.ActionButton</item>