1
0
Fork 0
mirror of https://github.com/MatomoCamp/matomocamp-companion-android.git synced 2024-09-19 16:13:46 +02:00

Migrate all non-Cursor Loaders code to LiveData & ViewModel

This commit is contained in:
Christophe Beyls 2018-01-27 00:00:05 +01:00
parent 9b39c73615
commit d7ddcc4ad7
12 changed files with 439 additions and 500 deletions

View file

@ -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'
}

View file

@ -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<Event>, CreateNfcAppDataCallback {
public class EventDetailsActivity extends BaseActivity implements Observer<Event>, 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<Event> {
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<Event> 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<Event> 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<Event> loader) {
}
}

View file

@ -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<Day> 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<Event> events, String lastModifiedTag) {
boolean isComplete = false;
List<Day> 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.<Day>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<Day> getCachedDays() {
return cachedDays;
}
private final AsyncTaskLiveData<List<Day>> daysLiveData = new AsyncTaskLiveData<List<Day>>() {
{
onContentChanged();
}
@Override
protected List<Day> loadInBackground() throws Exception {
Cursor cursor = helper.getReadableDatabase().query(DatabaseHelper.DAYS_TABLE_NAME,
new String[]{"_index", "date"}, null, null, null, null, "_index ASC");
try {
List<Day> 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<Day> getDays() {
Cursor cursor = helper.getReadableDatabase().query(DatabaseHelper.DAYS_TABLE_NAME, new String[]{"_index", "date"}, null, null, null, null,
"_index ASC");
try {
List<Day> 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<List<Day>> 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<Day> 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<String> 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<Person> 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<Link> 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) {

View file

@ -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<Person> persons;
List<Link> 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<Boolean>() {
@Override
public void onChanged(@Nullable Boolean isBookmarked) {
updateBookmarkMenuItem(isBookmarked, true);
}
});
viewModel.getEventDetails().observe(this, new Observer<EventDetailsViewModel.EventDetails>() {
@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<Boolean, Void, Void> {
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<Boolean> bookmarkStatusLoaderCallbacks = new LoaderCallbacks<Boolean>() {
@Override
public Loader<Boolean> onCreateLoader(int id, Bundle args) {
return new BookmarkStatusLoader(getActivity(), event);
}
@Override
public void onLoadFinished(Loader<Boolean> 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<Boolean> loader) {
}
};
private static class EventDetailsLoader extends LocalCacheLoader<EventDetails> {
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<EventDetails> eventDetailsLoaderCallbacks = new LoaderCallbacks<EventDetails>() {
@Override
public Loader<EventDetails> onCreateLoader(int id, Bundle args) {
return new EventDetailsLoader(getActivity(), event);
}
@Override
public void onLoadFinished(Loader<EventDetails> 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<EventDetails> loader) {
}
};
private static class PersonClickableSpan extends ClickableSpan {
private final Person person;

View file

@ -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<List<Day>> {
public class TracksFragment extends Fragment implements RecycledViewPoolProvider, Observer<List<Day>> {
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<List<Day>> 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<List<Day>> {
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<Day> getCachedResult() {
return DatabaseManager.getInstance().getCachedDays();
}
@Override
public List<Day> loadInBackground() {
return DatabaseManager.getInstance().getDays();
}
}
@Override
public Loader<List<Day>> onCreateLoader(int id, Bundle args) {
return new DaysLoader(getActivity());
}
@Override
public void onLoadFinished(Loader<List<Day>> loader, List<Day> data) {
holder.daysAdapter.setDays(data);
public void onChanged(@Nullable List<Day> 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<List<Day>> loader) {
}
private static class DaysAdapter extends FragmentStatePagerAdapter {
private List<Day> days;

View file

@ -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<T> extends MutableLiveData<T> {
private boolean contentChanged = false;
AsyncTask<Void, Void, T> 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<Void, Void, T>() {
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();
}
}

View file

@ -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<Boolean> {
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);
}
}

View file

@ -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<T> extends AsyncTaskLoader<T> {
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();
}

View file

@ -1,53 +0,0 @@
package be.digitalia.fosdem.loaders;
import android.content.Context;
import android.support.v4.content.AsyncTaskLoader;
public abstract class LocalCacheLoader<T> extends AsyncTaskLoader<T> {
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);
}
}
}

View file

@ -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<Day>, 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;

View file

@ -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<Person> persons;
public List<Link> links;
}
private Event event = null;
private final AsyncTaskLiveData<Boolean> bookmarkStatus = new AsyncTaskLiveData<Boolean>() {
@Override
protected Boolean loadInBackground() throws Exception {
return DatabaseManager.getInstance().isBookmarked(event);
}
};
private final AsyncTaskLiveData<EventDetails> eventDetails = new AsyncTaskLiveData<EventDetails>() {
@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<Boolean> getBookmarkStatus() {
return bookmarkStatus;
}
public void toggleBookmarkStatus() {
Boolean isBookmarked = bookmarkStatus.getValue();
if (isBookmarked != null) {
new ToggleBookmarkAsyncTask(event).execute(isBookmarked);
}
}
private static class ToggleBookmarkAsyncTask extends AsyncTask<Boolean, Void, Void> {
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<EventDetails> getEventDetails() {
return eventDetails;
}
@Override
protected void onCleared() {
LocalBroadcastManager lbm = LocalBroadcastManager.getInstance(getApplication());
lbm.unregisterReceiver(addBookmarkReceiver);
lbm.unregisterReceiver(removeBookmarksReceiver);
}
}

View file

@ -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> event = new AsyncTaskLiveData<Event>() {
@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<Event> getEvent() {
return event;
}
}