From d7ddcc4ad7eddbe7b43137487082dae6f4c5301f Mon Sep 17 00:00:00 2001 From: Christophe Beyls Date: Sat, 27 Jan 2018 00:00:05 +0100 Subject: [PATCH] Migrate all non-Cursor Loaders code to LiveData & ViewModel --- app/build.gradle | 1 + .../activities/EventDetailsActivity.java | 97 ++++----- .../digitalia/fosdem/db/DatabaseManager.java | 96 ++++++--- .../fragments/EventDetailsFragment.java | 190 ++++++------------ .../fosdem/fragments/TracksFragment.java | 63 +----- .../fosdem/livedata/AsyncTaskLiveData.java | 99 +++++++++ .../fosdem/loaders/BookmarkStatusLoader.java | 105 ---------- .../fosdem/loaders/GlobalCacheLoader.java | 59 ------ .../fosdem/loaders/LocalCacheLoader.java | 53 ----- .../java/be/digitalia/fosdem/model/Day.java | 13 +- .../viewmodels/EventDetailsViewModel.java | 128 ++++++++++++ .../fosdem/viewmodels/EventViewModel.java | 35 ++++ 12 files changed, 439 insertions(+), 500 deletions(-) create mode 100644 app/src/main/java/be/digitalia/fosdem/livedata/AsyncTaskLiveData.java delete mode 100644 app/src/main/java/be/digitalia/fosdem/loaders/BookmarkStatusLoader.java delete mode 100644 app/src/main/java/be/digitalia/fosdem/loaders/GlobalCacheLoader.java delete mode 100644 app/src/main/java/be/digitalia/fosdem/loaders/LocalCacheLoader.java create mode 100644 app/src/main/java/be/digitalia/fosdem/viewmodels/EventDetailsViewModel.java create mode 100644 app/src/main/java/be/digitalia/fosdem/viewmodels/EventViewModel.java diff --git a/app/build.gradle b/app/build.gradle index 8011fd5..838b35f 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -33,5 +33,6 @@ dependencies { implementation "com.android.support:recyclerview-v7:$supportLibraryVersion" implementation "com.android.support:cardview-v7:$supportLibraryVersion" implementation "com.android.support:customtabs:$supportLibraryVersion" + implementation 'android.arch.lifecycle:extensions:1.1.0' implementation 'com.github.chrisbanes:PhotoView:2.1.3' } diff --git a/app/src/main/java/be/digitalia/fosdem/activities/EventDetailsActivity.java b/app/src/main/java/be/digitalia/fosdem/activities/EventDetailsActivity.java index bafa7d9..d6de8c3 100644 --- a/app/src/main/java/be/digitalia/fosdem/activities/EventDetailsActivity.java +++ b/app/src/main/java/be/digitalia/fosdem/activities/EventDetailsActivity.java @@ -1,39 +1,36 @@ package be.digitalia.fosdem.activities; -import android.content.Context; +import android.arch.lifecycle.Observer; +import android.arch.lifecycle.ViewModelProviders; import android.content.Intent; import android.os.Bundle; import android.support.annotation.NonNull; +import android.support.annotation.Nullable; import android.support.v4.app.Fragment; import android.support.v4.app.FragmentManager; -import android.support.v4.app.LoaderManager.LoaderCallbacks; import android.support.v4.app.NavUtils; import android.support.v4.app.TaskStackBuilder; -import android.support.v4.content.Loader; import android.support.v7.app.ActionBar; import android.view.MenuItem; import android.widget.Toast; import be.digitalia.fosdem.R; -import be.digitalia.fosdem.db.DatabaseManager; import be.digitalia.fosdem.fragments.EventDetailsFragment; -import be.digitalia.fosdem.loaders.LocalCacheLoader; import be.digitalia.fosdem.model.Event; import be.digitalia.fosdem.utils.NfcUtils; import be.digitalia.fosdem.utils.NfcUtils.CreateNfcAppDataCallback; import be.digitalia.fosdem.utils.ThemeUtils; +import be.digitalia.fosdem.viewmodels.EventViewModel; /** * Displays a single event passed either as a complete Parcelable object in extras or as an id in data. * * @author Christophe Beyls */ -public class EventDetailsActivity extends BaseActivity implements LoaderCallbacks, CreateNfcAppDataCallback { +public class EventDetailsActivity extends BaseActivity implements Observer, CreateNfcAppDataCallback { public static final String EXTRA_EVENT = "event"; - private static final int EVENT_LOADER_ID = 1; - private Event event; @Override @@ -56,7 +53,38 @@ public class EventDetailsActivity extends BaseActivity implements LoaderCallback } } else { // Load the event from the DB using its id - getSupportLoaderManager().initLoader(EVENT_LOADER_ID, null, this); + EventViewModel viewModel = ViewModelProviders.of(this).get(EventViewModel.class); + if (!viewModel.hasEventId()) { + Intent intent = getIntent(); + String eventIdString; + if (NfcUtils.hasAppData(intent)) { + // NFC intent + eventIdString = new String(NfcUtils.extractAppData(intent)); + } else { + // Normal in-app intent + eventIdString = intent.getDataString(); + } + viewModel.setEventId(Long.parseLong(eventIdString)); + } + viewModel.getEvent().observe(this, this); + } + } + + @Override + public void onChanged(@Nullable Event event) { + if (event == null) { + // Event not found, quit + Toast.makeText(this, getString(R.string.event_not_found_error), Toast.LENGTH_LONG).show(); + finish(); + return; + } + + initEvent(event); + + FragmentManager fm = getSupportFragmentManager(); + if (fm.findFragmentById(R.id.content) == null) { + Fragment f = EventDetailsFragment.newInstance(event); + fm.beginTransaction().add(R.id.content, f).commitAllowingStateLoss(); } } @@ -104,55 +132,4 @@ public class EventDetailsActivity extends BaseActivity implements LoaderCallback public byte[] createNfcAppData() { return String.valueOf(event.getId()).getBytes(); } - - private static class EventLoader extends LocalCacheLoader { - - private final long eventId; - - public EventLoader(Context context, long eventId) { - super(context); - this.eventId = eventId; - } - - @Override - public Event loadInBackground() { - return DatabaseManager.getInstance().getEvent(eventId); - } - } - - @Override - public Loader onCreateLoader(int id, Bundle args) { - Intent intent = getIntent(); - String eventIdString; - if (NfcUtils.hasAppData(intent)) { - // NFC intent - eventIdString = new String(NfcUtils.extractAppData(intent)); - } else { - // Normal in-app intent - eventIdString = intent.getDataString(); - } - return new EventLoader(this, Long.parseLong(eventIdString)); - } - - @Override - public void onLoadFinished(Loader loader, Event data) { - if (data == null) { - // Event not found, quit - Toast.makeText(this, getString(R.string.event_not_found_error), Toast.LENGTH_LONG).show(); - finish(); - return; - } - - initEvent(data); - - FragmentManager fm = getSupportFragmentManager(); - if (fm.findFragmentById(R.id.content) == null) { - Fragment f = EventDetailsFragment.newInstance(data); - fm.beginTransaction().add(R.id.content, f).commitAllowingStateLoss(); - } - } - - @Override - public void onLoaderReset(Loader loader) { - } } diff --git a/app/src/main/java/be/digitalia/fosdem/db/DatabaseManager.java b/app/src/main/java/be/digitalia/fosdem/db/DatabaseManager.java index 1a55507..c30d0a4 100644 --- a/app/src/main/java/be/digitalia/fosdem/db/DatabaseManager.java +++ b/app/src/main/java/be/digitalia/fosdem/db/DatabaseManager.java @@ -1,6 +1,7 @@ package be.digitalia.fosdem.db; import android.app.SearchManager; +import android.arch.lifecycle.LiveData; import android.content.ContentValues; import android.content.Context; import android.content.Intent; @@ -11,11 +12,14 @@ import android.database.sqlite.SQLiteConstraintException; import android.database.sqlite.SQLiteDatabase; import android.database.sqlite.SQLiteStatement; import android.provider.BaseColumns; +import android.support.annotation.Nullable; +import android.support.annotation.WorkerThread; import android.support.v4.content.LocalBroadcastManager; import android.text.TextUtils; import java.util.ArrayList; import java.util.Calendar; +import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.HashSet; @@ -25,6 +29,7 @@ import java.util.Map; import java.util.Set; import be.digitalia.fosdem.BuildConfig; +import be.digitalia.fosdem.livedata.AsyncTaskLiveData; import be.digitalia.fosdem.model.Day; import be.digitalia.fosdem.model.Event; import be.digitalia.fosdem.model.Link; @@ -55,7 +60,6 @@ public class DatabaseManager { private final Context context; private final DatabaseHelper helper; - private List cachedDays; private int year = -1; public static void init(Context context) { @@ -128,8 +132,10 @@ public class DatabaseManager { * @param events * @return The number of events processed. */ + @WorkerThread public int storeSchedule(Iterable events, String lastModifiedTag) { boolean isComplete = false; + List daysList = null; SQLiteDatabase db = helper.getWritableDatabase(); db.beginTransaction(); @@ -245,6 +251,8 @@ public class DatabaseManager { values.put("date", (date == null) ? 0L : date.getTime()); db.insert(DatabaseHelper.DAYS_TABLE_NAME, null, values); } + daysList = new ArrayList<>(days); + Collections.sort(daysList); // 4: Purge outdated bookmarks if (minEventId < Long.MAX_VALUE) { @@ -262,8 +270,8 @@ public class DatabaseManager { db.endTransaction(); if (isComplete) { - // Clear cache - cachedDays = null; + // Update/clear cache + daysLiveData.postValue(daysList); year = -1; // Set last update time and server's last modified tag getSharedPreferences().edit() @@ -276,6 +284,7 @@ public class DatabaseManager { } } + @WorkerThread public void clearSchedule() { SQLiteDatabase db = helper.getWritableDatabase(); db.beginTransaction(); @@ -284,7 +293,7 @@ public class DatabaseManager { db.setTransactionSuccessful(); - cachedDays = null; + daysLiveData.postValue(Collections.emptyList()); year = -1; getSharedPreferences().edit() .remove(LAST_UPDATE_TIME_PREF) @@ -306,34 +315,36 @@ public class DatabaseManager { db.delete(DatabaseHelper.DAYS_TABLE_NAME, null, null); } - /** - * Returns the cached days list or null. Can be safely called on the main thread without blocking it. - * - * @return - */ - public List getCachedDays() { - return cachedDays; - } + private final AsyncTaskLiveData> daysLiveData = new AsyncTaskLiveData>() { + + { + onContentChanged(); + } + + @Override + protected List loadInBackground() throws Exception { + Cursor cursor = helper.getReadableDatabase().query(DatabaseHelper.DAYS_TABLE_NAME, + new String[]{"_index", "date"}, null, null, null, null, "_index ASC"); + try { + List result = new ArrayList<>(cursor.getCount()); + while (cursor.moveToNext()) { + Day day = new Day(); + day.setIndex(cursor.getInt(0)); + day.setDate(new Date(cursor.getLong(1))); + result.add(day); + } + return result; + } finally { + cursor.close(); + } + } + }; /** * @return The Days the events span to. */ - public List getDays() { - Cursor cursor = helper.getReadableDatabase().query(DatabaseHelper.DAYS_TABLE_NAME, new String[]{"_index", "date"}, null, null, null, null, - "_index ASC"); - try { - List result = new ArrayList<>(cursor.getCount()); - while (cursor.moveToNext()) { - Day day = new Day(); - day.setIndex(cursor.getInt(0)); - day.setDate(new Date(cursor.getLong(1))); - result.add(day); - } - cachedDays = result; - return result; - } finally { - cursor.close(); - } + public LiveData> getDays() { + return daysLiveData; } public int getYear() { @@ -344,10 +355,11 @@ public class DatabaseManager { Calendar cal = Calendar.getInstance(DateUtils.getBelgiumTimeZone(), Locale.US); - // Compute from cachedDays if available - if (cachedDays != null) { - if (cachedDays.size() > 0) { - cal.setTime(cachedDays.get(0).getDate()); + // Compute from cached days if available + List days = daysLiveData.getValue(); + if (days != null) { + if (days.size() > 0) { + cal.setTime(days.get(0).getDate()); } } else { // Perform a quick DB query to retrieve the time of the first day @@ -367,6 +379,7 @@ public class DatabaseManager { return year; } + @WorkerThread public Cursor getTracks(Day day) { String[] selectionArgs = new String[]{String.valueOf(day.getIndex())}; Cursor cursor = helper.getReadableDatabase().rawQuery( @@ -392,13 +405,16 @@ public class DatabaseManager { return toTrack(cursor, null); } + @WorkerThread public long getEventsCount() { return queryNumEntries(helper.getReadableDatabase(), DatabaseHelper.EVENTS_TABLE_NAME, null, null); } /** - * Returns the event with the specified id. + * Returns the event with the specified id, or null if not found. */ + @WorkerThread + @Nullable public Event getEvent(long id) { String[] selectionArgs = new String[]{String.valueOf(id)}; Cursor cursor = helper.getReadableDatabase().rawQuery( @@ -436,6 +452,7 @@ public class DatabaseManager { * @param track * @return A cursor to Events */ + @WorkerThread public Cursor getEvents(Day day, Track track) { String[] selectionArgs = new String[]{String.valueOf(day.getIndex()), track.getName(), track.getType().name()}; Cursor cursor = helper.getReadableDatabase().rawQuery( @@ -462,6 +479,7 @@ public class DatabaseManager { * @param ascending If true, order results from start time ascending, else order from start time descending * @return */ + @WorkerThread public Cursor getEvents(long minStartTime, long maxStartTime, long minEndTime, boolean ascending) { ArrayList selectionArgs = new ArrayList<>(3); StringBuilder whereCondition = new StringBuilder(); @@ -511,6 +529,7 @@ public class DatabaseManager { * @param person * @return A cursor to Events */ + @WorkerThread public Cursor getEvents(Person person) { String[] selectionArgs = new String[]{String.valueOf(person.getId())}; Cursor cursor = helper.getReadableDatabase().rawQuery( @@ -535,6 +554,7 @@ public class DatabaseManager { * @param minStartTime When positive, only return the events starting after this time. * @return A cursor to Events */ + @WorkerThread public Cursor getBookmarks(long minStartTime) { String whereCondition; String[] selectionArgs; @@ -568,6 +588,7 @@ public class DatabaseManager { * @param query * @return A cursor to Events */ + @WorkerThread public Cursor getSearchResults(String query) { final String matchQuery = query + "*"; String[] selectionArgs = new String[]{matchQuery, "%" + query + "%", matchQuery}; @@ -603,6 +624,7 @@ public class DatabaseManager { /** * Method called by SearchSuggestionProvider to return search results in the format expected by the search framework. */ + @WorkerThread public Cursor getSearchSuggestionResults(String query, int limit) { final String matchQuery = query + "*"; String[] selectionArgs = new String[]{matchQuery, "%" + query + "%", matchQuery, String.valueOf(limit)}; @@ -720,6 +742,7 @@ public class DatabaseManager { /** * Returns all persons in alphabetical order. */ + @WorkerThread public Cursor getPersons() { Cursor cursor = helper.getReadableDatabase().rawQuery( "SELECT rowid AS _id, name" @@ -733,6 +756,7 @@ public class DatabaseManager { /** * Returns persons presenting the specified event. */ + @WorkerThread public List getPersons(Event event) { String[] selectionArgs = new String[]{String.valueOf(event.getId())}; Cursor cursor = helper.getReadableDatabase().rawQuery( @@ -765,6 +789,7 @@ public class DatabaseManager { return toPerson(cursor, null); } + @WorkerThread public List getLinks(Event event) { String[] selectionArgs = new String[]{String.valueOf(event.getId())}; Cursor cursor = helper.getReadableDatabase().rawQuery( @@ -786,11 +811,13 @@ public class DatabaseManager { } } + @WorkerThread public boolean isBookmarked(Event event) { String[] selectionArgs = new String[]{String.valueOf(event.getId())}; return queryNumEntries(helper.getReadableDatabase(), DatabaseHelper.BOOKMARKS_TABLE_NAME, "event_id = ?", selectionArgs) > 0L; } + @WorkerThread public boolean addBookmark(Event event) { boolean complete = false; @@ -823,14 +850,17 @@ public class DatabaseManager { } } + @WorkerThread public boolean removeBookmark(Event event) { return removeBookmarks(new long[]{event.getId()}); } + @WorkerThread public boolean removeBookmark(long eventId) { return removeBookmarks(new long[]{eventId}); } + @WorkerThread public boolean removeBookmarks(long[] eventIds) { int length = eventIds.length; if (length == 0) { diff --git a/app/src/main/java/be/digitalia/fosdem/fragments/EventDetailsFragment.java b/app/src/main/java/be/digitalia/fosdem/fragments/EventDetailsFragment.java index 1ad3b73..2b8e926 100644 --- a/app/src/main/java/be/digitalia/fosdem/fragments/EventDetailsFragment.java +++ b/app/src/main/java/be/digitalia/fosdem/fragments/EventDetailsFragment.java @@ -2,22 +2,21 @@ package be.digitalia.fosdem.fragments; import android.annotation.SuppressLint; import android.app.Activity; +import android.arch.lifecycle.Observer; +import android.arch.lifecycle.ViewModelProviders; import android.content.ActivityNotFoundException; import android.content.Context; import android.content.Intent; import android.graphics.drawable.Animatable; import android.net.Uri; -import android.os.AsyncTask; import android.os.Bundle; import android.provider.CalendarContract; import android.support.annotation.NonNull; +import android.support.annotation.Nullable; import android.support.customtabs.CustomTabsIntent; import android.support.v4.app.Fragment; -import android.support.v4.app.LoaderManager; -import android.support.v4.app.LoaderManager.LoaderCallbacks; import android.support.v4.app.ShareCompat; import android.support.v4.content.ContextCompat; -import android.support.v4.content.Loader; import android.text.Spannable; import android.text.SpannableString; import android.text.SpannableStringBuilder; @@ -38,19 +37,16 @@ import android.widget.Toast; import java.text.DateFormat; import java.util.Date; -import java.util.List; import be.digitalia.fosdem.R; import be.digitalia.fosdem.activities.PersonInfoActivity; -import be.digitalia.fosdem.db.DatabaseManager; -import be.digitalia.fosdem.loaders.BookmarkStatusLoader; -import be.digitalia.fosdem.loaders.LocalCacheLoader; import be.digitalia.fosdem.model.Building; import be.digitalia.fosdem.model.Event; import be.digitalia.fosdem.model.Link; import be.digitalia.fosdem.model.Person; import be.digitalia.fosdem.utils.DateUtils; import be.digitalia.fosdem.utils.StringUtils; +import be.digitalia.fosdem.viewmodels.EventDetailsViewModel; public class EventDetailsFragment extends Fragment { @@ -62,11 +58,6 @@ public class EventDetailsFragment extends Fragment { ImageView getActionButton(); } - static class EventDetails { - List persons; - List links; - } - static class ViewHolder { LayoutInflater inflater; TextView personsTextView; @@ -74,15 +65,12 @@ public class EventDetailsFragment extends Fragment { ViewGroup linksContainer; } - private static final int BOOKMARK_STATUS_LOADER_ID = 1; - private static final int EVENT_DETAILS_LOADER_ID = 2; - private static final String ARG_EVENT = "event"; Event event; int personsCount = 1; - Boolean isBookmarked; ViewHolder holder; + EventDetailsViewModel viewModel; private MenuItem bookmarkMenuItem; private ImageView actionButton; @@ -99,6 +87,8 @@ public class EventDetailsFragment extends Fragment { public void onCreate(Bundle savedInstanceState) { super.onCreate(savedInstanceState); event = getArguments().getParcelable(ARG_EVENT); + viewModel = ViewModelProviders.of(this).get(EventDetailsViewModel.class); + viewModel.setEvent(event); } public Event getEvent() { @@ -212,18 +202,25 @@ public class EventDetailsFragment extends Fragment { // Ensure the actionButton is initialized before creating the options menu setHasOptionsMenu(true); - LoaderManager loaderManager = getLoaderManager(); - loaderManager.initLoader(BOOKMARK_STATUS_LOADER_ID, null, bookmarkStatusLoaderCallbacks); - loaderManager.initLoader(EVENT_DETAILS_LOADER_ID, null, eventDetailsLoaderCallbacks); + viewModel.getBookmarkStatus().observe(this, new Observer() { + @Override + public void onChanged(@Nullable Boolean isBookmarked) { + updateBookmarkMenuItem(isBookmarked, true); + } + }); + viewModel.getEventDetails().observe(this, new Observer() { + @Override + public void onChanged(@Nullable EventDetailsViewModel.EventDetails eventDetails) { + setEventDetails(eventDetails); + } + }); } private final View.OnClickListener actionButtonClickListener = new View.OnClickListener() { @Override public void onClick(View view) { - if (isBookmarked != null) { - new UpdateBookmarkAsyncTask(event).execute(isBookmarked); - } + viewModel.toggleBookmarkStatus(); } }; @@ -246,7 +243,7 @@ public class EventDetailsFragment extends Fragment { if (actionButton != null) { bookmarkMenuItem.setEnabled(false).setVisible(false); } - updateBookmarkMenuItem(false); + updateBookmarkMenuItem(viewModel.getBookmarkStatus().getValue(), false); } private Intent getShareChooserIntent() { @@ -258,7 +255,7 @@ public class EventDetailsFragment extends Fragment { .createChooserIntent(); } - void updateBookmarkMenuItem(boolean animate) { + void updateBookmarkMenuItem(Boolean isBookmarked, boolean animate) { if (actionButton != null) { // Action Button is used as bookmark button @@ -317,9 +314,7 @@ public class EventDetailsFragment extends Fragment { public boolean onOptionsItemSelected(MenuItem item) { switch (item.getItemId()) { case R.id.bookmark: - if (isBookmarked != null) { - new UpdateBookmarkAsyncTask(event).execute(isBookmarked); - } + viewModel.toggleBookmarkStatus(); return true; case R.id.add_to_agenda: addToAgenda(); @@ -328,25 +323,6 @@ public class EventDetailsFragment extends Fragment { return false; } - private static class UpdateBookmarkAsyncTask extends AsyncTask { - - private final Event event; - - public UpdateBookmarkAsyncTask(Event event) { - this.event = event; - } - - @Override - protected Void doInBackground(Boolean... remove) { - if (remove[0]) { - DatabaseManager.getInstance().removeBookmark(event); - } else { - DatabaseManager.getInstance().addBookmark(event); - } - return null; - } - } - @SuppressLint("InlinedApi") private void addToAgenda() { Intent intent = new Intent(Intent.ACTION_EDIT); @@ -379,98 +355,46 @@ public class EventDetailsFragment extends Fragment { } } - private final LoaderCallbacks bookmarkStatusLoaderCallbacks = new LoaderCallbacks() { - - @Override - public Loader onCreateLoader(int id, Bundle args) { - return new BookmarkStatusLoader(getActivity(), event); - } - - @Override - public void onLoadFinished(Loader loader, Boolean data) { - if (isBookmarked != data) { - isBookmarked = data; - updateBookmarkMenuItem(true); + void setEventDetails(@NonNull EventDetailsViewModel.EventDetails data) { + // 1. Persons + if (data.persons != null) { + personsCount = data.persons.size(); + if (personsCount > 0) { + // Build a list of clickable persons + SpannableStringBuilder sb = new SpannableStringBuilder(); + int length = 0; + for (Person person : data.persons) { + if (length != 0) { + sb.append(", "); + } + String name = person.getName(); + sb.append(name); + length = sb.length(); + sb.setSpan(new PersonClickableSpan(person), length - name.length(), length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + } + holder.personsTextView.setText(sb); + holder.personsTextView.setVisibility(View.VISIBLE); } } - @Override - public void onLoaderReset(Loader loader) { - } - }; - - private static class EventDetailsLoader extends LocalCacheLoader { - - private final Event event; - - public EventDetailsLoader(Context context, Event event) { - super(context); - this.event = event; - } - - @Override - public EventDetails loadInBackground() { - EventDetails result = new EventDetails(); - DatabaseManager dbm = DatabaseManager.getInstance(); - result.persons = dbm.getPersons(event); - result.links = dbm.getLinks(event); - return result; + // 2. Links + holder.linksContainer.removeAllViews(); + if ((data.links != null) && (data.links.size() > 0)) { + holder.linksHeader.setVisibility(View.VISIBLE); + holder.linksContainer.setVisibility(View.VISIBLE); + for (Link link : data.links) { + View view = holder.inflater.inflate(R.layout.item_link, holder.linksContainer, false); + TextView tv = view.findViewById(R.id.description); + tv.setText(link.getDescription()); + view.setOnClickListener(new LinkClickListener(link)); + holder.linksContainer.addView(view); + } + } else { + holder.linksHeader.setVisibility(View.GONE); + holder.linksContainer.setVisibility(View.GONE); } } - private final LoaderCallbacks eventDetailsLoaderCallbacks = new LoaderCallbacks() { - - @Override - public Loader onCreateLoader(int id, Bundle args) { - return new EventDetailsLoader(getActivity(), event); - } - - @Override - public void onLoadFinished(Loader loader, EventDetails data) { - // 1. Persons - if (data.persons != null) { - personsCount = data.persons.size(); - if (personsCount > 0) { - // Build a list of clickable persons - SpannableStringBuilder sb = new SpannableStringBuilder(); - int length = 0; - for (Person person : data.persons) { - if (length != 0) { - sb.append(", "); - } - String name = person.getName(); - sb.append(name); - length = sb.length(); - sb.setSpan(new PersonClickableSpan(person), length - name.length(), length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); - } - holder.personsTextView.setText(sb); - holder.personsTextView.setVisibility(View.VISIBLE); - } - } - - // 2. Links - holder.linksContainer.removeAllViews(); - if ((data.links != null) && (data.links.size() > 0)) { - holder.linksHeader.setVisibility(View.VISIBLE); - holder.linksContainer.setVisibility(View.VISIBLE); - for (Link link : data.links) { - View view = holder.inflater.inflate(R.layout.item_link, holder.linksContainer, false); - TextView tv = view.findViewById(R.id.description); - tv.setText(link.getDescription()); - view.setOnClickListener(new LinkClickListener(link)); - holder.linksContainer.addView(view); - } - } else { - holder.linksHeader.setVisibility(View.GONE); - holder.linksContainer.setVisibility(View.GONE); - } - } - - @Override - public void onLoaderReset(Loader loader) { - } - }; - private static class PersonClickableSpan extends ClickableSpan { private final Person person; diff --git a/app/src/main/java/be/digitalia/fosdem/fragments/TracksFragment.java b/app/src/main/java/be/digitalia/fosdem/fragments/TracksFragment.java index a604131..46aea50 100644 --- a/app/src/main/java/be/digitalia/fosdem/fragments/TracksFragment.java +++ b/app/src/main/java/be/digitalia/fosdem/fragments/TracksFragment.java @@ -1,18 +1,16 @@ package be.digitalia.fosdem.fragments; -import android.content.BroadcastReceiver; +import android.arch.lifecycle.LiveData; +import android.arch.lifecycle.Observer; import android.content.Context; -import android.content.Intent; -import android.content.IntentFilter; import android.content.SharedPreferences; import android.os.Bundle; import android.support.annotation.NonNull; +import android.support.annotation.Nullable; import android.support.v4.app.Fragment; import android.support.v4.app.FragmentManager; import android.support.v4.app.FragmentStatePagerAdapter; -import android.support.v4.app.LoaderManager.LoaderCallbacks; import android.support.v4.content.Loader; -import android.support.v4.content.LocalBroadcastManager; import android.support.v4.view.ViewPager; import android.support.v7.widget.RecyclerView; import android.view.LayoutInflater; @@ -23,11 +21,10 @@ import java.util.List; import be.digitalia.fosdem.R; import be.digitalia.fosdem.db.DatabaseManager; -import be.digitalia.fosdem.loaders.GlobalCacheLoader; import be.digitalia.fosdem.model.Day; import be.digitalia.fosdem.widgets.SlidingTabLayout; -public class TracksFragment extends Fragment implements RecycledViewPoolProvider, LoaderCallbacks> { +public class TracksFragment extends Fragment implements RecycledViewPoolProvider, Observer> { static class ViewHolder { View contentView; @@ -38,7 +35,6 @@ public class TracksFragment extends Fragment implements RecycledViewPoolProvider RecyclerView.RecycledViewPool recycledViewPool; } - private static final int DAYS_LOADER_ID = 1; private static final String PREF_CURRENT_PAGE = "tracks_current_page"; private ViewHolder holder; @@ -79,7 +75,9 @@ public class TracksFragment extends Fragment implements RecycledViewPoolProvider public void onActivityCreated(Bundle savedInstanceState) { super.onActivityCreated(savedInstanceState); - getLoaderManager().initLoader(DAYS_LOADER_ID, null, this); + LiveData> daysLiveData = DatabaseManager.getInstance().getDays(); + daysLiveData.removeObserver(this); + daysLiveData.observe(this, this); } @Override @@ -100,48 +98,9 @@ public class TracksFragment extends Fragment implements RecycledViewPoolProvider return (holder == null) ? null : holder.recycledViewPool; } - private static class DaysLoader extends GlobalCacheLoader> { - - private final BroadcastReceiver scheduleRefreshedReceiver = new BroadcastReceiver() { - - @Override - public void onReceive(Context context, Intent intent) { - onContentChanged(); - } - }; - - public DaysLoader(Context context) { - super(context); - // Reload days list when the schedule has been refreshed - LocalBroadcastManager.getInstance(context).registerReceiver(scheduleRefreshedReceiver, - new IntentFilter(DatabaseManager.ACTION_SCHEDULE_REFRESHED)); - } - - @Override - protected void onReset() { - super.onReset(); - LocalBroadcastManager.getInstance(getContext()).unregisterReceiver(scheduleRefreshedReceiver); - } - - @Override - protected List getCachedResult() { - return DatabaseManager.getInstance().getCachedDays(); - } - - @Override - public List loadInBackground() { - return DatabaseManager.getInstance().getDays(); - } - } - @Override - public Loader> onCreateLoader(int id, Bundle args) { - return new DaysLoader(getActivity()); - } - - @Override - public void onLoadFinished(Loader> loader, List data) { - holder.daysAdapter.setDays(data); + public void onChanged(@Nullable List days) { + holder.daysAdapter.setDays(days); final int totalPages = holder.daysAdapter.getCount(); if (totalPages == 0) { @@ -161,10 +120,6 @@ public class TracksFragment extends Fragment implements RecycledViewPoolProvider } } - @Override - public void onLoaderReset(Loader> loader) { - } - private static class DaysAdapter extends FragmentStatePagerAdapter { private List days; diff --git a/app/src/main/java/be/digitalia/fosdem/livedata/AsyncTaskLiveData.java b/app/src/main/java/be/digitalia/fosdem/livedata/AsyncTaskLiveData.java new file mode 100644 index 0000000..9c585df --- /dev/null +++ b/app/src/main/java/be/digitalia/fosdem/livedata/AsyncTaskLiveData.java @@ -0,0 +1,99 @@ +package be.digitalia.fosdem.livedata; + +import android.annotation.SuppressLint; +import android.arch.lifecycle.MutableLiveData; +import android.os.AsyncTask; +import android.support.annotation.MainThread; +import android.support.annotation.WorkerThread; + +/** + * A LiveData implementation with the same basic functionality as AsyncTaskLoader. + */ +public abstract class AsyncTaskLiveData extends MutableLiveData { + + private boolean contentChanged = false; + AsyncTask task = null; + + @MainThread + public void onContentChanged() { + if (hasActiveObservers()) { + forceLoad(); + } else { + contentChanged = true; + } + } + + @MainThread + public boolean cancelLoad() { + if (task != null) { + boolean result = task.cancel(false); + task = null; + return result; + } + return false; + } + + @Override + protected void onActive() { + if (contentChanged) { + contentChanged = false; + forceLoad(); + } + } + + @Override + public void setValue(T value) { + // Setting a value will cancel any pending AsyncTask + contentChanged = false; + cancelLoad(); + super.setValue(value); + } + + @MainThread + @SuppressLint("StaticFieldLeak") + public void forceLoad() { + cancelLoad(); + task = new AsyncTask() { + + private Throwable error; + + @Override + protected T doInBackground(Void... voids) { + try { + return loadInBackground(); + } catch (Throwable e) { + error = e; + return null; + } + } + + @Override + protected void onPostExecute(T result) { + task = null; + if (error == null) { + onSuccess(result); + } else { + onError(error); + } + } + }; + task.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); + } + + + @WorkerThread + protected abstract T loadInBackground() throws Exception; + + @MainThread + protected void onSuccess(T result) { + setValue(result); + } + + /** + * Override this method for custom error handling. + */ + @MainThread + protected void onError(Throwable error) { + error.printStackTrace(); + } +} diff --git a/app/src/main/java/be/digitalia/fosdem/loaders/BookmarkStatusLoader.java b/app/src/main/java/be/digitalia/fosdem/loaders/BookmarkStatusLoader.java deleted file mode 100644 index 6535eb5..0000000 --- a/app/src/main/java/be/digitalia/fosdem/loaders/BookmarkStatusLoader.java +++ /dev/null @@ -1,105 +0,0 @@ -package be.digitalia.fosdem.loaders; - -import be.digitalia.fosdem.db.DatabaseManager; -import be.digitalia.fosdem.model.Event; -import be.digitalia.fosdem.utils.ArrayUtils; -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; -import android.content.IntentFilter; -import android.support.v4.content.AsyncTaskLoader; -import android.support.v4.content.LocalBroadcastManager; - -/** - * This loader retrieves the bookmark status of an event from the database, then updates it in real time by listening to broadcasts. - * - * @author Christophe Beyls - * - */ -public class BookmarkStatusLoader extends AsyncTaskLoader { - - final Event event; - private Boolean isBookmarked; - - private final BroadcastReceiver addBookmarkReceiver = new BroadcastReceiver() { - - @Override - public void onReceive(Context context, Intent intent) { - if (event.getId() == intent.getLongExtra(DatabaseManager.EXTRA_EVENT_ID, -1L)) { - updateBookmark(true); - } - } - }; - - private final BroadcastReceiver removeBookmarksReceiver = new BroadcastReceiver() { - - @Override - public void onReceive(Context context, Intent intent) { - long[] eventIds = intent.getLongArrayExtra(DatabaseManager.EXTRA_EVENT_IDS); - if (ArrayUtils.indexOf(eventIds, event.getId()) != -1) { - updateBookmark(false); - } - } - }; - - public BookmarkStatusLoader(Context context, Event event) { - super(context); - this.event = event; - - LocalBroadcastManager lbm = LocalBroadcastManager.getInstance(getContext()); - lbm.registerReceiver(addBookmarkReceiver, new IntentFilter(DatabaseManager.ACTION_ADD_BOOKMARK)); - lbm.registerReceiver(removeBookmarksReceiver, new IntentFilter(DatabaseManager.ACTION_REMOVE_BOOKMARKS)); - } - - void updateBookmark(Boolean result) { - if (isStarted()) { - cancelLoad(); - } - deliverResult(result); - } - - @Override - protected void onStartLoading() { - if (isBookmarked != null) { - // If we currently have a result available, deliver it - // immediately. - super.deliverResult(isBookmarked); - } else { - forceLoad(); - } - } - - @Override - protected void onStopLoading() { - // Attempt to cancel the current load task if possible. - cancelLoad(); - } - - @Override - protected void onReset() { - super.onReset(); - - onStopLoading(); - isBookmarked = null; - - LocalBroadcastManager lbm = LocalBroadcastManager.getInstance(getContext()); - lbm.unregisterReceiver(addBookmarkReceiver); - lbm.unregisterReceiver(removeBookmarksReceiver); - } - - @Override - public void deliverResult(Boolean data) { - isBookmarked = data; - - if (isStarted()) { - // If the Loader is currently started, we can immediately - // deliver its results. - super.deliverResult(data); - } - } - - @Override - public Boolean loadInBackground() { - return DatabaseManager.getInstance().isBookmarked(event); - } -} diff --git a/app/src/main/java/be/digitalia/fosdem/loaders/GlobalCacheLoader.java b/app/src/main/java/be/digitalia/fosdem/loaders/GlobalCacheLoader.java deleted file mode 100644 index 154559b..0000000 --- a/app/src/main/java/be/digitalia/fosdem/loaders/GlobalCacheLoader.java +++ /dev/null @@ -1,59 +0,0 @@ -package be.digitalia.fosdem.loaders; - -import android.content.Context; -import android.support.v4.content.AsyncTaskLoader; - -/** - * A Loader working with a global application cache instead of a local cache. - * This allows to avoid starting a background thread if the result is already in cache. - * You do NOT need to destroy this loader after the result has been delivered. - * The cache will be checked each time the fragment is started. - * - * @author Christophe Beyls - */ -public abstract class GlobalCacheLoader extends AsyncTaskLoader { - - public GlobalCacheLoader(Context context) { - super(context); - } - - @Override - protected void onStartLoading() { - T cachedResult = getCachedResult(); - if (cachedResult != null) { - // If we currently have a result available, deliver it - // immediately. - deliverResult(cachedResult); - } - - if (takeContentChanged() || cachedResult == null) { - // If the data has changed since the last time it was loaded - // or is not currently available, start a load. - forceLoad(); - } - } - - @Override - protected void onStopLoading() { - // Attempt to cancel the current load task if possible. - cancelLoad(); - } - - @Override - protected void onReset() { - super.onReset(); - - onStopLoading(); - } - - @Override - public void deliverResult(T data) { - if (isStarted()) { - // If the Loader is currently started, we can immediately - // deliver its results. - super.deliverResult(data); - } - } - - protected abstract T getCachedResult(); -} diff --git a/app/src/main/java/be/digitalia/fosdem/loaders/LocalCacheLoader.java b/app/src/main/java/be/digitalia/fosdem/loaders/LocalCacheLoader.java deleted file mode 100644 index 8e96a7c..0000000 --- a/app/src/main/java/be/digitalia/fosdem/loaders/LocalCacheLoader.java +++ /dev/null @@ -1,53 +0,0 @@ -package be.digitalia.fosdem.loaders; - -import android.content.Context; -import android.support.v4.content.AsyncTaskLoader; - -public abstract class LocalCacheLoader extends AsyncTaskLoader { - - private T mResult; - - public LocalCacheLoader(Context context) { - super(context); - } - - @Override - protected void onStartLoading() { - if (mResult != null) { - // If we currently have a result available, deliver it - // immediately. - deliverResult(mResult); - } - - if (takeContentChanged() || mResult == null) { - // If the data has changed since the last time it was loaded - // or is not currently available, start a load. - forceLoad(); - } - } - - @Override - protected void onStopLoading() { - // Attempt to cancel the current load task if possible. - cancelLoad(); - } - - @Override - protected void onReset() { - super.onReset(); - - onStopLoading(); - mResult = null; - } - - @Override - public void deliverResult(T data) { - mResult = data; - - if (isStarted()) { - // If the Loader is currently started, we can immediately - // deliver its results. - super.deliverResult(data); - } - } -} diff --git a/app/src/main/java/be/digitalia/fosdem/model/Day.java b/app/src/main/java/be/digitalia/fosdem/model/Day.java index accfaf5..5003bca 100644 --- a/app/src/main/java/be/digitalia/fosdem/model/Day.java +++ b/app/src/main/java/be/digitalia/fosdem/model/Day.java @@ -1,15 +1,17 @@ package be.digitalia.fosdem.model; +import android.os.Parcel; +import android.os.Parcelable; +import android.support.annotation.NonNull; + import java.text.DateFormat; import java.text.SimpleDateFormat; import java.util.Date; import java.util.Locale; -import android.os.Parcel; -import android.os.Parcelable; import be.digitalia.fosdem.utils.DateUtils; -public class Day implements Parcelable { +public class Day implements Comparable, Parcelable { private static final DateFormat DAY_DATE_FORMAT = DateUtils.withBelgiumTimeZone(new SimpleDateFormat("EEEE", Locale.US)); @@ -63,6 +65,11 @@ public class Day implements Parcelable { return (index == other.index); } + @Override + public int compareTo(@NonNull Day other) { + return index - other.index; + } + @Override public int describeContents() { return 0; diff --git a/app/src/main/java/be/digitalia/fosdem/viewmodels/EventDetailsViewModel.java b/app/src/main/java/be/digitalia/fosdem/viewmodels/EventDetailsViewModel.java new file mode 100644 index 0000000..363a769 --- /dev/null +++ b/app/src/main/java/be/digitalia/fosdem/viewmodels/EventDetailsViewModel.java @@ -0,0 +1,128 @@ +package be.digitalia.fosdem.viewmodels; + +import android.app.Application; +import android.arch.lifecycle.AndroidViewModel; +import android.arch.lifecycle.LiveData; +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.os.AsyncTask; +import android.support.annotation.NonNull; +import android.support.v4.content.LocalBroadcastManager; + +import java.util.List; + +import be.digitalia.fosdem.db.DatabaseManager; +import be.digitalia.fosdem.livedata.AsyncTaskLiveData; +import be.digitalia.fosdem.model.Event; +import be.digitalia.fosdem.model.Link; +import be.digitalia.fosdem.model.Person; +import be.digitalia.fosdem.utils.ArrayUtils; + +public class EventDetailsViewModel extends AndroidViewModel { + + public static class EventDetails { + public List persons; + public List links; + } + + private Event event = null; + + private final AsyncTaskLiveData bookmarkStatus = new AsyncTaskLiveData() { + + @Override + protected Boolean loadInBackground() throws Exception { + return DatabaseManager.getInstance().isBookmarked(event); + } + }; + private final AsyncTaskLiveData eventDetails = new AsyncTaskLiveData() { + + @Override + protected EventDetails loadInBackground() throws Exception { + EventDetails result = new EventDetails(); + DatabaseManager dbm = DatabaseManager.getInstance(); + result.persons = dbm.getPersons(event); + result.links = dbm.getLinks(event); + return result; + } + }; + + private final BroadcastReceiver addBookmarkReceiver = new BroadcastReceiver() { + + @Override + public void onReceive(Context context, Intent intent) { + if (event.getId() == intent.getLongExtra(DatabaseManager.EXTRA_EVENT_ID, -1L)) { + bookmarkStatus.setValue(true); + } + } + }; + private final BroadcastReceiver removeBookmarksReceiver = new BroadcastReceiver() { + + @Override + public void onReceive(Context context, Intent intent) { + long[] eventIds = intent.getLongArrayExtra(DatabaseManager.EXTRA_EVENT_IDS); + if (ArrayUtils.indexOf(eventIds, event.getId()) != -1) { + bookmarkStatus.setValue(false); + } + } + }; + + public EventDetailsViewModel(@NonNull Application application) { + super(application); + } + + public void setEvent(@NonNull Event event) { + if (this.event == null) { + this.event = event; + + bookmarkStatus.forceLoad(); + LocalBroadcastManager lbm = LocalBroadcastManager.getInstance(getApplication()); + lbm.registerReceiver(addBookmarkReceiver, new IntentFilter(DatabaseManager.ACTION_ADD_BOOKMARK)); + lbm.registerReceiver(removeBookmarksReceiver, new IntentFilter(DatabaseManager.ACTION_REMOVE_BOOKMARKS)); + + eventDetails.forceLoad(); + } + } + + public LiveData getBookmarkStatus() { + return bookmarkStatus; + } + + public void toggleBookmarkStatus() { + Boolean isBookmarked = bookmarkStatus.getValue(); + if (isBookmarked != null) { + new ToggleBookmarkAsyncTask(event).execute(isBookmarked); + } + } + + private static class ToggleBookmarkAsyncTask extends AsyncTask { + + private final Event event; + + public ToggleBookmarkAsyncTask(Event event) { + this.event = event; + } + + @Override + protected Void doInBackground(Boolean... remove) { + if (remove[0]) { + DatabaseManager.getInstance().removeBookmark(event); + } else { + DatabaseManager.getInstance().addBookmark(event); + } + return null; + } + } + + public LiveData getEventDetails() { + return eventDetails; + } + + @Override + protected void onCleared() { + LocalBroadcastManager lbm = LocalBroadcastManager.getInstance(getApplication()); + lbm.unregisterReceiver(addBookmarkReceiver); + lbm.unregisterReceiver(removeBookmarksReceiver); + } +} diff --git a/app/src/main/java/be/digitalia/fosdem/viewmodels/EventViewModel.java b/app/src/main/java/be/digitalia/fosdem/viewmodels/EventViewModel.java new file mode 100644 index 0000000..05a1c75 --- /dev/null +++ b/app/src/main/java/be/digitalia/fosdem/viewmodels/EventViewModel.java @@ -0,0 +1,35 @@ +package be.digitalia.fosdem.viewmodels; + +import android.arch.lifecycle.LiveData; +import android.arch.lifecycle.ViewModel; + +import be.digitalia.fosdem.db.DatabaseManager; +import be.digitalia.fosdem.livedata.AsyncTaskLiveData; +import be.digitalia.fosdem.model.Event; + +public class EventViewModel extends ViewModel { + + private long eventId = -1L; + + private final AsyncTaskLiveData event = new AsyncTaskLiveData() { + @Override + protected Event loadInBackground() throws Exception { + return DatabaseManager.getInstance().getEvent(eventId); + } + }; + + public boolean hasEventId() { + return this.eventId != -1L; + } + + public void setEventId(long eventId) { + if (this.eventId != eventId) { + this.eventId = eventId; + event.forceLoad(); + } + } + + public LiveData getEvent() { + return event; + } +}