From 106365e2a47a69468b89c55d0b2cd34a9b11ce9a Mon Sep 17 00:00:00 2001 From: Alex Baker Date: Wed, 23 Jan 2019 17:02:40 -0600 Subject: [PATCH] Update filter adapters off main thread --- .../todoroo/astrid/adapter/FilterAdapter.java | 293 +++++------------- .../java/org/tasks/LocalBroadcastManager.java | 5 +- .../activities/FilterSelectionActivity.java | 43 ++- .../activities/RemoteListNativePicker.java | 44 ++- .../activities/RemoteListSupportPicker.java | 53 +++- .../java/org/tasks/filters/FilterCounter.java | 71 ----- .../org/tasks/filters/FilterProvider.java | 177 ++++++++++- .../tasks/ui/NavigationDrawerFragment.java | 86 +++-- 8 files changed, 421 insertions(+), 351 deletions(-) delete mode 100644 app/src/main/java/org/tasks/filters/FilterCounter.java diff --git a/app/src/main/java/com/todoroo/astrid/adapter/FilterAdapter.java b/app/src/main/java/com/todoroo/astrid/adapter/FilterAdapter.java index 2eaf5edd5..597338d9a 100644 --- a/app/src/main/java/com/todoroo/astrid/adapter/FilterAdapter.java +++ b/app/src/main/java/com/todoroo/astrid/adapter/FilterAdapter.java @@ -7,92 +7,64 @@ package com.todoroo.astrid.adapter; import static androidx.core.content.ContextCompat.getColor; -import static com.google.common.base.Strings.isNullOrEmpty; +import static com.google.common.collect.Lists.newArrayList; +import static com.todoroo.andlib.utility.AndroidUtilities.assertMainThread; import static com.todoroo.andlib.utility.AndroidUtilities.preLollipop; -import static org.tasks.caldav.CaldavCalendarSettingsActivity.EXTRA_CALDAV_ACCOUNT; import android.app.Activity; -import android.content.BroadcastReceiver; -import android.content.Context; import android.content.Intent; import android.content.res.ColorStateList; import android.graphics.drawable.Drawable; -import android.net.Uri; +import android.os.Bundle; import android.view.LayoutInflater; import android.view.View; import android.view.ViewGroup; -import android.widget.ArrayAdapter; +import android.widget.BaseAdapter; import android.widget.CheckedTextView; import android.widget.ImageView; import android.widget.TextView; import androidx.annotation.NonNull; +import androidx.annotation.Nullable; import androidx.core.content.ContextCompat; import androidx.core.content.res.ResourcesCompat; import androidx.core.graphics.drawable.DrawableCompat; -import androidx.core.util.Pair; import com.todoroo.astrid.api.Filter; import com.todoroo.astrid.api.FilterListItem; -import com.todoroo.astrid.core.CustomFilterActivity; import java.util.ArrayList; +import java.util.HashMap; import java.util.List; +import java.util.Map; import javax.inject.Inject; -import org.tasks.BuildConfig; import org.tasks.R; -import org.tasks.activities.GoogleTaskListSettingsActivity; -import org.tasks.activities.TagSettingsActivity; -import org.tasks.billing.Inventory; -import org.tasks.billing.PurchaseActivity; -import org.tasks.caldav.CaldavCalendarSettingsActivity; -import org.tasks.data.CaldavAccount; -import org.tasks.data.GoogleTaskAccount; -import org.tasks.filters.FilterCounter; -import org.tasks.filters.FilterProvider; -import org.tasks.filters.NavigationDrawerAction; -import org.tasks.filters.NavigationDrawerSeparator; import org.tasks.filters.NavigationDrawerSubheader; import org.tasks.locale.Locale; -import org.tasks.preferences.BasicPreferences; import org.tasks.sync.SynchronizationPreferences; import org.tasks.themes.Theme; import org.tasks.themes.ThemeCache; -import org.tasks.ui.NavigationDrawerFragment; -public class FilterAdapter extends ArrayAdapter { +public class FilterAdapter extends BaseAdapter { public static final int REQUEST_SETTINGS = 10123; public static final int REQUEST_PURCHASE = 10124; - // --- instance variables + private static final String TOKEN_FILTERS = "token_filters"; + private static final String TOKEN_SELECTED = "token_selected"; private static final int VIEW_TYPE_COUNT = FilterListItem.Type.values().length; - private final FilterProvider filterProvider; - private final FilterCounter filterCounter; private final Activity activity; private final Theme theme; private final Locale locale; - private final Inventory inventory; - private final FilterListUpdateReceiver filterListUpdateReceiver = new FilterListUpdateReceiver(); - private final List items = new ArrayList<>(); private final LayoutInflater inflater; private final ThemeCache themeCache; private boolean navigationDrawer; - private Filter selected; + private FilterListItem selected; + private List items = new ArrayList<>(); + private Map counts = new HashMap<>(); @Inject - public FilterAdapter( - FilterProvider filterProvider, - FilterCounter filterCounter, - Activity activity, - Theme theme, - ThemeCache themeCache, - Locale locale, - Inventory inventory) { - super(activity, 0); - this.filterProvider = filterProvider; - this.filterCounter = filterCounter; + public FilterAdapter(Activity activity, Theme theme, ThemeCache themeCache, Locale locale) { this.activity = activity; this.theme = theme; this.locale = locale; - this.inventory = inventory; this.inflater = theme.getLayoutInflater(activity); this.themeCache = themeCache; } @@ -101,33 +73,52 @@ public class FilterAdapter extends ArrayAdapter { navigationDrawer = true; } - public FilterListUpdateReceiver getFilterListUpdateReceiver() { - return filterListUpdateReceiver; + public void save(Bundle outState) { + outState.putParcelableArrayList(TOKEN_FILTERS, getItems()); + outState.putParcelable(TOKEN_SELECTED, selected); } - @Override - public boolean hasStableIds() { - return true; + public void restore(Bundle savedInstanceState) { + items = savedInstanceState.getParcelableArrayList(TOKEN_FILTERS); + selected = savedInstanceState.getParcelable(TOKEN_SELECTED); } - @Override - public void add(FilterListItem item) { - super.add(item); + public void setData(List items) { + setData(items, null); + } - items.add(item); + public void setData(List items, @Nullable Filter selected) { + setData(items, selected, -1); + } - if (navigationDrawer && item instanceof Filter) { - filterCounter.registerFilter((Filter) item); - } + public void setData(List items, @Nullable Filter selected, int defaultIndex) { + assertMainThread(); + this.items = items; + this.selected = defaultIndex >= 0 ? getItem(indexOf(selected, defaultIndex)) : selected; + notifyDataSetChanged(); + } + + public void setCounts(Map counts) { + assertMainThread(); + this.counts = counts; + notifyDataSetChanged(); } @Override - public void notifyDataSetChanged() { - activity.runOnUiThread(FilterAdapter.super::notifyDataSetChanged); + public int getCount() { + assertMainThread(); + return items.size(); } - public void refreshFilterCount() { - filterCounter.refreshFilterCounts(this::notifyDataSetChanged); + @Override + public FilterListItem getItem(int position) { + assertMainThread(); + return items.get(position); + } + + @Override + public long getItemId(int position) { + return position; } /** Create or reuse a view */ @@ -179,14 +170,20 @@ public class FilterAdapter extends ArrayAdapter { } public Filter getSelected() { - return selected; + return selected instanceof Filter ? (Filter) selected : null; } public void setSelected(Filter selected) { this.selected = selected; } + public ArrayList getItems() { + assertMainThread(); + return newArrayList(items); + } + public int indexOf(FilterListItem item, int defaultValue) { + assertMainThread(); int index = items.indexOf(item); return index == -1 ? defaultValue : index; } @@ -233,148 +230,6 @@ public class FilterAdapter extends ArrayAdapter { return getView(position, convertView, parent); } - private void addSubMenu( - final int titleResource, boolean error, List filters, boolean hideIfEmpty) { - addSubMenu(activity.getResources().getString(titleResource), error, filters, hideIfEmpty); - } - - /* ====================================================================== - * ============================================================= receiver - * ====================================================================== */ - - private void addSubMenu(String title, boolean error, List filters, boolean hideIfEmpty) { - if (hideIfEmpty && filters.isEmpty()) { - return; - } - - add(new NavigationDrawerSubheader(title, error)); - - for (FilterListItem filterListItem : filters) { - add(filterListItem); - } - } - - @Override - public void clear() { - super.clear(); - items.clear(); - } - - public void populateRemoteListPicker() { - clear(); - - Filter item = new Filter(activity.getString(R.string.dont_sync), null); - item.icon = R.drawable.ic_outline_cloud_off_24px; - add(item); - - for (Pair> filters : filterProvider.getGoogleTaskFilters()) { - GoogleTaskAccount account = filters.first; - addSubMenu(account.getAccount(), !isNullOrEmpty(account.getError()), filters.second, true); - } - - for (Pair> filters : filterProvider.getCaldavFilters()) { - CaldavAccount account = filters.first; - addSubMenu(account.getName(), !isNullOrEmpty(account.getError()), filters.second, true); - } - - notifyDataSetChanged(); - } - - public void populateList() { - clear(); - - add(filterProvider.getMyTasksFilter()); - - addSubMenu(R.string.filters, false, filterProvider.getFilters(), false); - - if (navigationDrawer) { - add( - new NavigationDrawerAction( - activity.getResources().getString(R.string.FLA_new_filter), - R.drawable.ic_outline_add_24px, - new Intent(activity, CustomFilterActivity.class), - NavigationDrawerFragment.ACTIVITY_REQUEST_NEW_FILTER)); - } - - addSubMenu(R.string.tags, false, filterProvider.getTags(), false); - - if (navigationDrawer) { - add( - new NavigationDrawerAction( - activity.getResources().getString(R.string.new_tag), - R.drawable.ic_outline_add_24px, - new Intent(activity, TagSettingsActivity.class), - NavigationDrawerFragment.REQUEST_NEW_LIST)); - } - - for (Pair> filters : filterProvider.getGoogleTaskFilters()) { - GoogleTaskAccount account = filters.first; - addSubMenu( - account.getAccount(), - !isNullOrEmpty(account.getError()), - filters.second, - !navigationDrawer); - - if (navigationDrawer) { - add( - new NavigationDrawerAction( - activity.getResources().getString(R.string.new_list), - R.drawable.ic_outline_add_24px, - new Intent(activity, GoogleTaskListSettingsActivity.class) - .putExtra(GoogleTaskListSettingsActivity.EXTRA_ACCOUNT, account), - NavigationDrawerFragment.REQUEST_NEW_GTASK_LIST)); - } - } - - for (Pair> filters : filterProvider.getCaldavFilters()) { - CaldavAccount account = filters.first; - addSubMenu( - account.getName(), !isNullOrEmpty(account.getError()), filters.second, !navigationDrawer); - - if (navigationDrawer) { - add( - new NavigationDrawerAction( - activity.getString(R.string.new_list), - R.drawable.ic_outline_add_24px, - new Intent(activity, CaldavCalendarSettingsActivity.class) - .putExtra(EXTRA_CALDAV_ACCOUNT, account), - NavigationDrawerFragment.REQUEST_NEW_CALDAV_COLLECTION)); - } - } - - if (navigationDrawer) { - add(new NavigationDrawerSeparator()); - - //noinspection ConstantConditions - if (BuildConfig.FLAVOR.equals("generic")) { - add( - new NavigationDrawerAction( - activity.getResources().getString(R.string.TLA_menu_donate), - R.drawable.ic_outline_attach_money_24px, - new Intent(Intent.ACTION_VIEW, Uri.parse("http://tasks.org/donate")), - REQUEST_PURCHASE)); - } else if (!inventory.hasPro()) { - add( - new NavigationDrawerAction( - activity.getResources().getString(R.string.upgrade_to_pro), - R.drawable.ic_outline_attach_money_24px, - new Intent(activity, PurchaseActivity.class), - REQUEST_PURCHASE)); - } - - add( - new NavigationDrawerAction( - activity.getResources().getString(R.string.TLA_menu_settings), - R.drawable.ic_outline_settings_24px, - new Intent(activity, BasicPreferences.class), - REQUEST_SETTINGS)); - } - - notifyDataSetChanged(); - - filterCounter.refreshFilterCounts(this::notifyDataSetChanged); - } - private void populateItem(ViewHolder viewHolder) { FilterListItem filter = viewHolder.item; if (filter == null) { @@ -382,9 +237,14 @@ public class FilterAdapter extends ArrayAdapter { } if (selected != null && selected.equals(filter)) { - viewHolder.view.setBackgroundColor(getColor(activity, R.color.drawer_color_selected)); + if (navigationDrawer) { + viewHolder.view.setBackgroundColor(getColor(activity, R.color.drawer_color_selected)); + } else { + viewHolder.name.setChecked(true); + } } else { viewHolder.view.setBackgroundResource(0); + viewHolder.name.setChecked(false); } viewHolder.icon.setImageResource(filter.icon); @@ -393,17 +253,15 @@ public class FilterAdapter extends ArrayAdapter { ? themeCache.getThemeColor(filter.tint).getPrimaryColor() : getColor(activity, R.color.text_primary)); - String title = filter.listingTitle; - if (!title.equals(viewHolder.name.getText())) { - viewHolder.name.setText(title); - } + viewHolder.name.setText(filter.listingTitle); - int countInt = 0; - if (filterCounter.containsKey(filter)) { - countInt = filterCounter.get(filter); - viewHolder.size.setText(locale.formatNumber(countInt)); + Integer count = counts.get(filter); + if (count == null || count == 0) { + viewHolder.size.setVisibility(View.GONE); + } else { + viewHolder.size.setText(locale.formatNumber(count)); + viewHolder.size.setVisibility(View.VISIBLE); } - viewHolder.size.setVisibility(countInt > 0 ? View.VISIBLE : View.GONE); } private void populateHeader(ViewHolder viewHolder) { @@ -416,24 +274,11 @@ public class FilterAdapter extends ArrayAdapter { viewHolder.icon.setVisibility(filter.error ? View.VISIBLE : View.GONE); } - /* ====================================================================== - * ================================================================ views - * ====================================================================== */ - static class ViewHolder { - FilterListItem item; CheckedTextView name; ImageView icon; TextView size; View view; } - - public class FilterListUpdateReceiver extends BroadcastReceiver { - - @Override - public void onReceive(Context context, Intent intent) { - notifyDataSetChanged(); - } - } } diff --git a/app/src/main/java/org/tasks/LocalBroadcastManager.java b/app/src/main/java/org/tasks/LocalBroadcastManager.java index 88504d400..c7316c09f 100644 --- a/app/src/main/java/org/tasks/LocalBroadcastManager.java +++ b/app/src/main/java/org/tasks/LocalBroadcastManager.java @@ -31,7 +31,10 @@ public class LocalBroadcastManager { } public void registerRefreshListReceiver(BroadcastReceiver broadcastReceiver) { - localBroadcastManager.registerReceiver(broadcastReceiver, new IntentFilter(REFRESH_LIST)); + IntentFilter intentFilter = new IntentFilter(); + intentFilter.addAction(REFRESH); + intentFilter.addAction(REFRESH_LIST); + localBroadcastManager.registerReceiver(broadcastReceiver, intentFilter); } public void registerRepeatReceiver(BroadcastReceiver broadcastReceiver) { diff --git a/app/src/main/java/org/tasks/activities/FilterSelectionActivity.java b/app/src/main/java/org/tasks/activities/FilterSelectionActivity.java index 5a99722e9..6df447986 100644 --- a/app/src/main/java/org/tasks/activities/FilterSelectionActivity.java +++ b/app/src/main/java/org/tasks/activities/FilterSelectionActivity.java @@ -5,8 +5,13 @@ import android.os.Bundle; import com.todoroo.andlib.utility.AndroidUtilities; import com.todoroo.astrid.adapter.FilterAdapter; import com.todoroo.astrid.api.Filter; +import io.reactivex.Single; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.disposables.CompositeDisposable; +import io.reactivex.schedulers.Schedulers; import javax.inject.Inject; import org.tasks.dialogs.DialogBuilder; +import org.tasks.filters.FilterProvider; import org.tasks.injection.ActivityComponent; import org.tasks.injection.InjectingAppCompatActivity; @@ -20,22 +25,28 @@ public class FilterSelectionActivity extends InjectingAppCompatActivity { @Inject DialogBuilder dialogBuilder; @Inject FilterAdapter filterAdapter; + @Inject FilterProvider filterProvider; + + private CompositeDisposable disposables; + private Filter selected; @Override protected void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); Intent intent = getIntent(); - Filter selected = intent.getParcelableExtra(EXTRA_FILTER); boolean returnFilter = intent.getBooleanExtra(EXTRA_RETURN_FILTER, false); + selected = intent.getParcelableExtra(EXTRA_FILTER); - filterAdapter.populateList(); + if (savedInstanceState != null) { + filterAdapter.restore(savedInstanceState); + } dialogBuilder .newDialog() .setSingleChoiceItems( filterAdapter, - filterAdapter.indexOf(selected, -1), + -1, (dialog, which) -> { final Filter selectedFilter = (Filter) filterAdapter.getItem(which); Intent data = new Intent(); @@ -56,6 +67,32 @@ public class FilterSelectionActivity extends InjectingAppCompatActivity { .show(); } + @Override + protected void onResume() { + super.onResume(); + + disposables = new CompositeDisposable(); + disposables.add( + Single.fromCallable(() -> filterProvider.getItems(false)) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(items -> filterAdapter.setData(items, selected))); + } + + @Override + protected void onPause() { + super.onPause(); + + disposables.dispose(); + } + + @Override + protected void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + + filterAdapter.save(outState); + } + @Override public void inject(ActivityComponent component) { component.inject(this); diff --git a/app/src/main/java/org/tasks/activities/RemoteListNativePicker.java b/app/src/main/java/org/tasks/activities/RemoteListNativePicker.java index 135c43abb..ac09fcc31 100644 --- a/app/src/main/java/org/tasks/activities/RemoteListNativePicker.java +++ b/app/src/main/java/org/tasks/activities/RemoteListNativePicker.java @@ -7,8 +7,13 @@ import android.app.Dialog; import android.os.Bundle; import com.todoroo.astrid.adapter.FilterAdapter; import com.todoroo.astrid.api.Filter; +import io.reactivex.Single; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.disposables.CompositeDisposable; +import io.reactivex.schedulers.Schedulers; import javax.inject.Inject; import org.tasks.dialogs.DialogBuilder; +import org.tasks.filters.FilterProvider; import org.tasks.gtasks.RemoteListSelectionHandler; import org.tasks.injection.InjectingNativeDialogFragment; import org.tasks.injection.NativeDialogFragmentComponent; @@ -16,9 +21,12 @@ import org.tasks.injection.NativeDialogFragmentComponent; public class RemoteListNativePicker extends InjectingNativeDialogFragment { private static final String EXTRA_SELECTED = "extra_selected"; + @Inject DialogBuilder dialogBuilder; @Inject FilterAdapter filterAdapter; + @Inject FilterProvider filterProvider; private RemoteListSelectionHandler handler; + private CompositeDisposable disposables; public static RemoteListNativePicker newRemoteListNativePicker(Filter selected) { RemoteListNativePicker dialog = new RemoteListNativePicker(); @@ -32,9 +40,32 @@ public class RemoteListNativePicker extends InjectingNativeDialogFragment { @Override public Dialog onCreateDialog(Bundle savedInstanceState) { - filterAdapter.populateRemoteListPicker(); - int selected = filterAdapter.indexOf(getArguments().getParcelable(EXTRA_SELECTED), 0); - return createDialog(filterAdapter, dialogBuilder, selected, list -> handler.selectedList(list)); + if (savedInstanceState != null) { + filterAdapter.restore(savedInstanceState); + } + + return createDialog(filterAdapter, dialogBuilder, handler); + } + + @Override + public void onResume() { + super.onResume(); + + Filter selected = getArguments().getParcelable(EXTRA_SELECTED); + + disposables = + new CompositeDisposable( + Single.fromCallable(filterProvider::getRemoteListPickerItems) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(items -> filterAdapter.setData(items, selected, 0))); + } + + @Override + public void onPause() { + super.onPause(); + + disposables.dispose(); } @Override @@ -44,6 +75,13 @@ public class RemoteListNativePicker extends InjectingNativeDialogFragment { handler = (RemoteListSelectionHandler) activity; } + @Override + public void onSaveInstanceState(Bundle outState) { + super.onSaveInstanceState(outState); + + filterAdapter.save(outState); + } + @Override protected void inject(NativeDialogFragmentComponent component) { component.inject(this); diff --git a/app/src/main/java/org/tasks/activities/RemoteListSupportPicker.java b/app/src/main/java/org/tasks/activities/RemoteListSupportPicker.java index 27d51ff0a..3c502de17 100644 --- a/app/src/main/java/org/tasks/activities/RemoteListSupportPicker.java +++ b/app/src/main/java/org/tasks/activities/RemoteListSupportPicker.java @@ -12,8 +12,13 @@ import com.todoroo.astrid.api.CaldavFilter; import com.todoroo.astrid.api.Filter; import com.todoroo.astrid.api.FilterListItem; import com.todoroo.astrid.api.GtasksFilter; +import io.reactivex.Single; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.disposables.CompositeDisposable; +import io.reactivex.schedulers.Schedulers; import javax.inject.Inject; import org.tasks.dialogs.DialogBuilder; +import org.tasks.filters.FilterProvider; import org.tasks.gtasks.RemoteListSelectionHandler; import org.tasks.injection.DialogFragmentComponent; import org.tasks.injection.InjectingDialogFragment; @@ -25,6 +30,9 @@ public class RemoteListSupportPicker extends InjectingDialogFragment { @Inject DialogBuilder dialogBuilder; @Inject FilterAdapter filterAdapter; + @Inject FilterProvider filterProvider; + + private CompositeDisposable disposables; public static RemoteListSupportPicker newRemoteListSupportPicker( Filter selected, Fragment targetFragment, int requestCode) { @@ -46,16 +54,15 @@ public class RemoteListSupportPicker extends InjectingDialogFragment { return dialog; } - public static AlertDialog createDialog( + static AlertDialog createDialog( FilterAdapter filterAdapter, DialogBuilder dialogBuilder, - int selectedIndex, RemoteListSelectionHandler handler) { return dialogBuilder .newDialog() .setSingleChoiceItems( filterAdapter, - selectedIndex, + -1, (dialog, which) -> { if (which == 0) { handler.selectedList(null); @@ -73,13 +80,41 @@ public class RemoteListSupportPicker extends InjectingDialogFragment { @NonNull @Override public Dialog onCreateDialog(Bundle savedInstanceState) { - filterAdapter.populateRemoteListPicker(); + if (savedInstanceState != null) { + filterAdapter.restore(savedInstanceState); + } + + return createDialog(filterAdapter, dialogBuilder, this::selected); + } + + @Override + public void onResume() { + super.onResume(); + Bundle arguments = getArguments(); - int selected = - arguments.getBoolean(EXTRA_NO_SELECTION, false) - ? -1 - : filterAdapter.indexOf(arguments.getParcelable(EXTRA_SELECTED_FILTER), 0); - return createDialog(filterAdapter, dialogBuilder, selected, this::selected); + boolean noSelection = arguments.getBoolean(EXTRA_NO_SELECTION, false); + Filter selected = noSelection ? null : arguments.getParcelable(EXTRA_SELECTED_FILTER); + + disposables = + new CompositeDisposable( + Single.fromCallable(filterProvider::getRemoteListPickerItems) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(items -> filterAdapter.setData(items, selected, noSelection ? -1 : 0))); + } + + @Override + public void onPause() { + super.onPause(); + + disposables.dispose(); + } + + @Override + public void onSaveInstanceState(@NonNull Bundle outState) { + super.onSaveInstanceState(outState); + + filterAdapter.save(outState); } private void selected(Filter filter) { diff --git a/app/src/main/java/org/tasks/filters/FilterCounter.java b/app/src/main/java/org/tasks/filters/FilterCounter.java deleted file mode 100644 index 855cc3c85..000000000 --- a/app/src/main/java/org/tasks/filters/FilterCounter.java +++ /dev/null @@ -1,71 +0,0 @@ -package org.tasks.filters; - -import com.todoroo.astrid.api.Filter; -import com.todoroo.astrid.api.FilterListItem; -import com.todoroo.astrid.dao.TaskDao; -import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.ExecutorService; -import java.util.concurrent.LinkedBlockingQueue; -import java.util.concurrent.ThreadPoolExecutor; -import java.util.concurrent.TimeUnit; -import javax.inject.Inject; - -public class FilterCounter { - - // Previous solution involved a queue of filters and a filterSizeLoadingThread. The - // filterSizeLoadingThread had - // a few problems: how to make sure that the thread is resumed when the controlling activity is - // resumed, and - // how to make sure that the the filterQueue does not accumulate filters without being processed. - // I am replacing - // both the queue and a the thread with a thread pool, which will shut itself off after a second - // if it has - // nothing to do (corePoolSize == 0, which makes it available for garbage collection), and will - // wake itself up - // if new filters are queued (obviously it cannot be garbage collected if it is possible for new - // filters to - // be added). - private final ExecutorService executorService; - - private final Map filterCounts = new ConcurrentHashMap<>(); - - private final TaskDao taskDao; - - @Inject - public FilterCounter(TaskDao taskDao) { - this(taskDao, new ThreadPoolExecutor(0, 1, 1, TimeUnit.SECONDS, new LinkedBlockingQueue<>())); - } - - private FilterCounter(TaskDao taskDao, ExecutorService executorService) { - this.taskDao = taskDao; - this.executorService = executorService; - } - - public void refreshFilterCounts(final Runnable onComplete) { - executorService.submit( - () -> { - for (Filter filter : filterCounts.keySet()) { - int size = taskDao.count(filter); - filterCounts.put(filter, size); - } - if (onComplete != null) { - onComplete.run(); - } - }); - } - - public void registerFilter(Filter filter) { - if (!filterCounts.containsKey(filter)) { - filterCounts.put(filter, 0); - } - } - - public boolean containsKey(FilterListItem filter) { - return filterCounts.containsKey(filter); - } - - public Integer get(FilterListItem filter) { - return filterCounts.get(filter); - } -} diff --git a/app/src/main/java/org/tasks/filters/FilterProvider.java b/app/src/main/java/org/tasks/filters/FilterProvider.java index c6267e824..34c28c3c5 100644 --- a/app/src/main/java/org/tasks/filters/FilterProvider.java +++ b/app/src/main/java/org/tasks/filters/FilterProvider.java @@ -1,8 +1,22 @@ package org.tasks.filters; +import static com.google.common.base.Strings.isNullOrEmpty; +import static com.google.common.collect.Iterables.concat; +import static com.google.common.collect.Lists.newArrayList; +import static com.todoroo.andlib.utility.AndroidUtilities.assertNotMainThread; +import static com.todoroo.astrid.adapter.FilterAdapter.REQUEST_PURCHASE; +import static com.todoroo.astrid.adapter.FilterAdapter.REQUEST_SETTINGS; +import static org.tasks.caldav.CaldavCalendarSettingsActivity.EXTRA_CALDAV_ACCOUNT; + +import android.content.Context; +import android.content.Intent; +import android.net.Uri; import androidx.core.util.Pair; +import com.google.common.collect.ImmutableList; import com.todoroo.astrid.api.Filter; +import com.todoroo.astrid.api.FilterListItem; import com.todoroo.astrid.core.BuiltInFilterExposer; +import com.todoroo.astrid.core.CustomFilterActivity; import com.todoroo.astrid.core.CustomFilterExposer; import com.todoroo.astrid.gtasks.GtasksFilterExposer; import com.todoroo.astrid.tags.TagFilterExposer; @@ -10,12 +24,24 @@ import com.todoroo.astrid.timers.TimerFilterExposer; import java.util.ArrayList; import java.util.List; import javax.inject.Inject; +import org.tasks.BuildConfig; +import org.tasks.R; +import org.tasks.activities.GoogleTaskListSettingsActivity; +import org.tasks.activities.TagSettingsActivity; +import org.tasks.billing.Inventory; +import org.tasks.billing.PurchaseActivity; +import org.tasks.caldav.CaldavCalendarSettingsActivity; import org.tasks.caldav.CaldavFilterExposer; import org.tasks.data.CaldavAccount; import org.tasks.data.GoogleTaskAccount; +import org.tasks.injection.ForApplication; +import org.tasks.preferences.BasicPreferences; +import org.tasks.ui.NavigationDrawerFragment; public class FilterProvider { + private final Context context; + private final Inventory inventory; private final BuiltInFilterExposer builtInFilterExposer; private final TimerFilterExposer timerFilterExposer; private final CustomFilterExposer customFilterExposer; @@ -25,12 +51,16 @@ public class FilterProvider { @Inject public FilterProvider( + @ForApplication Context context, + Inventory inventory, BuiltInFilterExposer builtInFilterExposer, TimerFilterExposer timerFilterExposer, CustomFilterExposer customFilterExposer, TagFilterExposer tagFilterExposer, GtasksFilterExposer gtasksFilterExposer, CaldavFilterExposer caldavFilterExposer) { + this.context = context; + this.inventory = inventory; this.builtInFilterExposer = builtInFilterExposer; this.timerFilterExposer = timerFilterExposer; this.customFilterExposer = customFilterExposer; @@ -39,11 +69,132 @@ public class FilterProvider { this.caldavFilterExposer = caldavFilterExposer; } - public Filter getMyTasksFilter() { - return builtInFilterExposer.getMyTasksFilter(); + public List getRemoteListPickerItems() { + assertNotMainThread(); + + List items = new ArrayList<>(); + + Filter item = new Filter(context.getString(R.string.dont_sync), null); + item.icon = R.drawable.ic_outline_cloud_off_24px; + items.add(item); + + for (Pair> filters : getGoogleTaskFilters()) { + GoogleTaskAccount account = filters.first; + items.addAll( + getSubmenu( + account.getAccount(), !isNullOrEmpty(account.getError()), filters.second, true)); + } + + for (Pair> filters : getCaldavFilters()) { + CaldavAccount account = filters.first; + items.addAll( + getSubmenu(account.getName(), !isNullOrEmpty(account.getError()), filters.second, true)); + } + + return items; + } + + public List getItems(boolean navigationDrawer) { + assertNotMainThread(); + + List items = new ArrayList<>(); + + items.add(builtInFilterExposer.getMyTasksFilter()); + + items.addAll(getSubmenu(R.string.filters, getFilters())); + + if (navigationDrawer) { + items.add( + new NavigationDrawerAction( + context.getString(R.string.FLA_new_filter), + R.drawable.ic_outline_add_24px, + new Intent(context, CustomFilterActivity.class), + NavigationDrawerFragment.ACTIVITY_REQUEST_NEW_FILTER)); + } + + items.addAll(getSubmenu(R.string.tags, tagFilterExposer.getFilters())); + + if (navigationDrawer) { + items.add( + new NavigationDrawerAction( + context.getString(R.string.new_tag), + R.drawable.ic_outline_add_24px, + new Intent(context, TagSettingsActivity.class), + NavigationDrawerFragment.REQUEST_NEW_LIST)); + } + + for (Pair> filters : getGoogleTaskFilters()) { + GoogleTaskAccount account = filters.first; + items.addAll( + getSubmenu( + account.getAccount(), + !isNullOrEmpty(account.getError()), + filters.second, + !navigationDrawer)); + + if (navigationDrawer) { + items.add( + new NavigationDrawerAction( + context.getString(R.string.new_list), + R.drawable.ic_outline_add_24px, + new Intent(context, GoogleTaskListSettingsActivity.class) + .putExtra(GoogleTaskListSettingsActivity.EXTRA_ACCOUNT, account), + NavigationDrawerFragment.REQUEST_NEW_GTASK_LIST)); + } + } + + for (Pair> filters : getCaldavFilters()) { + CaldavAccount account = filters.first; + items.addAll( + getSubmenu( + account.getName(), + !isNullOrEmpty(account.getError()), + filters.second, + !navigationDrawer)); + + if (navigationDrawer) { + items.add( + new NavigationDrawerAction( + context.getString(R.string.new_list), + R.drawable.ic_outline_add_24px, + new Intent(context, CaldavCalendarSettingsActivity.class) + .putExtra(EXTRA_CALDAV_ACCOUNT, account), + NavigationDrawerFragment.REQUEST_NEW_CALDAV_COLLECTION)); + } + } + + if (navigationDrawer) { + items.add(new NavigationDrawerSeparator()); + + //noinspection ConstantConditions + if (BuildConfig.FLAVOR.equals("generic")) { + items.add( + new NavigationDrawerAction( + context.getString(R.string.TLA_menu_donate), + R.drawable.ic_outline_attach_money_24px, + new Intent(Intent.ACTION_VIEW, Uri.parse("http://tasks.org/donate")), + REQUEST_PURCHASE)); + } else if (!inventory.hasPro()) { + items.add( + new NavigationDrawerAction( + context.getString(R.string.upgrade_to_pro), + R.drawable.ic_outline_attach_money_24px, + new Intent(context, PurchaseActivity.class), + REQUEST_PURCHASE)); + } + + items.add( + new NavigationDrawerAction( + context.getString(R.string.TLA_menu_settings), + R.drawable.ic_outline_settings_24px, + new Intent(context, BasicPreferences.class), + REQUEST_SETTINGS)); + } + + return items; } - public List getFilters() { + private List getFilters() { ArrayList filters = new ArrayList<>(); filters.addAll(builtInFilterExposer.getFilters()); filters.addAll(timerFilterExposer.getFilters()); @@ -51,15 +202,23 @@ public class FilterProvider { return filters; } - public List getTags() { - return tagFilterExposer.getFilters(); - } - - public List>> getGoogleTaskFilters() { + private List>> getGoogleTaskFilters() { return gtasksFilterExposer.getFilters(); } - public List>> getCaldavFilters() { + private List>> getCaldavFilters() { return caldavFilterExposer.getFilters(); } + + private List getSubmenu(int title, List filters) { + return getSubmenu(context.getString(title), false, filters, false); + } + + private List getSubmenu( + String title, boolean error, List filters, boolean hideIfEmpty) { + return hideIfEmpty && filters.isEmpty() + ? ImmutableList.of() + : newArrayList( + concat(ImmutableList.of(new NavigationDrawerSubheader(title, error)), filters)); + } } diff --git a/app/src/main/java/org/tasks/ui/NavigationDrawerFragment.java b/app/src/main/java/org/tasks/ui/NavigationDrawerFragment.java index 72a1fc2f0..1323dec9e 100644 --- a/app/src/main/java/org/tasks/ui/NavigationDrawerFragment.java +++ b/app/src/main/java/org/tasks/ui/NavigationDrawerFragment.java @@ -1,6 +1,8 @@ package org.tasks.ui; import static android.app.Activity.RESULT_OK; +import static com.google.common.collect.Iterables.filter; +import static com.todoroo.andlib.utility.AndroidUtilities.assertNotMainThread; import static com.todoroo.andlib.utility.AndroidUtilities.atLeastLollipop; import android.app.Activity; @@ -18,14 +20,23 @@ import com.todoroo.astrid.activity.MainActivity; import com.todoroo.astrid.adapter.FilterAdapter; import com.todoroo.astrid.api.Filter; import com.todoroo.astrid.api.FilterListItem; +import com.todoroo.astrid.dao.TaskDao; +import io.reactivex.Single; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.disposables.CompositeDisposable; +import io.reactivex.disposables.Disposable; +import io.reactivex.schedulers.Schedulers; +import java.util.HashMap; +import java.util.List; +import java.util.Map; import javax.inject.Inject; import org.tasks.LocalBroadcastManager; import org.tasks.R; +import org.tasks.filters.FilterProvider; import org.tasks.filters.NavigationDrawerAction; import org.tasks.injection.FragmentComponent; import org.tasks.injection.InjectingFragment; import org.tasks.preferences.AppearancePreferences; -import timber.log.Timber; public class NavigationDrawerFragment extends InjectingFragment { @@ -34,23 +45,25 @@ public class NavigationDrawerFragment extends InjectingFragment { public static final int ACTIVITY_REQUEST_NEW_FILTER = 5; public static final int REQUEST_NEW_GTASK_LIST = 6; public static final int REQUEST_NEW_CALDAV_COLLECTION = 7; - private static final String TOKEN_LAST_SELECTED = "lastSelected"; // $NON-NLS-1$ private final RefreshReceiver refreshReceiver = new RefreshReceiver(); @Inject LocalBroadcastManager localBroadcastManager; @Inject FilterAdapter adapter; + @Inject FilterProvider filterProvider; + @Inject TaskDao taskDao; /** A pointer to the current callbacks instance (the Activity). */ private OnFilterItemClickedListener mCallbacks; private DrawerLayout mDrawerLayout; private ListView mDrawerListView; private View mFragmentContainerView; + private CompositeDisposable disposables; @Override public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); if (savedInstanceState != null) { - adapter.setSelected(savedInstanceState.getParcelable(TOKEN_LAST_SELECTED)); + adapter.restore(savedInstanceState); } } @@ -145,15 +158,10 @@ public class NavigationDrawerFragment extends InjectingFragment { @Override public void onPause() { super.onPause(); - if (adapter != null) { - localBroadcastManager.unregisterReceiver(adapter.getFilterListUpdateReceiver()); - } - try { - localBroadcastManager.unregisterReceiver(refreshReceiver); - } catch (IllegalArgumentException e) { - // Might not have fully initialized - Timber.e(e); - } + + localBroadcastManager.unregisterReceiver(refreshReceiver); + + disposables.dispose(); } private void selectItem(int position) { @@ -195,7 +203,8 @@ public class NavigationDrawerFragment extends InjectingFragment { @Override public void onSaveInstanceState(Bundle outState) { super.onSaveInstanceState(outState); - outState.putParcelable(TOKEN_LAST_SELECTED, adapter.getSelected()); + + adapter.save(outState); } public void closeDrawer() { @@ -215,34 +224,49 @@ public class NavigationDrawerFragment extends InjectingFragment { } } - private void repopulateList() { - adapter.populateList(); - } - @Override public void onResume() { super.onResume(); - localBroadcastManager.registerRefreshReceiver(refreshReceiver); + + disposables = new CompositeDisposable(); localBroadcastManager.registerRefreshListReceiver(refreshReceiver); + disposables.add(updateFilters()); + } - if (adapter != null) { - localBroadcastManager.registerRefreshReceiver(adapter.getFilterListUpdateReceiver()); - repopulateList(); + private Disposable updateFilters() { + return Single.fromCallable(() -> filterProvider.getItems(true)) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .doOnSuccess(adapter::setData) + .observeOn(Schedulers.io()) + .map(this::refreshFilterCount) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(adapter::setCounts); + } + + private Disposable updateCount() { + List items = adapter.getItems(); + return Single.fromCallable(() -> this.refreshFilterCount(items)) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe(adapter::setCounts); + } + + private Map refreshFilterCount(List items) { + assertNotMainThread(); + + Map result = new HashMap<>(); + for (FilterListItem item : filter(items, i -> i instanceof Filter)) { + result.put((Filter) item, taskDao.count((Filter) item)); } + return result; } public interface OnFilterItemClickedListener { - void onFilterItemClicked(FilterListItem item); } - /** - * Receiver which receives refresh intents - * - * @author Tim Su - */ - protected class RefreshReceiver extends BroadcastReceiver { - + private class RefreshReceiver extends BroadcastReceiver { @Override public void onReceive(Context context, Intent intent) { if (intent == null) { @@ -250,9 +274,9 @@ public class NavigationDrawerFragment extends InjectingFragment { } String action = intent.getAction(); if (LocalBroadcastManager.REFRESH.equals(action)) { - adapter.refreshFilterCount(); + disposables.add(updateCount()); } else if (LocalBroadcastManager.REFRESH_LIST.equals(action)) { - repopulateList(); + disposables.add(updateFilters()); } } }