Update filter adapters off main thread

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

@ -7,92 +7,64 @@
package com.todoroo.astrid.adapter; package com.todoroo.astrid.adapter;
import static androidx.core.content.ContextCompat.getColor; 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 com.todoroo.andlib.utility.AndroidUtilities.preLollipop;
import static org.tasks.caldav.CaldavCalendarSettingsActivity.EXTRA_CALDAV_ACCOUNT;
import android.app.Activity; import android.app.Activity;
import android.content.BroadcastReceiver;
import android.content.Context;
import android.content.Intent; import android.content.Intent;
import android.content.res.ColorStateList; import android.content.res.ColorStateList;
import android.graphics.drawable.Drawable; import android.graphics.drawable.Drawable;
import android.net.Uri; import android.os.Bundle;
import android.view.LayoutInflater; import android.view.LayoutInflater;
import android.view.View; import android.view.View;
import android.view.ViewGroup; import android.view.ViewGroup;
import android.widget.ArrayAdapter; import android.widget.BaseAdapter;
import android.widget.CheckedTextView; import android.widget.CheckedTextView;
import android.widget.ImageView; import android.widget.ImageView;
import android.widget.TextView; import android.widget.TextView;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.content.ContextCompat; import androidx.core.content.ContextCompat;
import androidx.core.content.res.ResourcesCompat; import androidx.core.content.res.ResourcesCompat;
import androidx.core.graphics.drawable.DrawableCompat; import androidx.core.graphics.drawable.DrawableCompat;
import androidx.core.util.Pair;
import com.todoroo.astrid.api.Filter; import com.todoroo.astrid.api.Filter;
import com.todoroo.astrid.api.FilterListItem; import com.todoroo.astrid.api.FilterListItem;
import com.todoroo.astrid.core.CustomFilterActivity;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map;
import javax.inject.Inject; import javax.inject.Inject;
import org.tasks.BuildConfig;
import org.tasks.R; 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.filters.NavigationDrawerSubheader;
import org.tasks.locale.Locale; import org.tasks.locale.Locale;
import org.tasks.preferences.BasicPreferences;
import org.tasks.sync.SynchronizationPreferences; import org.tasks.sync.SynchronizationPreferences;
import org.tasks.themes.Theme; import org.tasks.themes.Theme;
import org.tasks.themes.ThemeCache; 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_SETTINGS = 10123;
public static final int REQUEST_PURCHASE = 10124; 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 static final int VIEW_TYPE_COUNT = FilterListItem.Type.values().length;
private final FilterProvider filterProvider;
private final FilterCounter filterCounter;
private final Activity activity; private final Activity activity;
private final Theme theme; private final Theme theme;
private final Locale locale; 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 LayoutInflater inflater;
private final ThemeCache themeCache; private final ThemeCache themeCache;
private boolean navigationDrawer; private boolean navigationDrawer;
private Filter selected; private FilterListItem selected;
private List<FilterListItem> items = new ArrayList<>();
private Map<Filter, Integer> counts = new HashMap<>();
@Inject @Inject
public FilterAdapter( public FilterAdapter(Activity activity, Theme theme, ThemeCache themeCache, Locale locale) {
FilterProvider filterProvider,
FilterCounter filterCounter,
Activity activity,
Theme theme,
ThemeCache themeCache,
Locale locale,
Inventory inventory) {
super(activity, 0);
this.filterProvider = filterProvider;
this.filterCounter = filterCounter;
this.activity = activity; this.activity = activity;
this.theme = theme; this.theme = theme;
this.locale = locale; this.locale = locale;
this.inventory = inventory;
this.inflater = theme.getLayoutInflater(activity); this.inflater = theme.getLayoutInflater(activity);
this.themeCache = themeCache; this.themeCache = themeCache;
} }
@ -101,33 +73,52 @@ public class FilterAdapter extends ArrayAdapter<FilterListItem> {
navigationDrawer = true; navigationDrawer = true;
} }
public FilterListUpdateReceiver getFilterListUpdateReceiver() { public void save(Bundle outState) {
return filterListUpdateReceiver; outState.putParcelableArrayList(TOKEN_FILTERS, getItems());
outState.putParcelable(TOKEN_SELECTED, selected);
} }
@Override public void restore(Bundle savedInstanceState) {
public boolean hasStableIds() { items = savedInstanceState.getParcelableArrayList(TOKEN_FILTERS);
return true; selected = savedInstanceState.getParcelable(TOKEN_SELECTED);
} }
@Override public void setData(List<FilterListItem> items) {
public void add(FilterListItem item) { setData(items, null);
super.add(item); }
items.add(item); public void setData(List<FilterListItem> items, @Nullable Filter selected) {
setData(items, selected, -1);
}
if (navigationDrawer && item instanceof Filter) { public void setData(List<FilterListItem> items, @Nullable Filter selected, int defaultIndex) {
filterCounter.registerFilter((Filter) item); 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 @Override
public void notifyDataSetChanged() { public int getCount() {
activity.runOnUiThread(FilterAdapter.super::notifyDataSetChanged); assertMainThread();
return items.size();
} }
public void refreshFilterCount() { @Override
filterCounter.refreshFilterCounts(this::notifyDataSetChanged); public FilterListItem getItem(int position) {
assertMainThread();
return items.get(position);
}
@Override
public long getItemId(int position) {
return position;
} }
/** Create or reuse a view */ /** Create or reuse a view */
@ -179,14 +170,20 @@ public class FilterAdapter extends ArrayAdapter<FilterListItem> {
} }
public Filter getSelected() { public Filter getSelected() {
return selected; return selected instanceof Filter ? (Filter) selected : null;
} }
public void setSelected(Filter selected) { public void setSelected(Filter selected) {
this.selected = selected; this.selected = selected;
} }
public ArrayList<FilterListItem> getItems() {
assertMainThread();
return newArrayList(items);
}
public int indexOf(FilterListItem item, int defaultValue) { public int indexOf(FilterListItem item, int defaultValue) {
assertMainThread();
int index = items.indexOf(item); int index = items.indexOf(item);
return index == -1 ? defaultValue : index; return index == -1 ? defaultValue : index;
} }
@ -233,148 +230,6 @@ public class FilterAdapter extends ArrayAdapter<FilterListItem> {
return getView(position, convertView, parent); 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) { private void populateItem(ViewHolder viewHolder) {
FilterListItem filter = viewHolder.item; FilterListItem filter = viewHolder.item;
if (filter == null) { if (filter == null) {
@ -382,9 +237,14 @@ public class FilterAdapter extends ArrayAdapter<FilterListItem> {
} }
if (selected != null && selected.equals(filter)) { 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 { } else {
viewHolder.view.setBackgroundResource(0); viewHolder.view.setBackgroundResource(0);
viewHolder.name.setChecked(false);
} }
viewHolder.icon.setImageResource(filter.icon); viewHolder.icon.setImageResource(filter.icon);
@ -393,17 +253,15 @@ public class FilterAdapter extends ArrayAdapter<FilterListItem> {
? themeCache.getThemeColor(filter.tint).getPrimaryColor() ? themeCache.getThemeColor(filter.tint).getPrimaryColor()
: getColor(activity, R.color.text_primary)); : getColor(activity, R.color.text_primary));
String title = filter.listingTitle; viewHolder.name.setText(filter.listingTitle);
if (!title.equals(viewHolder.name.getText())) {
viewHolder.name.setText(title);
}
int countInt = 0; Integer count = counts.get(filter);
if (filterCounter.containsKey(filter)) { if (count == null || count == 0) {
countInt = filterCounter.get(filter); viewHolder.size.setVisibility(View.GONE);
viewHolder.size.setText(locale.formatNumber(countInt)); } 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) { private void populateHeader(ViewHolder viewHolder) {
@ -416,24 +274,11 @@ public class FilterAdapter extends ArrayAdapter<FilterListItem> {
viewHolder.icon.setVisibility(filter.error ? View.VISIBLE : View.GONE); viewHolder.icon.setVisibility(filter.error ? View.VISIBLE : View.GONE);
} }
/* ======================================================================
* ================================================================ views
* ====================================================================== */
static class ViewHolder { static class ViewHolder {
FilterListItem item; FilterListItem item;
CheckedTextView name; CheckedTextView name;
ImageView icon; ImageView icon;
TextView size; TextView size;
View view; 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) { 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) { public void registerRepeatReceiver(BroadcastReceiver broadcastReceiver) {

@ -5,8 +5,13 @@ import android.os.Bundle;
import com.todoroo.andlib.utility.AndroidUtilities; import com.todoroo.andlib.utility.AndroidUtilities;
import com.todoroo.astrid.adapter.FilterAdapter; import com.todoroo.astrid.adapter.FilterAdapter;
import com.todoroo.astrid.api.Filter; 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 javax.inject.Inject;
import org.tasks.dialogs.DialogBuilder; import org.tasks.dialogs.DialogBuilder;
import org.tasks.filters.FilterProvider;
import org.tasks.injection.ActivityComponent; import org.tasks.injection.ActivityComponent;
import org.tasks.injection.InjectingAppCompatActivity; import org.tasks.injection.InjectingAppCompatActivity;
@ -20,22 +25,28 @@ public class FilterSelectionActivity extends InjectingAppCompatActivity {
@Inject DialogBuilder dialogBuilder; @Inject DialogBuilder dialogBuilder;
@Inject FilterAdapter filterAdapter; @Inject FilterAdapter filterAdapter;
@Inject FilterProvider filterProvider;
private CompositeDisposable disposables;
private Filter selected;
@Override @Override
protected void onCreate(Bundle savedInstanceState) { protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
Intent intent = getIntent(); Intent intent = getIntent();
Filter selected = intent.getParcelableExtra(EXTRA_FILTER);
boolean returnFilter = intent.getBooleanExtra(EXTRA_RETURN_FILTER, false); boolean returnFilter = intent.getBooleanExtra(EXTRA_RETURN_FILTER, false);
selected = intent.getParcelableExtra(EXTRA_FILTER);
filterAdapter.populateList(); if (savedInstanceState != null) {
filterAdapter.restore(savedInstanceState);
}
dialogBuilder dialogBuilder
.newDialog() .newDialog()
.setSingleChoiceItems( .setSingleChoiceItems(
filterAdapter, filterAdapter,
filterAdapter.indexOf(selected, -1), -1,
(dialog, which) -> { (dialog, which) -> {
final Filter selectedFilter = (Filter) filterAdapter.getItem(which); final Filter selectedFilter = (Filter) filterAdapter.getItem(which);
Intent data = new Intent(); Intent data = new Intent();
@ -56,6 +67,32 @@ public class FilterSelectionActivity extends InjectingAppCompatActivity {
.show(); .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 @Override
public void inject(ActivityComponent component) { public void inject(ActivityComponent component) {
component.inject(this); component.inject(this);

@ -7,8 +7,13 @@ import android.app.Dialog;
import android.os.Bundle; import android.os.Bundle;
import com.todoroo.astrid.adapter.FilterAdapter; import com.todoroo.astrid.adapter.FilterAdapter;
import com.todoroo.astrid.api.Filter; 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 javax.inject.Inject;
import org.tasks.dialogs.DialogBuilder; import org.tasks.dialogs.DialogBuilder;
import org.tasks.filters.FilterProvider;
import org.tasks.gtasks.RemoteListSelectionHandler; import org.tasks.gtasks.RemoteListSelectionHandler;
import org.tasks.injection.InjectingNativeDialogFragment; import org.tasks.injection.InjectingNativeDialogFragment;
import org.tasks.injection.NativeDialogFragmentComponent; import org.tasks.injection.NativeDialogFragmentComponent;
@ -16,9 +21,12 @@ import org.tasks.injection.NativeDialogFragmentComponent;
public class RemoteListNativePicker extends InjectingNativeDialogFragment { public class RemoteListNativePicker extends InjectingNativeDialogFragment {
private static final String EXTRA_SELECTED = "extra_selected"; private static final String EXTRA_SELECTED = "extra_selected";
@Inject DialogBuilder dialogBuilder; @Inject DialogBuilder dialogBuilder;
@Inject FilterAdapter filterAdapter; @Inject FilterAdapter filterAdapter;
@Inject FilterProvider filterProvider;
private RemoteListSelectionHandler handler; private RemoteListSelectionHandler handler;
private CompositeDisposable disposables;
public static RemoteListNativePicker newRemoteListNativePicker(Filter selected) { public static RemoteListNativePicker newRemoteListNativePicker(Filter selected) {
RemoteListNativePicker dialog = new RemoteListNativePicker(); RemoteListNativePicker dialog = new RemoteListNativePicker();
@ -32,9 +40,32 @@ public class RemoteListNativePicker extends InjectingNativeDialogFragment {
@Override @Override
public Dialog onCreateDialog(Bundle savedInstanceState) { public Dialog onCreateDialog(Bundle savedInstanceState) {
filterAdapter.populateRemoteListPicker(); if (savedInstanceState != null) {
int selected = filterAdapter.indexOf(getArguments().getParcelable(EXTRA_SELECTED), 0); filterAdapter.restore(savedInstanceState);
return createDialog(filterAdapter, dialogBuilder, selected, list -> handler.selectedList(list)); }
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 @Override
@ -44,6 +75,13 @@ public class RemoteListNativePicker extends InjectingNativeDialogFragment {
handler = (RemoteListSelectionHandler) activity; handler = (RemoteListSelectionHandler) activity;
} }
@Override
public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState);
filterAdapter.save(outState);
}
@Override @Override
protected void inject(NativeDialogFragmentComponent component) { protected void inject(NativeDialogFragmentComponent component) {
component.inject(this); component.inject(this);

@ -12,8 +12,13 @@ import com.todoroo.astrid.api.CaldavFilter;
import com.todoroo.astrid.api.Filter; import com.todoroo.astrid.api.Filter;
import com.todoroo.astrid.api.FilterListItem; import com.todoroo.astrid.api.FilterListItem;
import com.todoroo.astrid.api.GtasksFilter; 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 javax.inject.Inject;
import org.tasks.dialogs.DialogBuilder; import org.tasks.dialogs.DialogBuilder;
import org.tasks.filters.FilterProvider;
import org.tasks.gtasks.RemoteListSelectionHandler; import org.tasks.gtasks.RemoteListSelectionHandler;
import org.tasks.injection.DialogFragmentComponent; import org.tasks.injection.DialogFragmentComponent;
import org.tasks.injection.InjectingDialogFragment; import org.tasks.injection.InjectingDialogFragment;
@ -25,6 +30,9 @@ public class RemoteListSupportPicker extends InjectingDialogFragment {
@Inject DialogBuilder dialogBuilder; @Inject DialogBuilder dialogBuilder;
@Inject FilterAdapter filterAdapter; @Inject FilterAdapter filterAdapter;
@Inject FilterProvider filterProvider;
private CompositeDisposable disposables;
public static RemoteListSupportPicker newRemoteListSupportPicker( public static RemoteListSupportPicker newRemoteListSupportPicker(
Filter selected, Fragment targetFragment, int requestCode) { Filter selected, Fragment targetFragment, int requestCode) {
@ -46,16 +54,15 @@ public class RemoteListSupportPicker extends InjectingDialogFragment {
return dialog; return dialog;
} }
public static AlertDialog createDialog( static AlertDialog createDialog(
FilterAdapter filterAdapter, FilterAdapter filterAdapter,
DialogBuilder dialogBuilder, DialogBuilder dialogBuilder,
int selectedIndex,
RemoteListSelectionHandler handler) { RemoteListSelectionHandler handler) {
return dialogBuilder return dialogBuilder
.newDialog() .newDialog()
.setSingleChoiceItems( .setSingleChoiceItems(
filterAdapter, filterAdapter,
selectedIndex, -1,
(dialog, which) -> { (dialog, which) -> {
if (which == 0) { if (which == 0) {
handler.selectedList(null); handler.selectedList(null);
@ -73,13 +80,41 @@ public class RemoteListSupportPicker extends InjectingDialogFragment {
@NonNull @NonNull
@Override @Override
public Dialog onCreateDialog(Bundle savedInstanceState) { 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(); Bundle arguments = getArguments();
int selected = boolean noSelection = arguments.getBoolean(EXTRA_NO_SELECTION, false);
arguments.getBoolean(EXTRA_NO_SELECTION, false) Filter selected = noSelection ? null : arguments.getParcelable(EXTRA_SELECTED_FILTER);
? -1
: filterAdapter.indexOf(arguments.getParcelable(EXTRA_SELECTED_FILTER), 0); disposables =
return createDialog(filterAdapter, dialogBuilder, selected, this::selected); 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) { 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; 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 androidx.core.util.Pair;
import com.google.common.collect.ImmutableList;
import com.todoroo.astrid.api.Filter; import com.todoroo.astrid.api.Filter;
import com.todoroo.astrid.api.FilterListItem;
import com.todoroo.astrid.core.BuiltInFilterExposer; import com.todoroo.astrid.core.BuiltInFilterExposer;
import com.todoroo.astrid.core.CustomFilterActivity;
import com.todoroo.astrid.core.CustomFilterExposer; import com.todoroo.astrid.core.CustomFilterExposer;
import com.todoroo.astrid.gtasks.GtasksFilterExposer; import com.todoroo.astrid.gtasks.GtasksFilterExposer;
import com.todoroo.astrid.tags.TagFilterExposer; import com.todoroo.astrid.tags.TagFilterExposer;
@ -10,12 +24,24 @@ import com.todoroo.astrid.timers.TimerFilterExposer;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import javax.inject.Inject; 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.caldav.CaldavFilterExposer;
import org.tasks.data.CaldavAccount; import org.tasks.data.CaldavAccount;
import org.tasks.data.GoogleTaskAccount; import org.tasks.data.GoogleTaskAccount;
import org.tasks.injection.ForApplication;
import org.tasks.preferences.BasicPreferences;
import org.tasks.ui.NavigationDrawerFragment;
public class FilterProvider { public class FilterProvider {
private final Context context;
private final Inventory inventory;
private final BuiltInFilterExposer builtInFilterExposer; private final BuiltInFilterExposer builtInFilterExposer;
private final TimerFilterExposer timerFilterExposer; private final TimerFilterExposer timerFilterExposer;
private final CustomFilterExposer customFilterExposer; private final CustomFilterExposer customFilterExposer;
@ -25,12 +51,16 @@ public class FilterProvider {
@Inject @Inject
public FilterProvider( public FilterProvider(
@ForApplication Context context,
Inventory inventory,
BuiltInFilterExposer builtInFilterExposer, BuiltInFilterExposer builtInFilterExposer,
TimerFilterExposer timerFilterExposer, TimerFilterExposer timerFilterExposer,
CustomFilterExposer customFilterExposer, CustomFilterExposer customFilterExposer,
TagFilterExposer tagFilterExposer, TagFilterExposer tagFilterExposer,
GtasksFilterExposer gtasksFilterExposer, GtasksFilterExposer gtasksFilterExposer,
CaldavFilterExposer caldavFilterExposer) { CaldavFilterExposer caldavFilterExposer) {
this.context = context;
this.inventory = inventory;
this.builtInFilterExposer = builtInFilterExposer; this.builtInFilterExposer = builtInFilterExposer;
this.timerFilterExposer = timerFilterExposer; this.timerFilterExposer = timerFilterExposer;
this.customFilterExposer = customFilterExposer; this.customFilterExposer = customFilterExposer;
@ -39,11 +69,132 @@ public class FilterProvider {
this.caldavFilterExposer = caldavFilterExposer; this.caldavFilterExposer = caldavFilterExposer;
} }
public Filter getMyTasksFilter() { public List<FilterListItem> getRemoteListPickerItems() {
return builtInFilterExposer.getMyTasksFilter(); 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<>(); ArrayList<Filter> filters = new ArrayList<>();
filters.addAll(builtInFilterExposer.getFilters()); filters.addAll(builtInFilterExposer.getFilters());
filters.addAll(timerFilterExposer.getFilters()); filters.addAll(timerFilterExposer.getFilters());
@ -51,15 +202,23 @@ public class FilterProvider {
return filters; return filters;
} }
public List<Filter> getTags() { private List<Pair<GoogleTaskAccount, List<Filter>>> getGoogleTaskFilters() {
return tagFilterExposer.getFilters();
}
public List<Pair<GoogleTaskAccount, List<Filter>>> getGoogleTaskFilters() {
return gtasksFilterExposer.getFilters(); return gtasksFilterExposer.getFilters();
} }
public List<Pair<CaldavAccount, List<Filter>>> getCaldavFilters() { private List<Pair<CaldavAccount, List<Filter>>> getCaldavFilters() {
return caldavFilterExposer.getFilters(); 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; package org.tasks.ui;
import static android.app.Activity.RESULT_OK; 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 static com.todoroo.andlib.utility.AndroidUtilities.atLeastLollipop;
import android.app.Activity; import android.app.Activity;
@ -18,14 +20,23 @@ import com.todoroo.astrid.activity.MainActivity;
import com.todoroo.astrid.adapter.FilterAdapter; import com.todoroo.astrid.adapter.FilterAdapter;
import com.todoroo.astrid.api.Filter; import com.todoroo.astrid.api.Filter;
import com.todoroo.astrid.api.FilterListItem; 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 javax.inject.Inject;
import org.tasks.LocalBroadcastManager; import org.tasks.LocalBroadcastManager;
import org.tasks.R; import org.tasks.R;
import org.tasks.filters.FilterProvider;
import org.tasks.filters.NavigationDrawerAction; import org.tasks.filters.NavigationDrawerAction;
import org.tasks.injection.FragmentComponent; import org.tasks.injection.FragmentComponent;
import org.tasks.injection.InjectingFragment; import org.tasks.injection.InjectingFragment;
import org.tasks.preferences.AppearancePreferences; import org.tasks.preferences.AppearancePreferences;
import timber.log.Timber;
public class NavigationDrawerFragment extends InjectingFragment { 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 ACTIVITY_REQUEST_NEW_FILTER = 5;
public static final int REQUEST_NEW_GTASK_LIST = 6; public static final int REQUEST_NEW_GTASK_LIST = 6;
public static final int REQUEST_NEW_CALDAV_COLLECTION = 7; 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(); private final RefreshReceiver refreshReceiver = new RefreshReceiver();
@Inject LocalBroadcastManager localBroadcastManager; @Inject LocalBroadcastManager localBroadcastManager;
@Inject FilterAdapter adapter; @Inject FilterAdapter adapter;
@Inject FilterProvider filterProvider;
@Inject TaskDao taskDao;
/** A pointer to the current callbacks instance (the Activity). */ /** A pointer to the current callbacks instance (the Activity). */
private OnFilterItemClickedListener mCallbacks; private OnFilterItemClickedListener mCallbacks;
private DrawerLayout mDrawerLayout; private DrawerLayout mDrawerLayout;
private ListView mDrawerListView; private ListView mDrawerListView;
private View mFragmentContainerView; private View mFragmentContainerView;
private CompositeDisposable disposables;
@Override @Override
public void onCreate(Bundle savedInstanceState) { public void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState); super.onCreate(savedInstanceState);
if (savedInstanceState != null) { if (savedInstanceState != null) {
adapter.setSelected(savedInstanceState.getParcelable(TOKEN_LAST_SELECTED)); adapter.restore(savedInstanceState);
} }
} }
@ -145,15 +158,10 @@ public class NavigationDrawerFragment extends InjectingFragment {
@Override @Override
public void onPause() { public void onPause() {
super.onPause(); super.onPause();
if (adapter != null) {
localBroadcastManager.unregisterReceiver(adapter.getFilterListUpdateReceiver()); localBroadcastManager.unregisterReceiver(refreshReceiver);
}
try { disposables.dispose();
localBroadcastManager.unregisterReceiver(refreshReceiver);
} catch (IllegalArgumentException e) {
// Might not have fully initialized
Timber.e(e);
}
} }
private void selectItem(int position) { private void selectItem(int position) {
@ -195,7 +203,8 @@ public class NavigationDrawerFragment extends InjectingFragment {
@Override @Override
public void onSaveInstanceState(Bundle outState) { public void onSaveInstanceState(Bundle outState) {
super.onSaveInstanceState(outState); super.onSaveInstanceState(outState);
outState.putParcelable(TOKEN_LAST_SELECTED, adapter.getSelected());
adapter.save(outState);
} }
public void closeDrawer() { public void closeDrawer() {
@ -215,34 +224,49 @@ public class NavigationDrawerFragment extends InjectingFragment {
} }
} }
private void repopulateList() {
adapter.populateList();
}
@Override @Override
public void onResume() { public void onResume() {
super.onResume(); super.onResume();
localBroadcastManager.registerRefreshReceiver(refreshReceiver);
disposables = new CompositeDisposable();
localBroadcastManager.registerRefreshListReceiver(refreshReceiver); localBroadcastManager.registerRefreshListReceiver(refreshReceiver);
disposables.add(updateFilters());
}
if (adapter != null) { private Disposable updateFilters() {
localBroadcastManager.registerRefreshReceiver(adapter.getFilterListUpdateReceiver()); return Single.fromCallable(() -> filterProvider.getItems(true))
repopulateList(); .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 { public interface OnFilterItemClickedListener {
void onFilterItemClicked(FilterListItem item); void onFilterItemClicked(FilterListItem item);
} }
/** private class RefreshReceiver extends BroadcastReceiver {
* Receiver which receives refresh intents
*
* @author Tim Su <tim@todoroo.com>
*/
protected class RefreshReceiver extends BroadcastReceiver {
@Override @Override
public void onReceive(Context context, Intent intent) { public void onReceive(Context context, Intent intent) {
if (intent == null) { if (intent == null) {
@ -250,9 +274,9 @@ public class NavigationDrawerFragment extends InjectingFragment {
} }
String action = intent.getAction(); String action = intent.getAction();
if (LocalBroadcastManager.REFRESH.equals(action)) { if (LocalBroadcastManager.REFRESH.equals(action)) {
adapter.refreshFilterCount(); disposables.add(updateCount());
} else if (LocalBroadcastManager.REFRESH_LIST.equals(action)) { } else if (LocalBroadcastManager.REFRESH_LIST.equals(action)) {
repopulateList(); disposables.add(updateFilters());
} }
} }
} }

Loading…
Cancel
Save