Update filter adapters off main thread

pull/795/head
Alex Baker 5 years ago
parent ea2e4ebc12
commit 106365e2a4

@ -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<FilterListItem> {
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<FilterListItem> items = new ArrayList<>();
private final LayoutInflater inflater;
private final ThemeCache themeCache;
private boolean navigationDrawer;
private Filter selected;
private FilterListItem selected;
private List<FilterListItem> items = new ArrayList<>();
private Map<Filter, Integer> 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<FilterListItem> {
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<FilterListItem> items) {
setData(items, null);
}
items.add(item);
public void setData(List<FilterListItem> items, @Nullable Filter selected) {
setData(items, selected, -1);
}
if (navigationDrawer && item instanceof Filter) {
filterCounter.registerFilter((Filter) item);
}
public void setData(List<FilterListItem> items, @Nullable Filter selected, int defaultIndex) {
assertMainThread();
this.items = items;
this.selected = defaultIndex >= 0 ? getItem(indexOf(selected, defaultIndex)) : selected;
notifyDataSetChanged();
}
public void setCounts(Map<Filter, Integer> 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<FilterListItem> {
}
public Filter getSelected() {
return selected;
return selected instanceof Filter ? (Filter) selected : null;
}
public void setSelected(Filter selected) {
this.selected = selected;
}
public ArrayList<FilterListItem> 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<FilterListItem> {
return getView(position, convertView, parent);
}
private void addSubMenu(
final int titleResource, boolean error, List<Filter> filters, boolean hideIfEmpty) {
addSubMenu(activity.getResources().getString(titleResource), error, filters, hideIfEmpty);
}
/* ======================================================================
* ============================================================= receiver
* ====================================================================== */
private void addSubMenu(String title, boolean error, List<Filter> 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<GoogleTaskAccount, List<Filter>> filters : filterProvider.getGoogleTaskFilters()) {
GoogleTaskAccount account = filters.first;
addSubMenu(account.getAccount(), !isNullOrEmpty(account.getError()), filters.second, true);
}
for (Pair<CaldavAccount, List<Filter>> 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<GoogleTaskAccount, List<Filter>> 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<CaldavAccount, List<Filter>> 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<FilterListItem> {
}
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<FilterListItem> {
? 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<FilterListItem> {
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();
}
}
}

@ -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) {

@ -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);

@ -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);

@ -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) {

@ -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<Filter, Integer> 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);
}
}

@ -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<FilterListItem> getRemoteListPickerItems() {
assertNotMainThread();
List<FilterListItem> 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<GoogleTaskAccount, List<Filter>> filters : getGoogleTaskFilters()) {
GoogleTaskAccount account = filters.first;
items.addAll(
getSubmenu(
account.getAccount(), !isNullOrEmpty(account.getError()), filters.second, true));
}
for (Pair<CaldavAccount, List<Filter>> filters : getCaldavFilters()) {
CaldavAccount account = filters.first;
items.addAll(
getSubmenu(account.getName(), !isNullOrEmpty(account.getError()), filters.second, true));
}
return items;
}
public List<FilterListItem> getItems(boolean navigationDrawer) {
assertNotMainThread();
List<FilterListItem> 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<GoogleTaskAccount, List<Filter>> 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<CaldavAccount, List<Filter>> 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<Filter> getFilters() {
private List<Filter> getFilters() {
ArrayList<Filter> filters = new ArrayList<>();
filters.addAll(builtInFilterExposer.getFilters());
filters.addAll(timerFilterExposer.getFilters());
@ -51,15 +202,23 @@ public class FilterProvider {
return filters;
}
public List<Filter> getTags() {
return tagFilterExposer.getFilters();
}
public List<Pair<GoogleTaskAccount, List<Filter>>> getGoogleTaskFilters() {
private List<Pair<GoogleTaskAccount, List<Filter>>> getGoogleTaskFilters() {
return gtasksFilterExposer.getFilters();
}
public List<Pair<CaldavAccount, List<Filter>>> getCaldavFilters() {
private List<Pair<CaldavAccount, List<Filter>>> getCaldavFilters() {
return caldavFilterExposer.getFilters();
}
private List<FilterListItem> getSubmenu(int title, List<Filter> filters) {
return getSubmenu(context.getString(title), false, filters, false);
}
private List<FilterListItem> getSubmenu(
String title, boolean error, List<Filter> filters, boolean hideIfEmpty) {
return hideIfEmpty && filters.isEmpty()
? ImmutableList.of()
: newArrayList(
concat(ImmutableList.of(new NavigationDrawerSubheader(title, error)), filters));
}
}

@ -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<FilterListItem> items = adapter.getItems();
return Single.fromCallable(() -> this.refreshFilterCount(items))
.subscribeOn(Schedulers.io())
.observeOn(AndroidSchedulers.mainThread())
.subscribe(adapter::setCounts);
}
private Map<Filter, Integer> refreshFilterCount(List<FilterListItem> items) {
assertNotMainThread();
Map<Filter, Integer> 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 <tim@todoroo.com>
*/
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());
}
}
}

Loading…
Cancel
Save