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:
parent
7659c3a33f
commit
40cc328e98
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
};
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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;
|
||||
}
|
||||
}
|
@ -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");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -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>
|
@ -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>
|
62
app/src/main/res/layout/updateable_app_list_item.xml
Normal file
62
app/src/main/res/layout/updateable_app_list_item.xml
Normal file
@ -0,0 +1,62 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<android.support.constraint.ConstraintLayout
|
||||
xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
xmlns:app="http://schemas.android.com/apk/res-auto"
|
||||
xmlns:tools="http://schemas.android.com/tools"
|
||||
android:layout_width="match_parent"
|
||||
android:layout_height="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>
|
87
app/src/main/res/layout/updateable_app_status_item.xml
Normal file
87
app/src/main/res/layout/updateable_app_status_item.xml
Normal 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>
|
61
app/src/main/res/layout/updates_header.xml
Normal file
61
app/src/main/res/layout/updates_header.xml
Normal 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>
|
@ -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>
|
@ -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>
|
||||
|
Loading…
x
Reference in New Issue
Block a user