mirror of
https://github.com/MatomoCamp/matomocamp-companion-android.git
synced 2024-09-19 16:13:46 +02:00
Refactor all database code to use Room, LiveData and the pagination library (#42)
* Bump minSDK version to 16 because SQLite < 3.7.11 doesn't support syntax 'CREATE TABLE IF NOT EXISTS' for FTS tables * Reimplement search results screen using pagination, share a ViewModel between the Activity and the Fragment to allow updating the same fragment instance * Preserve scroll position 0 in live fragments to ensure the insert/remove animation will be visible * Simplify MultiChoiceHelper to always dispatch selection changes to the adapter using a payload, which prevents item crossfading on selection state change * Use withLayer() for better performance of progress bar fade out animation
This commit is contained in:
parent
f943edf747
commit
4df07a40b3
67 changed files with 2640 additions and 2625 deletions
|
@ -6,9 +6,9 @@ android {
|
|||
|
||||
defaultConfig {
|
||||
applicationId "be.digitalia.fosdem"
|
||||
minSdkVersion 15
|
||||
minSdkVersion 16
|
||||
targetSdkVersion 28
|
||||
versionCode 1500160
|
||||
versionCode 1600160
|
||||
versionName "1.6.0"
|
||||
// Supported languages
|
||||
resConfigs "en"
|
||||
|
@ -25,6 +25,8 @@ android {
|
|||
}
|
||||
|
||||
dependencies {
|
||||
def room_version = "2.1.0-alpha03"
|
||||
|
||||
implementation 'androidx.appcompat:appcompat:1.0.2'
|
||||
implementation 'com.google.android.material:material:1.0.0'
|
||||
implementation 'androidx.recyclerview:recyclerview:1.0.0'
|
||||
|
@ -36,5 +38,8 @@ dependencies {
|
|||
}
|
||||
implementation 'androidx.browser:browser:1.0.0'
|
||||
implementation 'androidx.lifecycle:lifecycle-extensions:2.0.0'
|
||||
implementation 'androidx.paging:paging-runtime:2.1.0-rc01'
|
||||
implementation "androidx.room:room-runtime:$room_version"
|
||||
annotationProcessor "androidx.room:room-compiler:$room_version"
|
||||
implementation 'com.github.chrisbanes:PhotoView:2.3.0'
|
||||
}
|
||||
|
|
|
@ -1,10 +1,8 @@
|
|||
package be.digitalia.fosdem;
|
||||
|
||||
import android.app.Application;
|
||||
|
||||
import androidx.preference.PreferenceManager;
|
||||
import be.digitalia.fosdem.alarms.FosdemAlarmManager;
|
||||
import be.digitalia.fosdem.db.DatabaseManager;
|
||||
|
||||
public class FosdemApplication extends Application {
|
||||
|
||||
|
@ -12,7 +10,6 @@ public class FosdemApplication extends Application {
|
|||
public void onCreate() {
|
||||
super.onCreate();
|
||||
|
||||
DatabaseManager.init(this);
|
||||
// Initialize settings
|
||||
PreferenceManager.setDefaultValues(this, R.xml.settings, false);
|
||||
// Alarms (requires settings)
|
||||
|
|
|
@ -36,12 +36,11 @@ import androidx.fragment.app.Fragment;
|
|||
import androidx.fragment.app.FragmentManager;
|
||||
import androidx.fragment.app.FragmentTransaction;
|
||||
import androidx.lifecycle.Observer;
|
||||
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
|
||||
import be.digitalia.fosdem.BuildConfig;
|
||||
import be.digitalia.fosdem.R;
|
||||
import be.digitalia.fosdem.api.FosdemApi;
|
||||
import be.digitalia.fosdem.api.FosdemUrls;
|
||||
import be.digitalia.fosdem.db.DatabaseManager;
|
||||
import be.digitalia.fosdem.db.AppDatabase;
|
||||
import be.digitalia.fosdem.fragments.*;
|
||||
import be.digitalia.fosdem.livedata.SingleEvent;
|
||||
import be.digitalia.fosdem.model.DownloadScheduleResult;
|
||||
|
@ -150,14 +149,6 @@ public class MainActivity extends AppCompatActivity {
|
|||
}
|
||||
};
|
||||
|
||||
private final BroadcastReceiver scheduleRefreshedReceiver = new BroadcastReceiver() {
|
||||
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
updateLastUpdateTime();
|
||||
}
|
||||
};
|
||||
|
||||
public static class DownloadScheduleReminderDialogFragment extends DialogFragment {
|
||||
|
||||
@NonNull
|
||||
|
@ -213,6 +204,7 @@ public class MainActivity extends AppCompatActivity {
|
|||
progressBar.setProgress(100);
|
||||
progressBar.animate()
|
||||
.alpha(0f)
|
||||
.withLayer()
|
||||
.setListener(new AnimatorListenerAdapter() {
|
||||
@Override
|
||||
public void onAnimationEnd(Animator animation) {
|
||||
|
@ -283,11 +275,10 @@ public class MainActivity extends AppCompatActivity {
|
|||
}
|
||||
});
|
||||
|
||||
LocalBroadcastManager.getInstance(this).registerReceiver(scheduleRefreshedReceiver, new IntentFilter(DatabaseManager.ACTION_SCHEDULE_REFRESHED));
|
||||
|
||||
// Last update date, below the list
|
||||
lastUpdateTextView = mainMenu.findViewById(R.id.last_update);
|
||||
updateLastUpdateTime();
|
||||
AppDatabase.getInstance(this).getScheduleDao().getLastUpdateTime()
|
||||
.observe(this, lastUpdateTimeObserver);
|
||||
|
||||
if (savedInstanceState == null) {
|
||||
// Select initial section
|
||||
|
@ -319,13 +310,15 @@ public class MainActivity extends AppCompatActivity {
|
|||
}
|
||||
}
|
||||
|
||||
void updateLastUpdateTime() {
|
||||
long lastUpdateTime = DatabaseManager.getInstance().getLastUpdateTime();
|
||||
private final Observer<Long> lastUpdateTimeObserver = new Observer<Long>() {
|
||||
@Override
|
||||
public void onChanged(Long lastUpdateTime) {
|
||||
lastUpdateTextView.setText(getString(R.string.last_update,
|
||||
(lastUpdateTime == -1L)
|
||||
? getString(R.string.never)
|
||||
: android.text.format.DateFormat.format(LAST_UPDATE_DATE_FORMAT, lastUpdateTime)));
|
||||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
protected void onPostCreate(Bundle savedInstanceState) {
|
||||
|
@ -371,7 +364,8 @@ public class MainActivity extends AppCompatActivity {
|
|||
|
||||
// Download reminder
|
||||
long now = System.currentTimeMillis();
|
||||
long time = DatabaseManager.getInstance().getLastUpdateTime();
|
||||
Long timeValue = AppDatabase.getInstance(this).getScheduleDao().getLastUpdateTime().getValue();
|
||||
long time = (timeValue == null) ? -1L : timeValue;
|
||||
if ((time == -1L) || (time < (now - DATABASE_VALIDITY_DURATION))) {
|
||||
SharedPreferences prefs = getPreferences(Context.MODE_PRIVATE);
|
||||
time = prefs.getLong(PREF_LAST_DOWNLOAD_REMINDER_TIME, -1L);
|
||||
|
@ -397,12 +391,6 @@ public class MainActivity extends AppCompatActivity {
|
|||
super.onStop();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onDestroy() {
|
||||
super.onDestroy();
|
||||
LocalBroadcastManager.getInstance(this).unregisterReceiver(scheduleRefreshedReceiver);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onCreateOptionsMenu(Menu menu) {
|
||||
getMenuInflater().inflate(R.menu.main, menu);
|
||||
|
|
|
@ -77,7 +77,7 @@ public class RoomImageDialogActivity extends AppCompatActivity {
|
|||
});
|
||||
|
||||
// Display the room status as subtitle
|
||||
FosdemApi.getRoomStatuses().observe(owner, new Observer<Map<String, RoomStatus>>() {
|
||||
FosdemApi.getRoomStatuses(toolbar.getContext()).observe(owner, new Observer<Map<String, RoomStatus>>() {
|
||||
@Override
|
||||
public void onChanged(Map<String, RoomStatus> roomStatuses) {
|
||||
RoomStatus roomStatus = roomStatuses.get(roomName);
|
||||
|
|
|
@ -13,22 +13,19 @@ import android.view.MenuItem;
|
|||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.appcompat.widget.SearchView;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.core.util.ObjectsCompat;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
import androidx.lifecycle.ViewModelProviders;
|
||||
import be.digitalia.fosdem.R;
|
||||
import be.digitalia.fosdem.fragments.SearchResultListFragment;
|
||||
import be.digitalia.fosdem.viewmodels.SearchViewModel;
|
||||
import com.google.android.material.snackbar.Snackbar;
|
||||
|
||||
public class SearchResultActivity extends AppCompatActivity {
|
||||
|
||||
public static final int MIN_SEARCH_LENGTH = 3;
|
||||
|
||||
private static final String STATE_CURRENT_QUERY = "current_query";
|
||||
// Search Intent sent by Google Now
|
||||
private static final String GMS_ACTION_SEARCH = "com.google.android.gms.actions.SEARCH_ACTION";
|
||||
|
||||
private String currentQuery;
|
||||
private SearchViewModel viewModel;
|
||||
private SearchView searchView;
|
||||
|
||||
@Override
|
||||
|
@ -38,17 +35,22 @@ public class SearchResultActivity extends AppCompatActivity {
|
|||
|
||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
|
||||
viewModel = ViewModelProviders.of(this).get(SearchViewModel.class);
|
||||
|
||||
if (savedInstanceState == null) {
|
||||
SearchResultListFragment f = SearchResultListFragment.newInstance();
|
||||
getSupportFragmentManager().beginTransaction().replace(R.id.content, f).commit();
|
||||
|
||||
handleIntent(getIntent(), false);
|
||||
} else {
|
||||
currentQuery = savedInstanceState.getString(STATE_CURRENT_QUERY);
|
||||
viewModel.setQuery(savedInstanceState.getString(STATE_CURRENT_QUERY, ""));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onSaveInstanceState(Bundle outState) {
|
||||
super.onSaveInstanceState(outState);
|
||||
outState.putString(STATE_CURRENT_QUERY, currentQuery);
|
||||
outState.putString(STATE_CURRENT_QUERY, viewModel.getQuery());
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -62,29 +64,18 @@ public class SearchResultActivity extends AppCompatActivity {
|
|||
if (Intent.ACTION_SEARCH.equals(intentAction) || GMS_ACTION_SEARCH.equals(intentAction)) {
|
||||
// Normal search, results are displayed here
|
||||
String query = intent.getStringExtra(SearchManager.QUERY);
|
||||
if (query != null) {
|
||||
if (query == null) {
|
||||
query = "";
|
||||
} else {
|
||||
query = query.trim();
|
||||
}
|
||||
if (searchView != null) {
|
||||
setSearchViewQuery(query);
|
||||
}
|
||||
final boolean isQueryTooShort = (query == null) || (query.length() < MIN_SEARCH_LENGTH);
|
||||
|
||||
if (!ObjectsCompat.equals(currentQuery, query)) {
|
||||
currentQuery = query;
|
||||
FragmentManager fm = getSupportFragmentManager();
|
||||
if (isQueryTooShort) {
|
||||
Fragment f = fm.findFragmentById(R.id.content);
|
||||
if (f != null) {
|
||||
fm.beginTransaction().remove(f).commitAllowingStateLoss();
|
||||
}
|
||||
} else {
|
||||
SearchResultListFragment f = SearchResultListFragment.newInstance(query);
|
||||
fm.beginTransaction().replace(R.id.content, f).commitAllowingStateLoss();
|
||||
}
|
||||
}
|
||||
viewModel.setQuery(query);
|
||||
|
||||
if (isQueryTooShort) {
|
||||
if (SearchViewModel.isQueryTooShort(query)) {
|
||||
SpannableString errorMessage = new SpannableString(getString(R.string.search_length_error));
|
||||
int textColor = ContextCompat.getColor(this, R.color.error_material);
|
||||
errorMessage.setSpan(new ForegroundColorSpan(textColor), 0, errorMessage.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
|
@ -113,7 +104,7 @@ public class SearchResultActivity extends AppCompatActivity {
|
|||
searchView = (SearchView) searchMenuItem.getActionView();
|
||||
searchView.setSearchableInfo(searchManager.getSearchableInfo(getComponentName()));
|
||||
searchView.setIconifiedByDefault(false); // Always show the search view
|
||||
setSearchViewQuery(currentQuery);
|
||||
setSearchViewQuery(viewModel.getQuery());
|
||||
|
||||
return true;
|
||||
}
|
||||
|
|
|
@ -1,47 +1,42 @@
|
|||
package be.digitalia.fosdem.activities;
|
||||
|
||||
import android.database.Cursor;
|
||||
import android.os.Bundle;
|
||||
import android.provider.BaseColumns;
|
||||
import android.view.View;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.app.ActionBar;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.fragment.app.FragmentManager;
|
||||
import androidx.fragment.app.FragmentStatePagerAdapter;
|
||||
import androidx.loader.app.LoaderManager;
|
||||
import androidx.loader.app.LoaderManager.LoaderCallbacks;
|
||||
import androidx.loader.content.Loader;
|
||||
import androidx.lifecycle.Observer;
|
||||
import androidx.lifecycle.ViewModelProviders;
|
||||
import androidx.viewpager.widget.ViewPager;
|
||||
import be.digitalia.fosdem.R;
|
||||
import be.digitalia.fosdem.db.DatabaseManager;
|
||||
import be.digitalia.fosdem.fragments.EventDetailsFragment;
|
||||
import be.digitalia.fosdem.loaders.TrackScheduleLoader;
|
||||
import be.digitalia.fosdem.model.Day;
|
||||
import be.digitalia.fosdem.model.Event;
|
||||
import be.digitalia.fosdem.model.StatusEvent;
|
||||
import be.digitalia.fosdem.model.Track;
|
||||
import be.digitalia.fosdem.utils.NfcUtils;
|
||||
import be.digitalia.fosdem.utils.NfcUtils.CreateNfcAppDataCallback;
|
||||
import be.digitalia.fosdem.utils.ThemeUtils;
|
||||
import be.digitalia.fosdem.viewmodels.TrackScheduleViewModel;
|
||||
import be.digitalia.fosdem.widgets.ContentLoadingProgressBar;
|
||||
import com.viewpagerindicator.UnderlinePageIndicator;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Event view of the track schedule; allows to slide between events of the same track using a ViewPager.
|
||||
*
|
||||
* @author Christophe Beyls
|
||||
*/
|
||||
public class TrackScheduleEventActivity extends AppCompatActivity implements LoaderCallbacks<Cursor>, CreateNfcAppDataCallback {
|
||||
public class TrackScheduleEventActivity extends AppCompatActivity implements Observer<List<StatusEvent>>, CreateNfcAppDataCallback {
|
||||
|
||||
public static final String EXTRA_DAY = "day";
|
||||
public static final String EXTRA_TRACK = "track";
|
||||
public static final String EXTRA_POSITION = "position";
|
||||
|
||||
private static final int EVENTS_LOADER_ID = 1;
|
||||
|
||||
private Day day;
|
||||
private Track track;
|
||||
private int initialPosition = -1;
|
||||
private ContentLoadingProgressBar progress;
|
||||
private ViewPager pager;
|
||||
|
@ -55,8 +50,8 @@ public class TrackScheduleEventActivity extends AppCompatActivity implements Loa
|
|||
setContentView(R.layout.track_schedule_event);
|
||||
|
||||
Bundle extras = getIntent().getExtras();
|
||||
day = extras.getParcelable(EXTRA_DAY);
|
||||
track = extras.getParcelable(EXTRA_TRACK);
|
||||
final Day day = extras.getParcelable(EXTRA_DAY);
|
||||
final Track track = extras.getParcelable(EXTRA_TRACK);
|
||||
|
||||
progress = findViewById(R.id.progress);
|
||||
pager = findViewById(R.id.pager);
|
||||
|
@ -78,7 +73,9 @@ public class TrackScheduleEventActivity extends AppCompatActivity implements Loa
|
|||
NfcUtils.setAppDataPushMessageCallbackIfAvailable(this, this);
|
||||
|
||||
setCustomProgressVisibility(true);
|
||||
LoaderManager.getInstance(this).initLoader(EVENTS_LOADER_ID, null, this);
|
||||
final TrackScheduleViewModel viewModel = ViewModelProviders.of(this).get(TrackScheduleViewModel.class);
|
||||
viewModel.setTrack(day, track);
|
||||
viewModel.getSchedule().observe(this, this);
|
||||
}
|
||||
|
||||
private void setCustomProgressVisibility(boolean isVisible) {
|
||||
|
@ -94,11 +91,11 @@ public class TrackScheduleEventActivity extends AppCompatActivity implements Loa
|
|||
if (adapter.getCount() == 0) {
|
||||
return null;
|
||||
}
|
||||
long eventId = adapter.getItemId(pager.getCurrentItem());
|
||||
if (eventId == -1L) {
|
||||
Event event = adapter.getEvent(pager.getCurrentItem());
|
||||
if (event == null) {
|
||||
return null;
|
||||
}
|
||||
return String.valueOf(eventId).getBytes();
|
||||
return String.valueOf(event.getId()).getBytes();
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -107,19 +104,13 @@ public class TrackScheduleEventActivity extends AppCompatActivity implements Loa
|
|||
return true;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
|
||||
return new TrackScheduleLoader(this, day, track);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoadFinished(@NonNull Loader<Cursor> loader, Cursor data) {
|
||||
public void onChanged(List<StatusEvent> schedule) {
|
||||
setCustomProgressVisibility(false);
|
||||
|
||||
if (data != null) {
|
||||
if (schedule != null) {
|
||||
pager.setVisibility(View.VISIBLE);
|
||||
adapter.setCursor(data);
|
||||
adapter.setSchedule(schedule);
|
||||
|
||||
// Delay setting the adapter
|
||||
// to ensure the current position is restored properly
|
||||
|
@ -135,44 +126,34 @@ public class TrackScheduleEventActivity extends AppCompatActivity implements Loa
|
|||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoaderReset(@NonNull Loader<Cursor> loader) {
|
||||
adapter.setCursor(null);
|
||||
}
|
||||
|
||||
public static class TrackScheduleEventAdapter extends FragmentStatePagerAdapter {
|
||||
|
||||
private Cursor cursor;
|
||||
private List<StatusEvent> events = null;
|
||||
|
||||
public TrackScheduleEventAdapter(FragmentManager fm) {
|
||||
super(fm);
|
||||
}
|
||||
|
||||
public Cursor getCursor() {
|
||||
return cursor;
|
||||
}
|
||||
|
||||
public void setCursor(Cursor cursor) {
|
||||
this.cursor = cursor;
|
||||
public void setSchedule(List<StatusEvent> schedule) {
|
||||
this.events = schedule;
|
||||
notifyDataSetChanged();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getCount() {
|
||||
return (cursor == null) ? 0 : cursor.getCount();
|
||||
return (events == null) ? 0 : events.size();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Fragment getItem(int position) {
|
||||
cursor.moveToPosition(position);
|
||||
return EventDetailsFragment.newInstance(DatabaseManager.toEvent(cursor));
|
||||
return EventDetailsFragment.newInstance(events.get(position).getEvent());
|
||||
}
|
||||
|
||||
public long getItemId(int position) {
|
||||
if (!cursor.moveToPosition(position)) {
|
||||
return -1L;
|
||||
public Event getEvent(int position) {
|
||||
if (position < 0 || position >= getCount()) {
|
||||
return null;
|
||||
}
|
||||
return cursor.getLong(cursor.getColumnIndexOrThrow(BaseColumns._ID));
|
||||
return events.get(position).getEvent();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,187 +1,314 @@
|
|||
package be.digitalia.fosdem.adapters;
|
||||
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.content.Intent;
|
||||
import android.graphics.Typeface;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Parcelable;
|
||||
import android.text.SpannableString;
|
||||
import android.text.Spanned;
|
||||
import android.text.TextUtils;
|
||||
import android.text.style.ForegroundColorSpan;
|
||||
import android.text.style.StyleSpan;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuItem;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.TextView;
|
||||
import androidx.annotation.ColorInt;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.appcompat.view.ActionMode;
|
||||
import androidx.collection.SimpleArrayMap;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.core.util.ObjectsCompat;
|
||||
import androidx.lifecycle.LifecycleOwner;
|
||||
import androidx.lifecycle.Observer;
|
||||
import androidx.recyclerview.widget.DiffUtil;
|
||||
import androidx.recyclerview.widget.ListAdapter;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import be.digitalia.fosdem.R;
|
||||
import be.digitalia.fosdem.db.DatabaseManager;
|
||||
import be.digitalia.fosdem.activities.EventDetailsActivity;
|
||||
import be.digitalia.fosdem.api.FosdemApi;
|
||||
import be.digitalia.fosdem.model.Event;
|
||||
import be.digitalia.fosdem.model.RoomStatus;
|
||||
import be.digitalia.fosdem.model.Track;
|
||||
import be.digitalia.fosdem.utils.DateUtils;
|
||||
import be.digitalia.fosdem.widgets.MultiChoiceHelper;
|
||||
|
||||
public class BookmarksAdapter extends EventsAdapter {
|
||||
import java.text.DateFormat;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public class BookmarksAdapter extends ListAdapter<Event, BookmarksAdapter.ViewHolder>
|
||||
implements Observer<Map<String, RoomStatus>> {
|
||||
|
||||
private static final DiffUtil.ItemCallback<Event> DIFF_CALLBACK = new SimpleItemCallback<Event>() {
|
||||
@Override
|
||||
public boolean areContentsTheSame(@NonNull Event oldEvent, @NonNull Event newEvent) {
|
||||
return ObjectsCompat.equals(oldEvent.getTitle(), newEvent.getTitle())
|
||||
&& ObjectsCompat.equals(oldEvent.getPersonsSummary(), newEvent.getPersonsSummary())
|
||||
&& ObjectsCompat.equals(oldEvent.getTrack(), newEvent.getTrack())
|
||||
&& ObjectsCompat.equals(oldEvent.getDay(), newEvent.getDay())
|
||||
&& ObjectsCompat.equals(oldEvent.getStartTime(), newEvent.getStartTime())
|
||||
&& ObjectsCompat.equals(oldEvent.getEndTime(), newEvent.getEndTime())
|
||||
&& ObjectsCompat.equals(oldEvent.getRoomName(), newEvent.getRoomName());
|
||||
}
|
||||
};
|
||||
static final Object DETAILS_PAYLOAD = new Object();
|
||||
|
||||
private final DateFormat timeDateFormat;
|
||||
@ColorInt
|
||||
private final int errorColor;
|
||||
private final SimpleArrayMap<RecyclerView.AdapterDataObserver, BookmarksDataObserverWrapper> observers = new SimpleArrayMap<>();
|
||||
final MultiChoiceHelper multiChoiceHelper;
|
||||
private Map<String, RoomStatus> roomStatuses;
|
||||
|
||||
public BookmarksAdapter(AppCompatActivity activity, LifecycleOwner owner) {
|
||||
super(activity, owner);
|
||||
public BookmarksAdapter(@NonNull AppCompatActivity activity, @NonNull LifecycleOwner owner,
|
||||
@NonNull MultiChoiceHelper.MultiChoiceModeListener multiChoiceModeListener) {
|
||||
super(DIFF_CALLBACK);
|
||||
setHasStableIds(true);
|
||||
timeDateFormat = DateUtils.getTimeDateFormat(activity);
|
||||
errorColor = ContextCompat.getColor(activity, R.color.error_material);
|
||||
|
||||
multiChoiceHelper = new MultiChoiceHelper(activity, this);
|
||||
multiChoiceHelper.setMultiChoiceModeListener(new MultiChoiceHelper.MultiChoiceModeListener() {
|
||||
multiChoiceHelper.setMultiChoiceModeListener(multiChoiceModeListener);
|
||||
|
||||
@Override
|
||||
public boolean onCreateActionMode(ActionMode mode, Menu menu) {
|
||||
mode.getMenuInflater().inflate(R.menu.action_mode_bookmarks, menu);
|
||||
return true;
|
||||
FosdemApi.getRoomStatuses(activity).observe(owner, this);
|
||||
}
|
||||
|
||||
private void updateSelectedCountDisplay(ActionMode mode) {
|
||||
int count = multiChoiceHelper.getCheckedItemCount();
|
||||
mode.setTitle(multiChoiceHelper.getContext().getResources().getQuantityString(R.plurals.selected, count, count));
|
||||
@NonNull
|
||||
public MultiChoiceHelper getMultiChoiceHelper() {
|
||||
return multiChoiceHelper;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
|
||||
updateSelectedCountDisplay(mode);
|
||||
return true;
|
||||
public void onChanged(@Nullable Map<String, RoomStatus> roomStatuses) {
|
||||
this.roomStatuses = roomStatuses;
|
||||
notifyItemRangeChanged(0, getItemCount(), DETAILS_PAYLOAD);
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
case R.id.delete:
|
||||
// Remove multiple bookmarks at once
|
||||
new RemoveBookmarksAsyncTask().execute(multiChoiceHelper.getCheckedItemIds());
|
||||
mode.finish();
|
||||
return true;
|
||||
public long getItemId(int position) {
|
||||
return getItem(position).getId();
|
||||
}
|
||||
return false;
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_event, parent, false);
|
||||
return new ViewHolder(view, multiChoiceHelper, timeDateFormat, errorColor);
|
||||
}
|
||||
|
||||
private RoomStatus getRoomStatus(Event event) {
|
||||
return (roomStatuses == null) ? null : roomStatuses.get(event.getRoomName());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onItemCheckedStateChanged(ActionMode mode, int position, long id, boolean checked) {
|
||||
updateSelectedCountDisplay(mode);
|
||||
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
|
||||
final Event event = getItem(position);
|
||||
holder.bind(event);
|
||||
final Event previous = position > 0 ? getItem(position - 1) : null;
|
||||
final Event next = position + 1 < getItemCount() ? getItem(position + 1) : null;
|
||||
holder.bindDetails(event, previous, next, getRoomStatus(event));
|
||||
holder.bindSelection();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroyActionMode(ActionMode mode) {
|
||||
public void onBindViewHolder(@NonNull ViewHolder holder, int position, @NonNull List<Object> payloads) {
|
||||
if (payloads.isEmpty()) {
|
||||
onBindViewHolder(holder, position);
|
||||
} else {
|
||||
final Event event = getItem(position);
|
||||
if (payloads.contains(DETAILS_PAYLOAD)) {
|
||||
final Event previous = position > 0 ? getItem(position - 1) : null;
|
||||
final Event next = position + 1 < getItemCount() ? getItem(position + 1) : null;
|
||||
holder.bindDetails(event, previous, next, getRoomStatus(event));
|
||||
}
|
||||
});
|
||||
if (payloads.contains(MultiChoiceHelper.SELECTION_PAYLOAD)) {
|
||||
holder.bindSelection();
|
||||
}
|
||||
|
||||
public Parcelable onSaveInstanceState() {
|
||||
return multiChoiceHelper.onSaveInstanceState();
|
||||
}
|
||||
|
||||
public void onRestoreInstanceState(Parcelable state) {
|
||||
multiChoiceHelper.onRestoreInstanceState(state);
|
||||
}
|
||||
|
||||
public void onDestroyView() {
|
||||
multiChoiceHelper.clearChoices();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(ViewHolder holder, Cursor cursor) {
|
||||
final int position = cursor.getPosition();
|
||||
Context context = holder.itemView.getContext();
|
||||
Event event = DatabaseManager.toEvent(cursor, holder.event);
|
||||
holder.event = event;
|
||||
holder.isOverlapping = isOverlapping(cursor, event.getStartTime(), event.getEndTime());
|
||||
public void registerAdapterDataObserver(@NonNull RecyclerView.AdapterDataObserver observer) {
|
||||
if (!observers.containsKey(observer)) {
|
||||
final BookmarksDataObserverWrapper wrapper = new BookmarksDataObserverWrapper(observer, this);
|
||||
observers.put(observer, wrapper);
|
||||
super.registerAdapterDataObserver(wrapper);
|
||||
}
|
||||
}
|
||||
|
||||
holder.title.setText(event.getTitle());
|
||||
@Override
|
||||
public void unregisterAdapterDataObserver(@NonNull RecyclerView.AdapterDataObserver observer) {
|
||||
final BookmarksDataObserverWrapper wrapper = observers.remove(observer);
|
||||
if (wrapper != null) {
|
||||
super.unregisterAdapterDataObserver(wrapper);
|
||||
}
|
||||
}
|
||||
|
||||
static class ViewHolder extends MultiChoiceHelper.ViewHolder implements View.OnClickListener {
|
||||
final TextView title;
|
||||
final TextView persons;
|
||||
final TextView trackName;
|
||||
final TextView details;
|
||||
|
||||
private final DateFormat timeDateFormat;
|
||||
@ColorInt
|
||||
private final int errorColor;
|
||||
|
||||
Event event;
|
||||
|
||||
public ViewHolder(@NonNull View itemView, @NonNull MultiChoiceHelper helper,
|
||||
@NonNull DateFormat timeDateFormat, @ColorInt int errorColor) {
|
||||
super(itemView, helper);
|
||||
|
||||
title = itemView.findViewById(R.id.title);
|
||||
persons = itemView.findViewById(R.id.persons);
|
||||
trackName = itemView.findViewById(R.id.track_name);
|
||||
details = itemView.findViewById(R.id.details);
|
||||
setOnClickListener(this);
|
||||
|
||||
this.timeDateFormat = timeDateFormat;
|
||||
this.errorColor = errorColor;
|
||||
}
|
||||
|
||||
void bind(@NonNull Event event) {
|
||||
Context context = itemView.getContext();
|
||||
this.event = event;
|
||||
|
||||
title.setText(event.getTitle());
|
||||
String personsSummary = event.getPersonsSummary();
|
||||
holder.persons.setText(personsSummary);
|
||||
holder.persons.setVisibility(TextUtils.isEmpty(personsSummary) ? View.GONE : View.VISIBLE);
|
||||
persons.setText(personsSummary);
|
||||
persons.setVisibility(TextUtils.isEmpty(personsSummary) ? View.GONE : View.VISIBLE);
|
||||
Track track = event.getTrack();
|
||||
holder.trackName.setText(track.getName());
|
||||
holder.trackName.setTextColor(ContextCompat.getColor(context, track.getType().getColorResId()));
|
||||
holder.trackName.setContentDescription(context.getString(R.string.track_content_description, track.getName()));
|
||||
|
||||
bindDetails(holder, event);
|
||||
|
||||
// Enable MultiChoice selection and update checked state
|
||||
holder.bind(multiChoiceHelper, position);
|
||||
trackName.setText(track.getName());
|
||||
trackName.setTextColor(ContextCompat.getColor(context, track.getType().getColorResId()));
|
||||
trackName.setContentDescription(context.getString(R.string.track_content_description, track.getName()));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void bindDetails(ViewHolder holder, Event event) {
|
||||
Context context = holder.details.getContext();
|
||||
void bindDetails(@NonNull Event event, @Nullable Event previous, @Nullable Event next, @Nullable RoomStatus roomStatus) {
|
||||
Context context = details.getContext();
|
||||
Date startTime = event.getStartTime();
|
||||
Date endTime = event.getEndTime();
|
||||
String startTimeString = (startTime != null) ? timeDateFormat.format(startTime) : "?";
|
||||
String endTimeString = (endTime != null) ? timeDateFormat.format(endTime) : "?";
|
||||
String roomName = event.getRoomName();
|
||||
String details = String.format("%1$s, %2$s ― %3$s | %4$s", event.getDay().getShortName(), startTimeString, endTimeString, roomName);
|
||||
SpannableString detailsSpannable = new SpannableString(details);
|
||||
String detailsContentDescription = details;
|
||||
String detailsText = String.format("%1$s, %2$s ― %3$s | %4$s", event.getDay().getShortName(), startTimeString, endTimeString, roomName);
|
||||
SpannableString detailsSpannable = new SpannableString(detailsText);
|
||||
CharSequence detailsDescription = detailsText;
|
||||
|
||||
// Highlight the date and time with error color in case of conflicting schedules
|
||||
if (holder.isOverlapping) {
|
||||
int endPosition = details.indexOf(" | ");
|
||||
if (isOverlapping(event, previous, next)) {
|
||||
int endPosition = detailsText.indexOf(" | ");
|
||||
detailsSpannable.setSpan(new ForegroundColorSpan(errorColor), 0, endPosition, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
detailsSpannable.setSpan(new StyleSpan(Typeface.BOLD), 0, endPosition, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
holder.details.setText(detailsSpannable);
|
||||
detailsContentDescription = context.getString(R.string.bookmark_conflict_content_description, detailsContentDescription);
|
||||
details.setText(detailsSpannable);
|
||||
detailsDescription = context.getString(R.string.bookmark_conflict_content_description, detailsDescription);
|
||||
}
|
||||
if (roomStatuses != null) {
|
||||
RoomStatus roomStatus = roomStatuses.get(roomName);
|
||||
if (roomStatus != null) {
|
||||
int color = ContextCompat.getColor(context, roomStatus.getColorResId());
|
||||
detailsSpannable.setSpan(new ForegroundColorSpan(color),
|
||||
details.length() - roomName.length(),
|
||||
details.length(),
|
||||
detailsText.length() - roomName.length(),
|
||||
detailsText.length(),
|
||||
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
}
|
||||
}
|
||||
holder.details.setText(detailsSpannable);
|
||||
holder.details.setContentDescription(context.getString(R.string.details_content_description, detailsContentDescription));
|
||||
details.setText(detailsSpannable);
|
||||
details.setContentDescription(context.getString(R.string.details_content_description, detailsDescription));
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if the current event is overlapping with the previous or next one.
|
||||
* Warning: this methods will update the cursor's position.
|
||||
*/
|
||||
private static boolean isOverlapping(Cursor cursor, Date startTime, Date endTime) {
|
||||
final int position = cursor.getPosition();
|
||||
|
||||
if ((startTime != null) && (position > 0) && cursor.moveToPosition(position - 1)) {
|
||||
long previousEndTime = DatabaseManager.toEventEndTimeMillis(cursor);
|
||||
if ((previousEndTime != -1L) && (previousEndTime > startTime.getTime())) {
|
||||
private static boolean isOverlapping(@NonNull Event event, @Nullable Event previous, @Nullable Event next) {
|
||||
final Date startTime = event.getStartTime();
|
||||
final Date previousEndTime = (previous == null) ? null : previous.getEndTime();
|
||||
if (startTime != null && previousEndTime != null && previousEndTime.getTime() > startTime.getTime()) {
|
||||
// The event overlaps with the previous one
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if ((endTime != null) && (position < (cursor.getCount() - 1)) && cursor.moveToPosition(position + 1)) {
|
||||
long nextStartTime = DatabaseManager.toEventStartTimeMillis(cursor);
|
||||
if ((nextStartTime != -1L) && (nextStartTime < endTime.getTime())) {
|
||||
final Date endTime = event.getEndTime();
|
||||
final Date nextStartTime = (next == null) ? null : next.getStartTime();
|
||||
// The event overlaps with the next one
|
||||
return true;
|
||||
return endTime != null && nextStartTime != null && nextStartTime.getTime() < endTime.getTime();
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
static class RemoveBookmarksAsyncTask extends AsyncTask<long[], Void, Void> {
|
||||
|
||||
@Override
|
||||
protected Void doInBackground(long[]... params) {
|
||||
DatabaseManager.getInstance().removeBookmarks(params[0]);
|
||||
return null;
|
||||
public void onClick(View view) {
|
||||
if (event != null) {
|
||||
Context context = view.getContext();
|
||||
Intent intent = new Intent(context, EventDetailsActivity.class)
|
||||
.putExtra(EventDetailsActivity.EXTRA_EVENT, event);
|
||||
context.startActivity(intent);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* An observer dispatching updates to the source observer while additionally notifying changes
|
||||
* of the immediately previous and next items in order to properly update their overlapping status display.
|
||||
*/
|
||||
static class BookmarksDataObserverWrapper extends RecyclerView.AdapterDataObserver {
|
||||
private final RecyclerView.AdapterDataObserver observer;
|
||||
private final RecyclerView.Adapter<?> adapter;
|
||||
|
||||
public BookmarksDataObserverWrapper(RecyclerView.AdapterDataObserver observer, RecyclerView.Adapter<?> adapter) {
|
||||
this.observer = observer;
|
||||
this.adapter = adapter;
|
||||
}
|
||||
|
||||
private void updatePrevious(int position) {
|
||||
if (position >= 0) {
|
||||
observer.onItemRangeChanged(position, 1, DETAILS_PAYLOAD);
|
||||
}
|
||||
}
|
||||
|
||||
private void updateNext(int position) {
|
||||
if (position < adapter.getItemCount()) {
|
||||
observer.onItemRangeChanged(position, 1, DETAILS_PAYLOAD);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onChanged() {
|
||||
observer.onChanged();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onItemRangeChanged(int positionStart, int itemCount) {
|
||||
observer.onItemRangeChanged(positionStart, itemCount);
|
||||
updatePrevious(positionStart - 1);
|
||||
updateNext(positionStart + itemCount);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onItemRangeChanged(int positionStart, int itemCount, @Nullable Object payload) {
|
||||
observer.onItemRangeChanged(positionStart, itemCount, payload);
|
||||
updatePrevious(positionStart - 1);
|
||||
updateNext(positionStart + itemCount);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onItemRangeInserted(int positionStart, int itemCount) {
|
||||
observer.onItemRangeInserted(positionStart, itemCount);
|
||||
updatePrevious(positionStart - 1);
|
||||
updateNext(positionStart + itemCount);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onItemRangeRemoved(int positionStart, int itemCount) {
|
||||
observer.onItemRangeRemoved(positionStart, itemCount);
|
||||
updatePrevious(positionStart - 1);
|
||||
updateNext(positionStart);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onItemRangeMoved(int fromPosition, int toPosition, int itemCount) {
|
||||
updatePrevious(fromPosition - 1);
|
||||
updateNext(fromPosition + itemCount);
|
||||
observer.onItemRangeMoved(fromPosition, toPosition, itemCount);
|
||||
updatePrevious(toPosition - 1);
|
||||
updateNext(toPosition + itemCount);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,7 +2,6 @@ package be.digitalia.fosdem.adapters;
|
|||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.database.Cursor;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.text.SpannableString;
|
||||
import android.text.Spanned;
|
||||
|
@ -12,49 +11,65 @@ import android.view.LayoutInflater;
|
|||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.TextView;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.content.res.AppCompatResources;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.core.util.ObjectsCompat;
|
||||
import androidx.core.widget.TextViewCompat;
|
||||
import androidx.lifecycle.LifecycleOwner;
|
||||
import androidx.lifecycle.Observer;
|
||||
import androidx.paging.PagedListAdapter;
|
||||
import androidx.recyclerview.widget.DiffUtil;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import be.digitalia.fosdem.R;
|
||||
import be.digitalia.fosdem.activities.EventDetailsActivity;
|
||||
import be.digitalia.fosdem.api.FosdemApi;
|
||||
import be.digitalia.fosdem.model.Event;
|
||||
import be.digitalia.fosdem.model.RoomStatus;
|
||||
import be.digitalia.fosdem.model.StatusEvent;
|
||||
import be.digitalia.fosdem.model.Track;
|
||||
import be.digitalia.fosdem.utils.DateUtils;
|
||||
|
||||
import java.text.DateFormat;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.content.res.AppCompatResources;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.core.widget.TextViewCompat;
|
||||
import androidx.lifecycle.LifecycleOwner;
|
||||
import androidx.lifecycle.Observer;
|
||||
import be.digitalia.fosdem.R;
|
||||
import be.digitalia.fosdem.activities.EventDetailsActivity;
|
||||
import be.digitalia.fosdem.api.FosdemApi;
|
||||
import be.digitalia.fosdem.db.DatabaseManager;
|
||||
import be.digitalia.fosdem.model.Event;
|
||||
import be.digitalia.fosdem.model.RoomStatus;
|
||||
import be.digitalia.fosdem.model.Track;
|
||||
import be.digitalia.fosdem.utils.DateUtils;
|
||||
import be.digitalia.fosdem.widgets.MultiChoiceHelper;
|
||||
|
||||
public class EventsAdapter extends RecyclerViewCursorAdapter<EventsAdapter.ViewHolder>
|
||||
public class EventsAdapter extends PagedListAdapter<StatusEvent, EventsAdapter.ViewHolder>
|
||||
implements Observer<Map<String, RoomStatus>> {
|
||||
|
||||
private static final DiffUtil.ItemCallback<StatusEvent> DIFF_CALLBACK = new SimpleItemCallback<StatusEvent>() {
|
||||
@Override
|
||||
public boolean areContentsTheSame(@NonNull StatusEvent oldItem, @NonNull StatusEvent newItem) {
|
||||
final Event oldEvent = oldItem.getEvent();
|
||||
final Event newEvent = newItem.getEvent();
|
||||
return ObjectsCompat.equals(oldEvent.getTitle(), newEvent.getTitle())
|
||||
&& ObjectsCompat.equals(oldEvent.getPersonsSummary(), newEvent.getPersonsSummary())
|
||||
&& ObjectsCompat.equals(oldEvent.getTrack(), newEvent.getTrack())
|
||||
&& ObjectsCompat.equals(oldEvent.getDay(), newEvent.getDay())
|
||||
&& ObjectsCompat.equals(oldEvent.getStartTime(), newEvent.getStartTime())
|
||||
&& ObjectsCompat.equals(oldEvent.getEndTime(), newEvent.getEndTime())
|
||||
&& ObjectsCompat.equals(oldEvent.getRoomName(), newEvent.getRoomName())
|
||||
&& oldItem.isBookmarked() == newItem.isBookmarked();
|
||||
}
|
||||
};
|
||||
private static final Object DETAILS_PAYLOAD = new Object();
|
||||
|
||||
protected final LayoutInflater inflater;
|
||||
protected final DateFormat timeDateFormat;
|
||||
private final DateFormat timeDateFormat;
|
||||
private final boolean showDay;
|
||||
protected Map<String, RoomStatus> roomStatuses;
|
||||
private Map<String, RoomStatus> roomStatuses;
|
||||
|
||||
public EventsAdapter(Context context, LifecycleOwner owner) {
|
||||
this(context, owner, true);
|
||||
}
|
||||
|
||||
public EventsAdapter(Context context, LifecycleOwner owner, boolean showDay) {
|
||||
inflater = LayoutInflater.from(context);
|
||||
super(DIFF_CALLBACK);
|
||||
timeDateFormat = DateUtils.getTimeDateFormat(context);
|
||||
this.showDay = showDay;
|
||||
|
||||
FosdemApi.getRoomStatuses().observe(owner, this);
|
||||
FosdemApi.getRoomStatuses(context).observe(owner, this);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -63,11 +78,6 @@ public class EventsAdapter extends RecyclerViewCursorAdapter<EventsAdapter.ViewH
|
|||
notifyItemRangeChanged(0, getItemCount(), DETAILS_PAYLOAD);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Event getItem(int position) {
|
||||
return DatabaseManager.toEvent((Cursor) super.getItem(position));
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemViewType(int position) {
|
||||
return R.layout.item_event;
|
||||
|
@ -76,67 +86,24 @@ public class EventsAdapter extends RecyclerViewCursorAdapter<EventsAdapter.ViewH
|
|||
@NonNull
|
||||
@Override
|
||||
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||
View view = inflater.inflate(R.layout.item_event, parent, false);
|
||||
return new ViewHolder(view);
|
||||
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_event, parent, false);
|
||||
return new ViewHolder(view, timeDateFormat);
|
||||
}
|
||||
|
||||
private RoomStatus getRoomStatus(Event event) {
|
||||
return (roomStatuses == null) ? null : roomStatuses.get(event.getRoomName());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(ViewHolder holder, Cursor cursor) {
|
||||
Context context = holder.itemView.getContext();
|
||||
Event event = DatabaseManager.toEvent(cursor, holder.event);
|
||||
holder.event = event;
|
||||
|
||||
holder.title.setText(event.getTitle());
|
||||
boolean isBookmarked = DatabaseManager.toBookmarkStatus(cursor);
|
||||
Drawable bookmarkDrawable = isBookmarked
|
||||
? AppCompatResources.getDrawable(context, R.drawable.ic_bookmark_grey600_24dp)
|
||||
: null;
|
||||
TextViewCompat.setCompoundDrawablesRelativeWithIntrinsicBounds(holder.title, null, null, bookmarkDrawable, null);
|
||||
holder.title.setContentDescription(isBookmarked
|
||||
? context.getString(R.string.in_bookmarks_content_description, event.getTitle())
|
||||
: null
|
||||
);
|
||||
String personsSummary = event.getPersonsSummary();
|
||||
holder.persons.setText(personsSummary);
|
||||
holder.persons.setVisibility(TextUtils.isEmpty(personsSummary) ? View.GONE : View.VISIBLE);
|
||||
Track track = event.getTrack();
|
||||
holder.trackName.setText(track.getName());
|
||||
holder.trackName.setTextColor(ContextCompat.getColor(context, track.getType().getColorResId()));
|
||||
holder.trackName.setContentDescription(context.getString(R.string.track_content_description, track.getName()));
|
||||
|
||||
bindDetails(holder, event);
|
||||
}
|
||||
|
||||
protected void bindDetails(ViewHolder holder, Event event) {
|
||||
Context context = holder.details.getContext();
|
||||
Date startTime = event.getStartTime();
|
||||
Date endTime = event.getEndTime();
|
||||
String startTimeString = (startTime != null) ? timeDateFormat.format(startTime) : "?";
|
||||
String endTimeString = (endTime != null) ? timeDateFormat.format(endTime) : "?";
|
||||
String roomName = event.getRoomName();
|
||||
CharSequence details;
|
||||
if (showDay) {
|
||||
details = String.format("%1$s, %2$s ― %3$s | %4$s", event.getDay().getShortName(), startTimeString, endTimeString, roomName);
|
||||
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
|
||||
final StatusEvent statusEvent = getItem(position);
|
||||
if (statusEvent == null) {
|
||||
holder.clear();
|
||||
} else {
|
||||
details = String.format("%1$s ― %2$s | %3$s", startTimeString, endTimeString, roomName);
|
||||
final Event event = statusEvent.getEvent();
|
||||
holder.bind(event, statusEvent.isBookmarked());
|
||||
holder.bindDetails(event, showDay, getRoomStatus(event));
|
||||
}
|
||||
CharSequence detailsDescription = details;
|
||||
if (roomStatuses != null) {
|
||||
RoomStatus roomStatus = roomStatuses.get(roomName);
|
||||
if (roomStatus != null) {
|
||||
SpannableString detailsSpannable = new SpannableString(details);
|
||||
int color = ContextCompat.getColor(context, roomStatus.getColorResId());
|
||||
detailsSpannable.setSpan(new ForegroundColorSpan(color),
|
||||
details.length() - roomName.length(),
|
||||
details.length(),
|
||||
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
details = detailsSpannable;
|
||||
|
||||
detailsDescription = String.format("%1$s (%2$s)", detailsDescription, context.getString(roomStatus.getNameResId()));
|
||||
}
|
||||
}
|
||||
holder.details.setText(details);
|
||||
holder.details.setContentDescription(context.getString(R.string.details_content_description, detailsDescription));
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -144,32 +111,99 @@ public class EventsAdapter extends RecyclerViewCursorAdapter<EventsAdapter.ViewH
|
|||
if (payloads.isEmpty()) {
|
||||
onBindViewHolder(holder, position);
|
||||
} else {
|
||||
if (payloads.contains(DETAILS_PAYLOAD) && (holder.event != null)) {
|
||||
bindDetails(holder, holder.event);
|
||||
final StatusEvent statusEvent = getItem(position);
|
||||
if (statusEvent != null) {
|
||||
if (payloads.contains(DETAILS_PAYLOAD)) {
|
||||
final Event event = statusEvent.getEvent();
|
||||
holder.bindDetails(event, showDay, getRoomStatus(event));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static class ViewHolder extends MultiChoiceHelper.ViewHolder implements View.OnClickListener {
|
||||
TextView title;
|
||||
TextView persons;
|
||||
TextView trackName;
|
||||
TextView details;
|
||||
static class ViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener {
|
||||
final TextView title;
|
||||
final TextView persons;
|
||||
final TextView trackName;
|
||||
final TextView details;
|
||||
|
||||
private final DateFormat timeDateFormat;
|
||||
|
||||
Event event;
|
||||
boolean isOverlapping;
|
||||
|
||||
public ViewHolder(View itemView) {
|
||||
ViewHolder(View itemView, @NonNull DateFormat timeDateFormat) {
|
||||
super(itemView);
|
||||
title = itemView.findViewById(R.id.title);
|
||||
persons = itemView.findViewById(R.id.persons);
|
||||
trackName = itemView.findViewById(R.id.track_name);
|
||||
details = itemView.findViewById(R.id.details);
|
||||
setOnClickListener(this);
|
||||
itemView.setOnClickListener(this);
|
||||
|
||||
this.timeDateFormat = timeDateFormat;
|
||||
}
|
||||
|
||||
void clear() {
|
||||
this.event = null;
|
||||
title.setText(null);
|
||||
persons.setText(null);
|
||||
trackName.setText(null);
|
||||
details.setText(null);
|
||||
}
|
||||
|
||||
void bind(@NonNull Event event, boolean isBookmarked) {
|
||||
Context context = itemView.getContext();
|
||||
this.event = event;
|
||||
|
||||
title.setText(event.getTitle());
|
||||
Drawable bookmarkDrawable = isBookmarked
|
||||
? AppCompatResources.getDrawable(context, R.drawable.ic_bookmark_grey600_24dp)
|
||||
: null;
|
||||
TextViewCompat.setCompoundDrawablesRelativeWithIntrinsicBounds(title, null, null, bookmarkDrawable, null);
|
||||
title.setContentDescription(isBookmarked
|
||||
? context.getString(R.string.in_bookmarks_content_description, event.getTitle())
|
||||
: null
|
||||
);
|
||||
String personsSummary = event.getPersonsSummary();
|
||||
persons.setText(personsSummary);
|
||||
persons.setVisibility(TextUtils.isEmpty(personsSummary) ? View.GONE : View.VISIBLE);
|
||||
Track track = event.getTrack();
|
||||
trackName.setText(track.getName());
|
||||
trackName.setTextColor(ContextCompat.getColor(context, track.getType().getColorResId()));
|
||||
trackName.setContentDescription(context.getString(R.string.track_content_description, track.getName()));
|
||||
}
|
||||
|
||||
void bindDetails(@NonNull Event event, boolean showDay, @Nullable RoomStatus roomStatus) {
|
||||
Context context = details.getContext();
|
||||
Date startTime = event.getStartTime();
|
||||
Date endTime = event.getEndTime();
|
||||
String startTimeString = (startTime != null) ? timeDateFormat.format(startTime) : "?";
|
||||
String endTimeString = (endTime != null) ? timeDateFormat.format(endTime) : "?";
|
||||
String roomName = event.getRoomName();
|
||||
CharSequence detailsText;
|
||||
if (showDay) {
|
||||
detailsText = String.format("%1$s, %2$s ― %3$s | %4$s", event.getDay().getShortName(), startTimeString, endTimeString, roomName);
|
||||
} else {
|
||||
detailsText = String.format("%1$s ― %2$s | %3$s", startTimeString, endTimeString, roomName);
|
||||
}
|
||||
CharSequence detailsDescription = detailsText;
|
||||
if (roomStatus != null) {
|
||||
SpannableString detailsSpannable = new SpannableString(detailsText);
|
||||
int color = ContextCompat.getColor(context, roomStatus.getColorResId());
|
||||
detailsSpannable.setSpan(new ForegroundColorSpan(color),
|
||||
detailsText.length() - roomName.length(),
|
||||
detailsText.length(),
|
||||
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||
detailsText = detailsSpannable;
|
||||
|
||||
detailsDescription = String.format("%1$s (%2$s)", detailsDescription, context.getString(roomStatus.getNameResId()));
|
||||
}
|
||||
details.setText(detailsText);
|
||||
details.setContentDescription(context.getString(R.string.details_content_description, detailsDescription));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
if (event != null) {
|
||||
Context context = view.getContext();
|
||||
Intent intent = new Intent(context, EventDetailsActivity.class)
|
||||
.putExtra(EventDetailsActivity.EXTRA_EVENT, event);
|
||||
|
@ -177,3 +211,4 @@ public class EventsAdapter extends RecyclerViewCursorAdapter<EventsAdapter.ViewH
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,80 +0,0 @@
|
|||
package be.digitalia.fosdem.adapters;
|
||||
|
||||
import android.database.Cursor;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
|
||||
/**
|
||||
* Simplified CursorAdapter designed for RecyclerView.
|
||||
*
|
||||
* @author Christophe Beyls
|
||||
*/
|
||||
public abstract class RecyclerViewCursorAdapter<VH extends RecyclerView.ViewHolder> extends RecyclerView.Adapter<VH> {
|
||||
|
||||
private Cursor cursor;
|
||||
private int rowIDColumn = -1;
|
||||
|
||||
public RecyclerViewCursorAdapter() {
|
||||
setHasStableIds(true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Swap in a new Cursor, returning the old Cursor.
|
||||
* The old cursor is not closed.
|
||||
*
|
||||
* @return The previously set Cursor, if any.
|
||||
* If the given new Cursor is the same instance as the previously set
|
||||
* Cursor, null is also returned.
|
||||
*/
|
||||
public Cursor swapCursor(Cursor newCursor) {
|
||||
if (newCursor == cursor) {
|
||||
return null;
|
||||
}
|
||||
Cursor oldCursor = cursor;
|
||||
cursor = newCursor;
|
||||
rowIDColumn = (newCursor == null) ? -1 : newCursor.getColumnIndexOrThrow("_id");
|
||||
notifyDataSetChanged();
|
||||
return oldCursor;
|
||||
}
|
||||
|
||||
public Cursor getCursor() {
|
||||
return cursor;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getItemCount() {
|
||||
return (cursor == null) ? 0 : cursor.getCount();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The cursor initialized to the specified position.
|
||||
*/
|
||||
public Object getItem(int position) {
|
||||
if (cursor != null) {
|
||||
cursor.moveToPosition(position);
|
||||
}
|
||||
return cursor;
|
||||
}
|
||||
|
||||
@Override
|
||||
public long getItemId(int position) {
|
||||
if ((cursor != null) && cursor.moveToPosition(position)) {
|
||||
return cursor.getLong(rowIDColumn);
|
||||
}
|
||||
return RecyclerView.NO_ID;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(@NonNull VH holder, int position) {
|
||||
if (cursor == null) {
|
||||
throw new IllegalStateException("this should only be called when the cursor is not null");
|
||||
}
|
||||
if (!cursor.moveToPosition(position)) {
|
||||
throw new IllegalStateException("couldn't move cursor to position " + position);
|
||||
}
|
||||
onBindViewHolder(holder, cursor);
|
||||
}
|
||||
|
||||
public abstract void onBindViewHolder(VH holder, Cursor cursor);
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
package be.digitalia.fosdem.adapters;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.core.util.ObjectsCompat;
|
||||
import androidx.recyclerview.widget.DiffUtil;
|
||||
|
||||
/**
|
||||
* Implementation of DiffUtil.ItemCallback which uses Object.equals() to determine if items are the same.
|
||||
*/
|
||||
public abstract class SimpleItemCallback<T> extends DiffUtil.ItemCallback<T> {
|
||||
@Override
|
||||
public boolean areItemsTheSame(@NonNull T oldItem, @NonNull T newItem) {
|
||||
return ObjectsCompat.equals(oldItem, newItem);
|
||||
}
|
||||
}
|
|
@ -2,7 +2,6 @@ package be.digitalia.fosdem.adapters;
|
|||
|
||||
import android.content.Context;
|
||||
import android.content.res.TypedArray;
|
||||
import android.database.Cursor;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.graphics.drawable.LayerDrawable;
|
||||
import android.text.TextUtils;
|
||||
|
@ -10,46 +9,60 @@ import android.view.LayoutInflater;
|
|||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.TextView;
|
||||
|
||||
import java.text.DateFormat;
|
||||
import java.util.List;
|
||||
|
||||
import androidx.annotation.DrawableRes;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.appcompat.content.res.AppCompatResources;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.core.util.ObjectsCompat;
|
||||
import androidx.core.view.ViewCompat;
|
||||
import androidx.core.widget.TextViewCompat;
|
||||
import androidx.recyclerview.widget.DiffUtil;
|
||||
import androidx.recyclerview.widget.ListAdapter;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import be.digitalia.fosdem.R;
|
||||
import be.digitalia.fosdem.db.DatabaseManager;
|
||||
import be.digitalia.fosdem.model.Event;
|
||||
import be.digitalia.fosdem.model.StatusEvent;
|
||||
import be.digitalia.fosdem.utils.DateUtils;
|
||||
|
||||
public class TrackScheduleAdapter extends RecyclerViewCursorAdapter<TrackScheduleAdapter.ViewHolder> {
|
||||
import java.text.DateFormat;
|
||||
import java.util.List;
|
||||
|
||||
public class TrackScheduleAdapter extends ListAdapter<StatusEvent, TrackScheduleAdapter.ViewHolder> {
|
||||
|
||||
public interface EventClickListener {
|
||||
void onEventClick(int position, Event event);
|
||||
}
|
||||
|
||||
private static final DiffUtil.ItemCallback<StatusEvent> DIFF_CALLBACK = new SimpleItemCallback<StatusEvent>() {
|
||||
@Override
|
||||
public boolean areContentsTheSame(@NonNull StatusEvent oldItem, @NonNull StatusEvent newItem) {
|
||||
final Event oldEvent = oldItem.getEvent();
|
||||
final Event newEvent = newItem.getEvent();
|
||||
return ObjectsCompat.equals(oldEvent.getTitle(), newEvent.getTitle())
|
||||
&& ObjectsCompat.equals(oldEvent.getPersonsSummary(), newEvent.getPersonsSummary())
|
||||
&& ObjectsCompat.equals(oldEvent.getRoomName(), newEvent.getRoomName())
|
||||
&& ObjectsCompat.equals(oldEvent.getStartTime(), newEvent.getStartTime())
|
||||
&& oldItem.isBookmarked() == newItem.isBookmarked();
|
||||
}
|
||||
};
|
||||
private static final Object TIME_COLORS_PAYLOAD = new Object();
|
||||
private static final Object SELECTION_PAYLOAD = new Object();
|
||||
|
||||
private final LayoutInflater inflater;
|
||||
private final DateFormat timeDateFormat;
|
||||
private final int timeBackgroundColor;
|
||||
private final int timeForegroundColor;
|
||||
private final int timeRunningBackgroundColor;
|
||||
private final int timeRunningForegroundColor;
|
||||
final DateFormat timeDateFormat;
|
||||
final int timeBackgroundColor;
|
||||
final int timeForegroundColor;
|
||||
final int timeRunningBackgroundColor;
|
||||
final int timeRunningForegroundColor;
|
||||
@Nullable
|
||||
private final EventClickListener listener;
|
||||
final EventClickListener listener;
|
||||
|
||||
private long currentTime = -1L;
|
||||
private long selectedId = -1L;
|
||||
|
||||
public TrackScheduleAdapter(Context context, @Nullable EventClickListener listener) {
|
||||
inflater = LayoutInflater.from(context);
|
||||
super(DIFF_CALLBACK);
|
||||
setHasStableIds(true);
|
||||
timeDateFormat = DateUtils.getTimeDateFormat(context);
|
||||
timeBackgroundColor = ContextCompat.getColor(context, R.color.schedule_time_background);
|
||||
timeRunningBackgroundColor = ContextCompat.getColor(context, R.color.schedule_time_running_background);
|
||||
|
@ -99,63 +112,24 @@ public class TrackScheduleAdapter extends RecyclerViewCursorAdapter<TrackSchedul
|
|||
}
|
||||
|
||||
@Override
|
||||
public Event getItem(int position) {
|
||||
return DatabaseManager.toEvent((Cursor) super.getItem(position));
|
||||
public long getItemId(int position) {
|
||||
return getItem(position).getEvent().getId();
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||
View view = inflater.inflate(R.layout.item_schedule_event, parent, false);
|
||||
return new ViewHolder(view, R.drawable.activated_background, listener);
|
||||
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_schedule_event, parent, false);
|
||||
return new ViewHolder(view, R.drawable.activated_background);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(ViewHolder holder, Cursor cursor) {
|
||||
Context context = holder.itemView.getContext();
|
||||
Event event = DatabaseManager.toEvent(cursor, holder.event);
|
||||
holder.event = event;
|
||||
|
||||
holder.time.setText(timeDateFormat.format(event.getStartTime()));
|
||||
bindTimeColors(holder, event);
|
||||
|
||||
holder.title.setText(event.getTitle());
|
||||
boolean isBookmarked = DatabaseManager.toBookmarkStatus(cursor);
|
||||
Drawable bookmarkDrawable = isBookmarked
|
||||
? AppCompatResources.getDrawable(context, R.drawable.ic_bookmark_grey600_24dp)
|
||||
: null;
|
||||
TextViewCompat.setCompoundDrawablesRelativeWithIntrinsicBounds(holder.title, null, null, bookmarkDrawable, null);
|
||||
holder.title.setContentDescription(isBookmarked
|
||||
? context.getString(R.string.in_bookmarks_content_description, event.getTitle())
|
||||
: null
|
||||
);
|
||||
String personsSummary = event.getPersonsSummary();
|
||||
holder.persons.setText(personsSummary);
|
||||
holder.persons.setVisibility(TextUtils.isEmpty(personsSummary) ? View.GONE : View.VISIBLE);
|
||||
holder.room.setText(event.getRoomName());
|
||||
holder.room.setContentDescription(context.getString(R.string.room_content_description, event.getRoomName()));
|
||||
|
||||
bindSelection(holder, event);
|
||||
}
|
||||
|
||||
private void bindTimeColors(ViewHolder holder, Event event) {
|
||||
final TextView timeTextView = holder.time;
|
||||
if ((currentTime != -1L) && event.isRunningAtTime(currentTime)) {
|
||||
// Contrast colors for running event
|
||||
timeTextView.setBackgroundColor(timeRunningBackgroundColor);
|
||||
timeTextView.setTextColor(timeRunningForegroundColor);
|
||||
timeTextView.setContentDescription(timeTextView.getContext().getString(R.string.in_progress_content_description, timeTextView.getText()));
|
||||
} else {
|
||||
// Normal colors
|
||||
timeTextView.setBackgroundColor(timeBackgroundColor);
|
||||
timeTextView.setTextColor(timeForegroundColor);
|
||||
// Use text as content description
|
||||
timeTextView.setContentDescription(null);
|
||||
}
|
||||
}
|
||||
|
||||
private void bindSelection(ViewHolder holder, Event event) {
|
||||
holder.itemView.setActivated(event.getId() == selectedId);
|
||||
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
|
||||
final StatusEvent statusEvent = getItem(position);
|
||||
final Event event = statusEvent.getEvent();
|
||||
holder.bind(event, statusEvent.isBookmarked());
|
||||
holder.bindTimeColors(event, currentTime);
|
||||
holder.bindSelection(event.getId() == selectedId);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -163,28 +137,25 @@ public class TrackScheduleAdapter extends RecyclerViewCursorAdapter<TrackSchedul
|
|||
if (payloads.isEmpty()) {
|
||||
onBindViewHolder(holder, position);
|
||||
} else {
|
||||
if (holder.event != null) {
|
||||
final StatusEvent statusEvent = getItem(position);
|
||||
if (payloads.contains(TIME_COLORS_PAYLOAD)) {
|
||||
bindTimeColors(holder, holder.event);
|
||||
holder.bindTimeColors(statusEvent.getEvent(), currentTime);
|
||||
}
|
||||
if (payloads.contains(SELECTION_PAYLOAD)) {
|
||||
bindSelection(holder, holder.event);
|
||||
}
|
||||
holder.bindSelection(statusEvent.getEvent().getId() == selectedId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static class ViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener {
|
||||
TextView time;
|
||||
TextView title;
|
||||
TextView persons;
|
||||
TextView room;
|
||||
@Nullable
|
||||
EventClickListener listener;
|
||||
class ViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener {
|
||||
final TextView time;
|
||||
final TextView title;
|
||||
final TextView persons;
|
||||
final TextView room;
|
||||
|
||||
Event event;
|
||||
|
||||
public ViewHolder(@NonNull View itemView, @DrawableRes int activatedBackgroundResId, @Nullable EventClickListener listener) {
|
||||
ViewHolder(@NonNull View itemView, @DrawableRes int activatedBackgroundResId) {
|
||||
super(itemView);
|
||||
time = itemView.findViewById(R.id.time);
|
||||
title = itemView.findViewById(R.id.title);
|
||||
|
@ -206,7 +177,46 @@ public class TrackScheduleAdapter extends RecyclerViewCursorAdapter<TrackSchedul
|
|||
}
|
||||
ViewCompat.setBackground(itemView, newBackground);
|
||||
}
|
||||
this.listener = listener;
|
||||
}
|
||||
|
||||
void bind(@NonNull Event event, boolean isBookmarked) {
|
||||
Context context = itemView.getContext();
|
||||
this.event = event;
|
||||
|
||||
time.setText(timeDateFormat.format(event.getStartTime()));
|
||||
title.setText(event.getTitle());
|
||||
Drawable bookmarkDrawable = isBookmarked
|
||||
? AppCompatResources.getDrawable(context, R.drawable.ic_bookmark_grey600_24dp)
|
||||
: null;
|
||||
TextViewCompat.setCompoundDrawablesRelativeWithIntrinsicBounds(title, null, null, bookmarkDrawable, null);
|
||||
title.setContentDescription(isBookmarked
|
||||
? context.getString(R.string.in_bookmarks_content_description, event.getTitle())
|
||||
: null
|
||||
);
|
||||
String personsSummary = event.getPersonsSummary();
|
||||
persons.setText(personsSummary);
|
||||
persons.setVisibility(TextUtils.isEmpty(personsSummary) ? View.GONE : View.VISIBLE);
|
||||
room.setText(event.getRoomName());
|
||||
room.setContentDescription(context.getString(R.string.room_content_description, event.getRoomName()));
|
||||
}
|
||||
|
||||
void bindTimeColors(@NonNull Event event, long currentTime) {
|
||||
if ((currentTime != -1L) && event.isRunningAtTime(currentTime)) {
|
||||
// Contrast colors for running event
|
||||
time.setBackgroundColor(timeRunningBackgroundColor);
|
||||
time.setTextColor(timeRunningForegroundColor);
|
||||
time.setContentDescription(time.getContext().getString(R.string.in_progress_content_description, time.getText()));
|
||||
} else {
|
||||
// Normal colors
|
||||
time.setBackgroundColor(timeBackgroundColor);
|
||||
time.setTextColor(timeForegroundColor);
|
||||
// Use text as content description
|
||||
time.setContentDescription(null);
|
||||
}
|
||||
}
|
||||
|
||||
void bindSelection(boolean isSelected) {
|
||||
itemView.setActivated(isSelected);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
|
|
@ -1,18 +1,16 @@
|
|||
package be.digitalia.fosdem.alarms;
|
||||
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
|
||||
|
||||
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
|
||||
import androidx.preference.PreferenceManager;
|
||||
import be.digitalia.fosdem.db.DatabaseManager;
|
||||
import be.digitalia.fosdem.fragments.SettingsFragment;
|
||||
import be.digitalia.fosdem.model.Event;
|
||||
import be.digitalia.fosdem.services.AlarmIntentService;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
/**
|
||||
* This class monitors bookmarks and preferences changes to dispatch alarm update work to AlarmIntentService.
|
||||
*
|
||||
|
@ -23,26 +21,7 @@ public class FosdemAlarmManager implements OnSharedPreferenceChangeListener {
|
|||
private static FosdemAlarmManager instance;
|
||||
|
||||
private final Context context;
|
||||
private boolean isEnabled;
|
||||
|
||||
private final BroadcastReceiver scheduleRefreshedReceiver = new BroadcastReceiver() {
|
||||
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
// When the schedule DB is updated, update the alarms too
|
||||
startUpdateAlarms();
|
||||
}
|
||||
};
|
||||
|
||||
private final BroadcastReceiver bookmarksReceiver = new BroadcastReceiver() {
|
||||
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
// Dispatch the Bookmark broadcasts to the service
|
||||
Intent serviceIntent = new Intent(intent);
|
||||
AlarmIntentService.enqueueWork(context, serviceIntent);
|
||||
}
|
||||
};
|
||||
private volatile boolean isEnabled;
|
||||
|
||||
public static void init(Context context) {
|
||||
if (instance == null) {
|
||||
|
@ -58,9 +37,6 @@ public class FosdemAlarmManager implements OnSharedPreferenceChangeListener {
|
|||
this.context = context;
|
||||
SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context);
|
||||
isEnabled = sharedPreferences.getBoolean(SettingsFragment.KEY_PREF_NOTIFICATIONS_ENABLED, false);
|
||||
if (isEnabled) {
|
||||
registerReceivers();
|
||||
}
|
||||
sharedPreferences.registerOnSharedPreferenceChangeListener(this);
|
||||
}
|
||||
|
||||
|
@ -68,15 +44,40 @@ public class FosdemAlarmManager implements OnSharedPreferenceChangeListener {
|
|||
return isEnabled;
|
||||
}
|
||||
|
||||
public void onScheduleRefreshed() {
|
||||
if (isEnabled) {
|
||||
startUpdateAlarms();
|
||||
}
|
||||
}
|
||||
|
||||
public void onBookmarkAdded(Event event) {
|
||||
if (isEnabled) {
|
||||
Intent serviceIntent = new Intent(AlarmIntentService.ACTION_ADD_BOOKMARK)
|
||||
.putExtra(AlarmIntentService.EXTRA_EVENT_ID, event.getId());
|
||||
final Date startTime = event.getStartTime();
|
||||
if (startTime != null) {
|
||||
serviceIntent.putExtra(AlarmIntentService.EXTRA_EVENT_START_TIME, startTime.getTime());
|
||||
}
|
||||
AlarmIntentService.enqueueWork(context, serviceIntent);
|
||||
}
|
||||
}
|
||||
|
||||
public void onBookmarksRemoved(long[] eventIds) {
|
||||
if (isEnabled) {
|
||||
Intent serviceIntent = new Intent(AlarmIntentService.ACTION_REMOVE_BOOKMARKS)
|
||||
.putExtra(AlarmIntentService.EXTRA_EVENT_IDS, eventIds);
|
||||
AlarmIntentService.enqueueWork(context, serviceIntent);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
|
||||
if (SettingsFragment.KEY_PREF_NOTIFICATIONS_ENABLED.equals(key)) {
|
||||
isEnabled = sharedPreferences.getBoolean(SettingsFragment.KEY_PREF_NOTIFICATIONS_ENABLED, false);
|
||||
final boolean isEnabled = sharedPreferences.getBoolean(SettingsFragment.KEY_PREF_NOTIFICATIONS_ENABLED, false);
|
||||
this.isEnabled = isEnabled;
|
||||
if (isEnabled) {
|
||||
registerReceivers();
|
||||
startUpdateAlarms();
|
||||
} else {
|
||||
unregisterReceivers();
|
||||
startDisableAlarms();
|
||||
}
|
||||
} else if (SettingsFragment.KEY_PREF_NOTIFICATIONS_DELAY.equals(key)) {
|
||||
|
@ -84,22 +85,7 @@ public class FosdemAlarmManager implements OnSharedPreferenceChangeListener {
|
|||
}
|
||||
}
|
||||
|
||||
private void registerReceivers() {
|
||||
LocalBroadcastManager lbm = LocalBroadcastManager.getInstance(context);
|
||||
lbm.registerReceiver(scheduleRefreshedReceiver, new IntentFilter(DatabaseManager.ACTION_SCHEDULE_REFRESHED));
|
||||
IntentFilter filter = new IntentFilter();
|
||||
filter.addAction(DatabaseManager.ACTION_ADD_BOOKMARK);
|
||||
filter.addAction(DatabaseManager.ACTION_REMOVE_BOOKMARKS);
|
||||
lbm.registerReceiver(bookmarksReceiver, filter);
|
||||
}
|
||||
|
||||
private void unregisterReceivers() {
|
||||
LocalBroadcastManager lbm = LocalBroadcastManager.getInstance(context);
|
||||
lbm.unregisterReceiver(scheduleRefreshedReceiver);
|
||||
lbm.unregisterReceiver(bookmarksReceiver);
|
||||
}
|
||||
|
||||
void startUpdateAlarms() {
|
||||
private void startUpdateAlarms() {
|
||||
Intent serviceIntent = new Intent(AlarmIntentService.ACTION_UPDATE_ALARMS);
|
||||
AlarmIntentService.enqueueWork(context, serviceIntent);
|
||||
}
|
||||
|
|
|
@ -6,10 +6,11 @@ import androidx.annotation.MainThread;
|
|||
import androidx.annotation.WorkerThread;
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.MutableLiveData;
|
||||
import be.digitalia.fosdem.db.DatabaseManager;
|
||||
import be.digitalia.fosdem.db.AppDatabase;
|
||||
import be.digitalia.fosdem.db.ScheduleDao;
|
||||
import be.digitalia.fosdem.livedata.SingleEvent;
|
||||
import be.digitalia.fosdem.model.DetailedEvent;
|
||||
import be.digitalia.fosdem.model.DownloadScheduleResult;
|
||||
import be.digitalia.fosdem.model.Event;
|
||||
import be.digitalia.fosdem.model.RoomStatus;
|
||||
import be.digitalia.fosdem.parsers.EventsParser;
|
||||
import be.digitalia.fosdem.utils.HttpUtils;
|
||||
|
@ -55,10 +56,10 @@ public class FosdemApi {
|
|||
progress.postValue(-1);
|
||||
DownloadScheduleResult res = DownloadScheduleResult.error();
|
||||
try {
|
||||
DatabaseManager dbManager = DatabaseManager.getInstance();
|
||||
ScheduleDao scheduleDao = AppDatabase.getInstance(context).getScheduleDao();
|
||||
HttpUtils.HttpResult httpResult = HttpUtils.get(
|
||||
FosdemUrls.getSchedule(),
|
||||
dbManager.getLastModifiedTag(),
|
||||
scheduleDao.getLastModifiedTag(),
|
||||
new HttpUtils.ProgressUpdateListener() {
|
||||
@Override
|
||||
public void onProgressUpdate(int percent) {
|
||||
|
@ -72,8 +73,8 @@ public class FosdemApi {
|
|||
}
|
||||
|
||||
try {
|
||||
Iterable<Event> events = new EventsParser().parse(httpResult.inputStream);
|
||||
int count = dbManager.storeSchedule(events, httpResult.lastModified);
|
||||
Iterable<DetailedEvent> events = new EventsParser().parse(httpResult.inputStream);
|
||||
int count = scheduleDao.storeSchedule(events, httpResult.lastModified);
|
||||
res = DownloadScheduleResult.success(count);
|
||||
} finally {
|
||||
try {
|
||||
|
@ -106,12 +107,12 @@ public class FosdemApi {
|
|||
}
|
||||
|
||||
@MainThread
|
||||
public static LiveData<Map<String, RoomStatus>> getRoomStatuses() {
|
||||
public static LiveData<Map<String, RoomStatus>> getRoomStatuses(Context context) {
|
||||
if (roomStatuses == null) {
|
||||
// The room statuses will only be loaded when the event is live.
|
||||
// RoomStatusesLiveData uses the days from the database to determine it.
|
||||
roomStatuses = new RoomStatusesLiveData(DatabaseManager.getInstance().getDays());
|
||||
// Implementors: replace the above live with the next one to disable room status support
|
||||
roomStatuses = new RoomStatusesLiveData(AppDatabase.getInstance(context).getScheduleDao().getDays());
|
||||
// Implementors: replace the above line with the next one to disable room status support
|
||||
// roomStatuses = new MutableLiveData<>();
|
||||
}
|
||||
return roomStatuses;
|
||||
|
|
99
app/src/main/java/be/digitalia/fosdem/db/AppDatabase.java
Normal file
99
app/src/main/java/be/digitalia/fosdem/db/AppDatabase.java
Normal file
|
@ -0,0 +1,99 @@
|
|||
package be.digitalia.fosdem.db;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
|
||||
import androidx.room.Database;
|
||||
import androidx.room.Room;
|
||||
import androidx.room.RoomDatabase;
|
||||
import androidx.room.TypeConverters;
|
||||
import androidx.room.migration.Migration;
|
||||
import androidx.sqlite.db.SupportSQLiteDatabase;
|
||||
import be.digitalia.fosdem.db.converters.GlobalTypeConverters;
|
||||
import be.digitalia.fosdem.db.entities.Bookmark;
|
||||
import be.digitalia.fosdem.db.entities.EventEntity;
|
||||
import be.digitalia.fosdem.db.entities.EventTitles;
|
||||
import be.digitalia.fosdem.db.entities.EventToPerson;
|
||||
import be.digitalia.fosdem.model.Day;
|
||||
import be.digitalia.fosdem.model.Link;
|
||||
import be.digitalia.fosdem.model.Person;
|
||||
import be.digitalia.fosdem.model.Track;
|
||||
|
||||
@Database(entities = {EventEntity.class, EventTitles.class, Person.class, EventToPerson.class, Link.class, Track.class, Day.class, Bookmark.class}, version = 2, exportSchema = false)
|
||||
@TypeConverters({GlobalTypeConverters.class})
|
||||
public abstract class AppDatabase extends RoomDatabase {
|
||||
|
||||
private static final String DB_PREFS_FILE = "database";
|
||||
private static volatile AppDatabase INSTANCE;
|
||||
|
||||
static final Migration MIGRATION_1_2 = new Migration(1, 2) {
|
||||
@Override
|
||||
public void migrate(SupportSQLiteDatabase database) {
|
||||
// Events: make primary key and track_id not null
|
||||
database.execSQL("CREATE TABLE tmp_"
|
||||
+ EventEntity.TABLE_NAME
|
||||
+ " (id INTEGER PRIMARY KEY NOT NULL, day_index INTEGER NOT NULL, start_time INTEGER, end_time INTEGER, room_name TEXT, slug TEXT, track_id INTEGER NOT NULL, abstract TEXT, description TEXT);");
|
||||
database.execSQL("INSERT INTO tmp_" + EventEntity.TABLE_NAME + " SELECT * FROM " + EventEntity.TABLE_NAME);
|
||||
database.execSQL("DROP TABLE " + EventEntity.TABLE_NAME);
|
||||
database.execSQL("ALTER TABLE tmp_" + EventEntity.TABLE_NAME + " RENAME TO " + EventEntity.TABLE_NAME);
|
||||
database.execSQL("CREATE INDEX event_day_index_idx ON " + EventEntity.TABLE_NAME + " (day_index)");
|
||||
database.execSQL("CREATE INDEX event_start_time_idx ON " + EventEntity.TABLE_NAME + " (start_time)");
|
||||
database.execSQL("CREATE INDEX event_end_time_idx ON " + EventEntity.TABLE_NAME + " (end_time)");
|
||||
database.execSQL("CREATE INDEX event_track_id_idx ON " + EventEntity.TABLE_NAME + " (track_id)");
|
||||
|
||||
// Links: add explicit primary key
|
||||
database.execSQL("CREATE TABLE tmp_" + Link.TABLE_NAME + " (id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL, event_id INTEGER NOT NULL, url TEXT NOT NULL, description TEXT);");
|
||||
database.execSQL("INSERT INTO tmp_" + Link.TABLE_NAME + " SELECT `rowid` AS id, event_id, url, description FROM " + Link.TABLE_NAME);
|
||||
database.execSQL("DROP TABLE " + Link.TABLE_NAME);
|
||||
database.execSQL("ALTER TABLE tmp_" + Link.TABLE_NAME + " RENAME TO " + Link.TABLE_NAME);
|
||||
database.execSQL("CREATE INDEX link_event_id_idx ON " + Link.TABLE_NAME + " (event_id)");
|
||||
|
||||
// Tracks: make primary key not null
|
||||
database.execSQL("CREATE TABLE tmp_" + Track.TABLE_NAME + " (id INTEGER PRIMARY KEY NOT NULL, name TEXT NOT NULL, type TEXT NOT NULL);");
|
||||
database.execSQL("INSERT INTO tmp_" + Track.TABLE_NAME + " SELECT * FROM " + Track.TABLE_NAME);
|
||||
database.execSQL("DROP TABLE " + Track.TABLE_NAME);
|
||||
database.execSQL("ALTER TABLE tmp_" + Track.TABLE_NAME + " RENAME TO " + Track.TABLE_NAME);
|
||||
database.execSQL("CREATE UNIQUE INDEX track_main_idx ON " + Track.TABLE_NAME + " (name, type)");
|
||||
|
||||
// Days: make primary key not null and rename _index to index
|
||||
database.execSQL("CREATE TABLE tmp_" + Day.TABLE_NAME + " (`index` INTEGER PRIMARY KEY NOT NULL, date INTEGER NOT NULL);");
|
||||
database.execSQL("INSERT INTO tmp_" + Day.TABLE_NAME + " SELECT _index as `index`, date FROM " + Day.TABLE_NAME);
|
||||
database.execSQL("DROP TABLE " + Day.TABLE_NAME);
|
||||
database.execSQL("ALTER TABLE tmp_" + Day.TABLE_NAME + " RENAME TO " + Day.TABLE_NAME);
|
||||
|
||||
// Bookmarks: make primary key not null
|
||||
database.execSQL("CREATE TABLE tmp_" + Bookmark.TABLE_NAME + " (event_id INTEGER PRIMARY KEY NOT NULL);");
|
||||
database.execSQL("INSERT INTO tmp_" + Bookmark.TABLE_NAME + " SELECT * FROM " + Bookmark.TABLE_NAME);
|
||||
database.execSQL("DROP TABLE " + Bookmark.TABLE_NAME);
|
||||
database.execSQL("ALTER TABLE tmp_" + Bookmark.TABLE_NAME + " RENAME TO " + Bookmark.TABLE_NAME);
|
||||
}
|
||||
};
|
||||
|
||||
private SharedPreferences sharedPreferences;
|
||||
|
||||
public SharedPreferences getSharedPreferences() {
|
||||
return sharedPreferences;
|
||||
}
|
||||
|
||||
public static AppDatabase getInstance(Context context) {
|
||||
AppDatabase res = INSTANCE;
|
||||
if (res == null) {
|
||||
synchronized (AppDatabase.class) {
|
||||
res = INSTANCE;
|
||||
if (res == null) {
|
||||
res = Room.databaseBuilder(context.getApplicationContext(), AppDatabase.class, "fosdem.sqlite")
|
||||
.addMigrations(MIGRATION_1_2)
|
||||
.setJournalMode(JournalMode.TRUNCATE)
|
||||
.build();
|
||||
res.sharedPreferences = context.getApplicationContext().getSharedPreferences(DB_PREFS_FILE, Context.MODE_PRIVATE);
|
||||
INSTANCE = res;
|
||||
}
|
||||
}
|
||||
}
|
||||
return res;
|
||||
}
|
||||
|
||||
public abstract ScheduleDao getScheduleDao();
|
||||
|
||||
public abstract BookmarksDao getBookmarksDao();
|
||||
}
|
86
app/src/main/java/be/digitalia/fosdem/db/BookmarksDao.java
Normal file
86
app/src/main/java/be/digitalia/fosdem/db/BookmarksDao.java
Normal file
|
@ -0,0 +1,86 @@
|
|||
package be.digitalia.fosdem.db;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.WorkerThread;
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.room.Dao;
|
||||
import androidx.room.Delete;
|
||||
import androidx.room.Insert;
|
||||
import androidx.room.Query;
|
||||
import be.digitalia.fosdem.alarms.FosdemAlarmManager;
|
||||
import be.digitalia.fosdem.db.entities.Bookmark;
|
||||
import be.digitalia.fosdem.model.AlarmInfo;
|
||||
import be.digitalia.fosdem.model.Event;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@Dao
|
||||
public abstract class BookmarksDao {
|
||||
|
||||
/**
|
||||
* Returns the bookmarks.
|
||||
*
|
||||
* @param minStartTime When greater than 0, only return the events starting after this time.
|
||||
*/
|
||||
@Query("SELECT e.id, e.start_time, e.end_time, e.room_name, e.slug, et.title, et.subtitle, e.abstract, e.description"
|
||||
+ ", GROUP_CONCAT(p.name, ', ') AS persons, e.day_index, d.date AS day_date, e.track_id, t.name AS track_name, t.type AS track_type"
|
||||
+ " FROM bookmarks b"
|
||||
+ " JOIN events e ON b.event_id = e.id"
|
||||
+ " JOIN events_titles et ON e.id = et.`rowid`"
|
||||
+ " JOIN days d ON e.day_index = d.`index`"
|
||||
+ " JOIN tracks t ON e.track_id = t.id"
|
||||
+ " LEFT JOIN events_persons ep ON e.id = ep.event_id"
|
||||
+ " LEFT JOIN persons p ON ep.person_id = p.`rowid`"
|
||||
+ " WHERE e.start_time > :minStartTime"
|
||||
+ " GROUP BY e.id"
|
||||
+ " ORDER BY e.start_time ASC")
|
||||
public abstract LiveData<List<Event>> getBookmarks(long minStartTime);
|
||||
|
||||
@Query("SELECT e.id, e.start_time, e.end_time, e.room_name, e.slug, et.title, et.subtitle, e.abstract, e.description"
|
||||
+ ", GROUP_CONCAT(p.name, ', ') AS persons, e.day_index, d.date AS day_date, e.track_id, t.name AS track_name, t.type AS track_type"
|
||||
+ " FROM bookmarks b"
|
||||
+ " JOIN events e ON b.event_id = e.id"
|
||||
+ " JOIN events_titles et ON e.id = et.`rowid`"
|
||||
+ " JOIN days d ON e.day_index = d.`index`"
|
||||
+ " JOIN tracks t ON e.track_id = t.id"
|
||||
+ " LEFT JOIN events_persons ep ON e.id = ep.event_id"
|
||||
+ " LEFT JOIN persons p ON ep.person_id = p.`rowid`"
|
||||
+ " GROUP BY e.id"
|
||||
+ " ORDER BY e.start_time ASC")
|
||||
@WorkerThread
|
||||
public abstract Event[] getBookmarks();
|
||||
|
||||
@Query("SELECT b.event_id, e.start_time"
|
||||
+ " FROM bookmarks b"
|
||||
+ " JOIN events e ON b.event_id = e.id"
|
||||
+ " WHERE e.start_time > :minStartTime"
|
||||
+ " ORDER BY e.start_time ASC")
|
||||
@WorkerThread
|
||||
public abstract AlarmInfo[] getBookmarksAlarmInfo(long minStartTime);
|
||||
|
||||
@Query("SELECT COUNT(*) FROM bookmarks WHERE event_id = :event")
|
||||
public abstract LiveData<Boolean> getBookmarkStatus(Event event);
|
||||
|
||||
public void addBookmark(@NonNull Event event) {
|
||||
if (addBookmarkInternal(new Bookmark(event.getId())) != -1L) {
|
||||
FosdemAlarmManager.getInstance().onBookmarkAdded(event);
|
||||
}
|
||||
}
|
||||
|
||||
@Insert
|
||||
protected abstract long addBookmarkInternal(Bookmark bookmark);
|
||||
|
||||
@Delete
|
||||
public void removeBookmark(@NonNull Event event) {
|
||||
removeBookmarks(event.getId());
|
||||
}
|
||||
|
||||
public void removeBookmarks(@NonNull long... eventIds) {
|
||||
if (removeBookmarksInternal(eventIds) > 0) {
|
||||
FosdemAlarmManager.getInstance().onBookmarksRemoved(eventIds);
|
||||
}
|
||||
}
|
||||
|
||||
@Query("DELETE FROM bookmarks WHERE event_id IN (:eventIds)")
|
||||
protected abstract int removeBookmarksInternal(long[] eventIds);
|
||||
}
|
|
@ -1,65 +0,0 @@
|
|||
package be.digitalia.fosdem.db;
|
||||
|
||||
import android.content.Context;
|
||||
import android.database.sqlite.SQLiteDatabase;
|
||||
import android.database.sqlite.SQLiteOpenHelper;
|
||||
|
||||
public class DatabaseHelper extends SQLiteOpenHelper {
|
||||
|
||||
private static final String DATABASE_NAME = "fosdem.sqlite";
|
||||
private static final int DATABASE_VERSION = 1;
|
||||
|
||||
public static final String EVENTS_TABLE_NAME = "events";
|
||||
public static final String EVENTS_TITLES_TABLE_NAME = "events_titles";
|
||||
public static final String PERSONS_TABLE_NAME = "persons";
|
||||
public static final String EVENTS_PERSONS_TABLE_NAME = "events_persons";
|
||||
public static final String LINKS_TABLE_NAME = "links";
|
||||
public static final String TRACKS_TABLE_NAME = "tracks";
|
||||
public static final String DAYS_TABLE_NAME = "days";
|
||||
public static final String BOOKMARKS_TABLE_NAME = "bookmarks";
|
||||
|
||||
public DatabaseHelper(Context context) {
|
||||
super(context, DATABASE_NAME, null, DATABASE_VERSION);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(SQLiteDatabase database) {
|
||||
// Events
|
||||
database.execSQL("CREATE TABLE "
|
||||
+ EVENTS_TABLE_NAME
|
||||
+ " (id INTEGER PRIMARY KEY, day_index INTEGER NOT NULL, start_time INTEGER, end_time INTEGER, room_name TEXT, slug TEXT, track_id INTEGER, abstract TEXT, description TEXT);");
|
||||
database.execSQL("CREATE INDEX event_day_index_idx ON " + EVENTS_TABLE_NAME + " (day_index)");
|
||||
database.execSQL("CREATE INDEX event_start_time_idx ON " + EVENTS_TABLE_NAME + " (start_time)");
|
||||
database.execSQL("CREATE INDEX event_end_time_idx ON " + EVENTS_TABLE_NAME + " (end_time)");
|
||||
database.execSQL("CREATE INDEX event_track_id_idx ON " + EVENTS_TABLE_NAME + " (track_id)");
|
||||
// Secondary table with fulltext index on the titles
|
||||
database.execSQL("CREATE VIRTUAL TABLE " + EVENTS_TITLES_TABLE_NAME + " USING fts3(title TEXT, subtitle TEXT);");
|
||||
|
||||
// Persons
|
||||
database.execSQL("CREATE VIRTUAL TABLE " + PERSONS_TABLE_NAME + " USING fts3(name TEXT);");
|
||||
|
||||
// Events-to-Persons
|
||||
database.execSQL("CREATE TABLE " + EVENTS_PERSONS_TABLE_NAME
|
||||
+ " (event_id INTEGER NOT NULL, person_id INTEGER NOT NULL, PRIMARY KEY(event_id, person_id));");
|
||||
database.execSQL("CREATE INDEX event_person_person_id_idx ON " + EVENTS_PERSONS_TABLE_NAME + " (person_id)");
|
||||
|
||||
// Links
|
||||
database.execSQL("CREATE TABLE " + LINKS_TABLE_NAME + " (event_id INTEGER NOT NULL, url TEXT NOT NULL, description TEXT);");
|
||||
database.execSQL("CREATE INDEX link_event_id_idx ON " + LINKS_TABLE_NAME + " (event_id)");
|
||||
|
||||
// Tracks
|
||||
database.execSQL("CREATE TABLE " + TRACKS_TABLE_NAME + " (id INTEGER PRIMARY KEY, name TEXT NOT NULL, type TEXT NOT NULL);");
|
||||
database.execSQL("CREATE UNIQUE INDEX track_main_idx ON " + TRACKS_TABLE_NAME + " (name, type)");
|
||||
|
||||
// Days
|
||||
database.execSQL("CREATE TABLE " + DAYS_TABLE_NAME + " (_index INTEGER PRIMARY KEY, date INTEGER NOT NULL);");
|
||||
|
||||
// Bookmarks
|
||||
database.execSQL("CREATE TABLE " + BOOKMARKS_TABLE_NAME + " (event_id INTEGER PRIMARY KEY);");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onUpgrade(SQLiteDatabase database, int oldVersion, int newVersion) {
|
||||
// Nothing to upgrade yet
|
||||
}
|
||||
}
|
|
@ -1,872 +0,0 @@
|
|||
package be.digitalia.fosdem.db;
|
||||
|
||||
import android.app.SearchManager;
|
||||
import android.content.ContentValues;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.content.SharedPreferences;
|
||||
import android.database.Cursor;
|
||||
import android.database.DatabaseUtils;
|
||||
import android.database.sqlite.SQLiteConstraintException;
|
||||
import android.database.sqlite.SQLiteDatabase;
|
||||
import android.database.sqlite.SQLiteStatement;
|
||||
import android.provider.BaseColumns;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.WorkerThread;
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
|
||||
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;
|
||||
import be.digitalia.fosdem.model.Person;
|
||||
import be.digitalia.fosdem.model.Track;
|
||||
import be.digitalia.fosdem.utils.DateUtils;
|
||||
|
||||
/**
|
||||
* Here comes the badass SQL.
|
||||
*
|
||||
* @author Christophe Beyls
|
||||
*/
|
||||
public class DatabaseManager {
|
||||
|
||||
public static final String ACTION_SCHEDULE_REFRESHED = BuildConfig.APPLICATION_ID + ".action.SCHEDULE_REFRESHED";
|
||||
public static final String ACTION_ADD_BOOKMARK = BuildConfig.APPLICATION_ID + ".action.ADD_BOOKMARK";
|
||||
public static final String EXTRA_EVENT_ID = "event_id";
|
||||
public static final String EXTRA_EVENT_START_TIME = "event_start";
|
||||
public static final String ACTION_REMOVE_BOOKMARKS = BuildConfig.APPLICATION_ID + ".action.REMOVE_BOOKMARKS";
|
||||
public static final String EXTRA_EVENT_IDS = "event_ids";
|
||||
|
||||
private static final String DB_PREFS_FILE = "database";
|
||||
private static final String LAST_UPDATE_TIME_PREF = "last_update_time";
|
||||
private static final String LAST_MODIFIED_TAG_PREF = "last_modified_tag";
|
||||
|
||||
private static DatabaseManager instance;
|
||||
|
||||
private final Context context;
|
||||
private final DatabaseHelper helper;
|
||||
|
||||
public static void init(Context context) {
|
||||
if (instance == null) {
|
||||
instance = new DatabaseManager(context);
|
||||
}
|
||||
}
|
||||
|
||||
public static DatabaseManager getInstance() {
|
||||
return instance;
|
||||
}
|
||||
|
||||
private DatabaseManager(Context context) {
|
||||
this.context = context;
|
||||
helper = new DatabaseHelper(context);
|
||||
}
|
||||
|
||||
private static final String TRACK_INSERT_STATEMENT = "INSERT INTO " + DatabaseHelper.TRACKS_TABLE_NAME + " (id, name, type) VALUES (?, ?, ?);";
|
||||
private static final String EVENT_INSERT_STATEMENT = "INSERT INTO " + DatabaseHelper.EVENTS_TABLE_NAME
|
||||
+ " (id, day_index, start_time, end_time, room_name, slug, track_id, abstract, description) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?);";
|
||||
private static final String EVENT_TITLES_INSERT_STATEMENT = "INSERT INTO " + DatabaseHelper.EVENTS_TITLES_TABLE_NAME
|
||||
+ " (rowid, title, subtitle) VALUES (?, ?, ?);";
|
||||
private static final String EVENT_PERSON_INSERT_STATEMENT = "INSERT INTO " + DatabaseHelper.EVENTS_PERSONS_TABLE_NAME
|
||||
+ " (event_id, person_id) VALUES (?, ?);";
|
||||
// Ignore conflicts in case of existing person
|
||||
private static final String PERSON_INSERT_STATEMENT = "INSERT OR IGNORE INTO " + DatabaseHelper.PERSONS_TABLE_NAME + " (rowid, name) VALUES (?, ?);";
|
||||
private static final String LINK_INSERT_STATEMENT = "INSERT INTO " + DatabaseHelper.LINKS_TABLE_NAME + " (event_id, url, description) VALUES (?, ?, ?);";
|
||||
|
||||
private static void bindString(SQLiteStatement statement, int index, String value) {
|
||||
if (value == null) {
|
||||
statement.bindNull(index);
|
||||
} else {
|
||||
statement.bindString(index, value);
|
||||
}
|
||||
}
|
||||
|
||||
private SharedPreferences getSharedPreferences() {
|
||||
return context.getSharedPreferences(DB_PREFS_FILE, Context.MODE_PRIVATE);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The last update time in milliseconds since EPOCH, or -1 if not available.
|
||||
*/
|
||||
public long getLastUpdateTime() {
|
||||
return getSharedPreferences().getLong(LAST_UPDATE_TIME_PREF, -1L);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The time identifier of the current version of the database.
|
||||
*/
|
||||
public String getLastModifiedTag() {
|
||||
return getSharedPreferences().getString(LAST_MODIFIED_TAG_PREF, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Stores the schedule to the database.
|
||||
*
|
||||
* @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();
|
||||
try {
|
||||
// 1: Delete the previous schedule
|
||||
clearSchedule(db);
|
||||
|
||||
// Compile the insert statements for the big tables
|
||||
final SQLiteStatement trackInsertStatement = db.compileStatement(TRACK_INSERT_STATEMENT);
|
||||
final SQLiteStatement eventInsertStatement = db.compileStatement(EVENT_INSERT_STATEMENT);
|
||||
final SQLiteStatement eventTitlesInsertStatement = db.compileStatement(EVENT_TITLES_INSERT_STATEMENT);
|
||||
final SQLiteStatement eventPersonInsertStatement = db.compileStatement(EVENT_PERSON_INSERT_STATEMENT);
|
||||
final SQLiteStatement personInsertStatement = db.compileStatement(PERSON_INSERT_STATEMENT);
|
||||
final SQLiteStatement linkInsertStatement = db.compileStatement(LINK_INSERT_STATEMENT);
|
||||
|
||||
// 2: Insert the events
|
||||
int totalEvents = 0;
|
||||
Map<Track, Long> tracks = new HashMap<>();
|
||||
long nextTrackId = 0L;
|
||||
long minEventId = Long.MAX_VALUE;
|
||||
Set<Day> days = new HashSet<>(2);
|
||||
|
||||
for (Event event : events) {
|
||||
// 2a: Retrieve or insert Track
|
||||
Track track = event.getTrack();
|
||||
Long trackId = tracks.get(track);
|
||||
if (trackId == null) {
|
||||
// New track
|
||||
nextTrackId++;
|
||||
trackId = nextTrackId;
|
||||
trackInsertStatement.clearBindings();
|
||||
trackInsertStatement.bindLong(1, nextTrackId);
|
||||
bindString(trackInsertStatement, 2, track.getName());
|
||||
bindString(trackInsertStatement, 3, track.getType().name());
|
||||
if (trackInsertStatement.executeInsert() != -1L) {
|
||||
tracks.put(track, trackId);
|
||||
}
|
||||
}
|
||||
|
||||
// 2b: Insert main event
|
||||
eventInsertStatement.clearBindings();
|
||||
long eventId = event.getId();
|
||||
if (eventId < minEventId) {
|
||||
minEventId = eventId;
|
||||
}
|
||||
eventInsertStatement.bindLong(1, eventId);
|
||||
Day day = event.getDay();
|
||||
days.add(day);
|
||||
eventInsertStatement.bindLong(2, day.getIndex());
|
||||
Date time = event.getStartTime();
|
||||
if (time == null) {
|
||||
eventInsertStatement.bindNull(3);
|
||||
} else {
|
||||
eventInsertStatement.bindLong(3, time.getTime());
|
||||
}
|
||||
time = event.getEndTime();
|
||||
if (time == null) {
|
||||
eventInsertStatement.bindNull(4);
|
||||
} else {
|
||||
eventInsertStatement.bindLong(4, time.getTime());
|
||||
}
|
||||
bindString(eventInsertStatement, 5, event.getRoomName());
|
||||
bindString(eventInsertStatement, 6, event.getSlug());
|
||||
eventInsertStatement.bindLong(7, trackId);
|
||||
bindString(eventInsertStatement, 8, event.getAbstractText());
|
||||
bindString(eventInsertStatement, 9, event.getDescription());
|
||||
|
||||
if (eventInsertStatement.executeInsert() != -1L) {
|
||||
// 2c: Insert fulltext fields
|
||||
eventTitlesInsertStatement.clearBindings();
|
||||
eventTitlesInsertStatement.bindLong(1, eventId);
|
||||
bindString(eventTitlesInsertStatement, 2, event.getTitle());
|
||||
bindString(eventTitlesInsertStatement, 3, event.getSubTitle());
|
||||
eventTitlesInsertStatement.executeInsert();
|
||||
|
||||
// 2d: Insert persons
|
||||
for (Person person : event.getPersons()) {
|
||||
eventPersonInsertStatement.clearBindings();
|
||||
eventPersonInsertStatement.bindLong(1, eventId);
|
||||
long personId = person.getId();
|
||||
eventPersonInsertStatement.bindLong(2, personId);
|
||||
eventPersonInsertStatement.executeInsert();
|
||||
|
||||
personInsertStatement.clearBindings();
|
||||
personInsertStatement.bindLong(1, personId);
|
||||
bindString(personInsertStatement, 2, person.getName());
|
||||
try {
|
||||
personInsertStatement.executeInsert();
|
||||
} catch (SQLiteConstraintException e) {
|
||||
// Older Android versions may not ignore an existing person
|
||||
}
|
||||
}
|
||||
|
||||
// 2e: Insert links
|
||||
for (Link link : event.getLinks()) {
|
||||
linkInsertStatement.clearBindings();
|
||||
linkInsertStatement.bindLong(1, eventId);
|
||||
bindString(linkInsertStatement, 2, link.getUrl());
|
||||
bindString(linkInsertStatement, 3, link.getDescription());
|
||||
linkInsertStatement.executeInsert();
|
||||
}
|
||||
}
|
||||
|
||||
totalEvents++;
|
||||
}
|
||||
|
||||
// 3: Insert collected days
|
||||
ContentValues values = new ContentValues();
|
||||
for (Day day : days) {
|
||||
values.clear();
|
||||
values.put("_index", day.getIndex());
|
||||
Date date = day.getDate();
|
||||
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) {
|
||||
String[] whereArgs = new String[]{String.valueOf(minEventId)};
|
||||
db.delete(DatabaseHelper.BOOKMARKS_TABLE_NAME, "event_id < ?", whereArgs);
|
||||
}
|
||||
|
||||
if (totalEvents > 0) {
|
||||
db.setTransactionSuccessful();
|
||||
isComplete = true;
|
||||
}
|
||||
|
||||
return totalEvents;
|
||||
} finally {
|
||||
db.endTransaction();
|
||||
|
||||
if (isComplete) {
|
||||
// Update/clear cache
|
||||
daysLiveData.postValue(daysList);
|
||||
// Set last update time and server's last modified tag
|
||||
getSharedPreferences().edit()
|
||||
.putLong(LAST_UPDATE_TIME_PREF, System.currentTimeMillis())
|
||||
.putString(LAST_MODIFIED_TAG_PREF, lastModifiedTag)
|
||||
.apply();
|
||||
|
||||
LocalBroadcastManager.getInstance(context).sendBroadcast(new Intent(ACTION_SCHEDULE_REFRESHED));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
public void clearSchedule() {
|
||||
SQLiteDatabase db = helper.getWritableDatabase();
|
||||
db.beginTransaction();
|
||||
try {
|
||||
clearSchedule(db);
|
||||
|
||||
db.setTransactionSuccessful();
|
||||
|
||||
daysLiveData.postValue(Collections.<Day>emptyList());
|
||||
getSharedPreferences().edit()
|
||||
.remove(LAST_UPDATE_TIME_PREF)
|
||||
.apply();
|
||||
} finally {
|
||||
db.endTransaction();
|
||||
|
||||
LocalBroadcastManager.getInstance(context).sendBroadcast(new Intent(ACTION_SCHEDULE_REFRESHED));
|
||||
}
|
||||
}
|
||||
|
||||
private static void clearSchedule(SQLiteDatabase db) {
|
||||
db.delete(DatabaseHelper.EVENTS_TABLE_NAME, null, null);
|
||||
db.delete(DatabaseHelper.EVENTS_TITLES_TABLE_NAME, null, null);
|
||||
db.delete(DatabaseHelper.PERSONS_TABLE_NAME, null, null);
|
||||
db.delete(DatabaseHelper.EVENTS_PERSONS_TABLE_NAME, null, null);
|
||||
db.delete(DatabaseHelper.LINKS_TABLE_NAME, null, null);
|
||||
db.delete(DatabaseHelper.TRACKS_TABLE_NAME, null, null);
|
||||
db.delete(DatabaseHelper.DAYS_TABLE_NAME, null, null);
|
||||
}
|
||||
|
||||
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 LiveData<List<Day>> getDays() {
|
||||
return daysLiveData;
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
public int getYear() {
|
||||
// Use the current year by default
|
||||
long date = System.currentTimeMillis();
|
||||
|
||||
// Compute from cached days if available
|
||||
List<Day> days = daysLiveData.getValue();
|
||||
if (days != null) {
|
||||
if (days.size() > 0) {
|
||||
date = days.get(0).getDate().getTime();
|
||||
}
|
||||
} else {
|
||||
// Perform a quick DB query to retrieve the time of the first day
|
||||
try {
|
||||
date = DatabaseUtils.longForQuery(helper.getReadableDatabase(),
|
||||
"SELECT date FROM " + DatabaseHelper.DAYS_TABLE_NAME + " ORDER BY _index ASC LIMIT 1", null);
|
||||
} catch (Exception ignore) {
|
||||
}
|
||||
}
|
||||
|
||||
return DateUtils.getYear(date);
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
public Cursor getTracks(Day day) {
|
||||
String[] selectionArgs = new String[]{String.valueOf(day.getIndex())};
|
||||
Cursor cursor = helper.getReadableDatabase().rawQuery(
|
||||
"SELECT t.id AS _id, t.name, t.type" + " FROM " + DatabaseHelper.TRACKS_TABLE_NAME + " t"
|
||||
+ " JOIN " + DatabaseHelper.EVENTS_TABLE_NAME + " e ON t.id = e.track_id"
|
||||
+ " WHERE e.day_index = ?"
|
||||
+ " GROUP BY t.id"
|
||||
+ " ORDER BY t.name ASC", selectionArgs);
|
||||
return new LocalBroadcastCursor(cursor, context, new IntentFilter(ACTION_SCHEDULE_REFRESHED));
|
||||
}
|
||||
|
||||
public static Track toTrack(Cursor cursor, Track track) {
|
||||
if (track == null) {
|
||||
track = new Track();
|
||||
}
|
||||
track.setName(cursor.getString(1));
|
||||
track.setType(Enum.valueOf(Track.Type.class, cursor.getString(2)));
|
||||
|
||||
return track;
|
||||
}
|
||||
|
||||
public static Track toTrack(Cursor cursor) {
|
||||
return toTrack(cursor, null);
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
public long getEventsCount() {
|
||||
return DatabaseUtils.queryNumEntries(helper.getReadableDatabase(), DatabaseHelper.EVENTS_TABLE_NAME, null, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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(
|
||||
"SELECT e.id AS _id, e.start_time, e.end_time, e.room_name, e.slug, et.title, et.subtitle, e.abstract, e.description, GROUP_CONCAT(p.name, ', '), e.day_index, d.date, t.name, t.type"
|
||||
+ " FROM " + DatabaseHelper.EVENTS_TABLE_NAME + " e"
|
||||
+ " JOIN " + DatabaseHelper.EVENTS_TITLES_TABLE_NAME + " et ON e.id = et.rowid"
|
||||
+ " JOIN " + DatabaseHelper.DAYS_TABLE_NAME + " d ON e.day_index = d._index"
|
||||
+ " JOIN " + DatabaseHelper.TRACKS_TABLE_NAME + " t ON e.track_id = t.id"
|
||||
+ " LEFT JOIN " + DatabaseHelper.EVENTS_PERSONS_TABLE_NAME + " ep ON e.id = ep.event_id"
|
||||
+ " LEFT JOIN " + DatabaseHelper.PERSONS_TABLE_NAME + " p ON ep.person_id = p.rowid"
|
||||
+ " WHERE e.id = ?"
|
||||
+ " GROUP BY e.id", selectionArgs);
|
||||
try {
|
||||
if (cursor.moveToFirst()) {
|
||||
return toEvent(cursor);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
} finally {
|
||||
cursor.close();
|
||||
}
|
||||
}
|
||||
|
||||
private Cursor toEventCursor(Cursor wrappedCursor) {
|
||||
IntentFilter intentFilter = new IntentFilter(ACTION_SCHEDULE_REFRESHED);
|
||||
intentFilter.addAction(ACTION_ADD_BOOKMARK);
|
||||
intentFilter.addAction(ACTION_REMOVE_BOOKMARKS);
|
||||
return new LocalBroadcastCursor(wrappedCursor, context, intentFilter);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the events for a specified track.
|
||||
*
|
||||
* @param day
|
||||
* @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(
|
||||
"SELECT e.id AS _id, e.start_time, e.end_time, e.room_name, e.slug, et.title, et.subtitle, e.abstract, e.description, GROUP_CONCAT(p.name, ', '), e.day_index, d.date, t.name, t.type, b.event_id"
|
||||
+ " FROM " + DatabaseHelper.EVENTS_TABLE_NAME + " e"
|
||||
+ " JOIN " + DatabaseHelper.EVENTS_TITLES_TABLE_NAME + " et ON e.id = et.rowid"
|
||||
+ " JOIN " + DatabaseHelper.DAYS_TABLE_NAME + " d ON e.day_index = d._index"
|
||||
+ " JOIN " + DatabaseHelper.TRACKS_TABLE_NAME + " t ON e.track_id = t.id"
|
||||
+ " LEFT JOIN " + DatabaseHelper.EVENTS_PERSONS_TABLE_NAME + " ep ON e.id = ep.event_id"
|
||||
+ " LEFT JOIN " + DatabaseHelper.PERSONS_TABLE_NAME + " p ON ep.person_id = p.rowid"
|
||||
+ " LEFT JOIN " + DatabaseHelper.BOOKMARKS_TABLE_NAME + " b ON e.id = b.event_id"
|
||||
+ " WHERE e.day_index = ? AND t.name = ? AND t.type = ?"
|
||||
+ " GROUP BY e.id"
|
||||
+ " ORDER BY e.start_time ASC", selectionArgs);
|
||||
return toEventCursor(cursor);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the events in the specified time window, ordered by start time. All parameters are optional but at least one must be provided.
|
||||
*
|
||||
* @param minStartTime Minimum start time, or -1
|
||||
* @param maxStartTime Maximum start time, or -1
|
||||
* @param minEndTime Minimum end time, or -1
|
||||
* @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();
|
||||
|
||||
if (minStartTime > 0L) {
|
||||
whereCondition.append("e.start_time > ?");
|
||||
selectionArgs.add(String.valueOf(minStartTime));
|
||||
}
|
||||
if (maxStartTime > 0L) {
|
||||
if (whereCondition.length() > 0) {
|
||||
whereCondition.append(" AND ");
|
||||
}
|
||||
whereCondition.append("e.start_time < ?");
|
||||
selectionArgs.add(String.valueOf(maxStartTime));
|
||||
}
|
||||
if (minEndTime > 0L) {
|
||||
if (whereCondition.length() > 0) {
|
||||
whereCondition.append(" AND ");
|
||||
}
|
||||
whereCondition.append("e.end_time > ?");
|
||||
selectionArgs.add(String.valueOf(minEndTime));
|
||||
}
|
||||
if (whereCondition.length() == 0) {
|
||||
throw new IllegalArgumentException("At least one filter must be provided");
|
||||
}
|
||||
String ascendingString = ascending ? "ASC" : "DESC";
|
||||
|
||||
Cursor cursor = helper.getReadableDatabase().rawQuery(
|
||||
"SELECT e.id AS _id, e.start_time, e.end_time, e.room_name, e.slug, et.title, et.subtitle, e.abstract, e.description, GROUP_CONCAT(p.name, ', '), e.day_index, d.date, t.name, t.type, b.event_id"
|
||||
+ " FROM " + DatabaseHelper.EVENTS_TABLE_NAME + " e"
|
||||
+ " JOIN " + DatabaseHelper.EVENTS_TITLES_TABLE_NAME + " et ON e.id = et.rowid"
|
||||
+ " JOIN " + DatabaseHelper.DAYS_TABLE_NAME + " d ON e.day_index = d._index"
|
||||
+ " JOIN " + DatabaseHelper.TRACKS_TABLE_NAME + " t ON e.track_id = t.id"
|
||||
+ " LEFT JOIN " + DatabaseHelper.EVENTS_PERSONS_TABLE_NAME + " ep ON e.id = ep.event_id"
|
||||
+ " LEFT JOIN " + DatabaseHelper.PERSONS_TABLE_NAME + " p ON ep.person_id = p.rowid"
|
||||
+ " LEFT JOIN " + DatabaseHelper.BOOKMARKS_TABLE_NAME + " b ON e.id = b.event_id"
|
||||
+ " WHERE " + whereCondition.toString()
|
||||
+ " GROUP BY e.id"
|
||||
+ " ORDER BY e.start_time " + ascendingString,
|
||||
selectionArgs.toArray(new String[selectionArgs.size()]));
|
||||
return toEventCursor(cursor);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the events presented by the specified person.
|
||||
*
|
||||
* @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(
|
||||
"SELECT e.id AS _id, e.start_time, e.end_time, e.room_name, e.slug, et.title, et.subtitle, e.abstract, e.description, GROUP_CONCAT(p.name, ', '), e.day_index, d.date, t.name, t.type, b.event_id"
|
||||
+ " FROM " + DatabaseHelper.EVENTS_TABLE_NAME + " e"
|
||||
+ " JOIN " + DatabaseHelper.EVENTS_TITLES_TABLE_NAME + " et ON e.id = et.rowid"
|
||||
+ " JOIN " + DatabaseHelper.DAYS_TABLE_NAME + " d ON e.day_index = d._index"
|
||||
+ " JOIN " + DatabaseHelper.TRACKS_TABLE_NAME + " t ON e.track_id = t.id"
|
||||
+ " LEFT JOIN " + DatabaseHelper.EVENTS_PERSONS_TABLE_NAME + " ep ON e.id = ep.event_id"
|
||||
+ " LEFT JOIN " + DatabaseHelper.PERSONS_TABLE_NAME + " p ON ep.person_id = p.rowid"
|
||||
+ " LEFT JOIN " + DatabaseHelper.BOOKMARKS_TABLE_NAME + " b ON e.id = b.event_id"
|
||||
+ " JOIN " + DatabaseHelper.EVENTS_PERSONS_TABLE_NAME + " ep2 ON e.id = ep2.event_id"
|
||||
+ " WHERE ep2.person_id = ?"
|
||||
+ " GROUP BY e.id"
|
||||
+ " ORDER BY e.start_time ASC", selectionArgs);
|
||||
return toEventCursor(cursor);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the bookmarks.
|
||||
*
|
||||
* @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;
|
||||
if (minStartTime > 0L) {
|
||||
whereCondition = " WHERE e.start_time > ?";
|
||||
selectionArgs = new String[]{String.valueOf(minStartTime)};
|
||||
} else {
|
||||
whereCondition = "";
|
||||
selectionArgs = null;
|
||||
}
|
||||
|
||||
Cursor cursor = helper.getReadableDatabase().rawQuery(
|
||||
"SELECT e.id AS _id, e.start_time, e.end_time, e.room_name, e.slug, et.title, et.subtitle, e.abstract, e.description, GROUP_CONCAT(p.name, ', '), e.day_index, d.date, t.name, t.type, 1"
|
||||
+ " FROM " + DatabaseHelper.BOOKMARKS_TABLE_NAME + " b"
|
||||
+ " JOIN " + DatabaseHelper.EVENTS_TABLE_NAME + " e ON b.event_id = e.id"
|
||||
+ " JOIN " + DatabaseHelper.EVENTS_TITLES_TABLE_NAME + " et ON e.id = et.rowid"
|
||||
+ " JOIN " + DatabaseHelper.DAYS_TABLE_NAME + " d ON e.day_index = d._index"
|
||||
+ " JOIN " + DatabaseHelper.TRACKS_TABLE_NAME + " t ON e.track_id = t.id"
|
||||
+ " LEFT JOIN " + DatabaseHelper.EVENTS_PERSONS_TABLE_NAME + " ep ON e.id = ep.event_id"
|
||||
+ " LEFT JOIN " + DatabaseHelper.PERSONS_TABLE_NAME + " p ON ep.person_id = p.rowid"
|
||||
+ whereCondition
|
||||
+ " GROUP BY e.id"
|
||||
+ " ORDER BY e.start_time ASC", selectionArgs);
|
||||
return toEventCursor(cursor);
|
||||
}
|
||||
|
||||
/**
|
||||
* Search through matching titles, subtitles, track names, person names. We need to use an union of 3 sub-queries because a "match" condition can not be
|
||||
* accompanied by other conditions in a "where" statement.
|
||||
*
|
||||
* @param query
|
||||
* @return A cursor to Events
|
||||
*/
|
||||
@WorkerThread
|
||||
public Cursor getSearchResults(String query) {
|
||||
final String matchQuery = query + "*";
|
||||
String[] selectionArgs = new String[]{matchQuery, "%" + query + "%", matchQuery};
|
||||
Cursor cursor = helper.getReadableDatabase().rawQuery(
|
||||
"SELECT e.id AS _id, e.start_time, e.end_time, e.room_name, e.slug, et.title, et.subtitle, e.abstract, e.description, GROUP_CONCAT(p.name, ', '), e.day_index, d.date, t.name, t.type, b.event_id"
|
||||
+ " FROM " + DatabaseHelper.EVENTS_TABLE_NAME + " e"
|
||||
+ " JOIN " + DatabaseHelper.EVENTS_TITLES_TABLE_NAME + " et ON e.id = et.rowid"
|
||||
+ " JOIN " + DatabaseHelper.DAYS_TABLE_NAME + " d ON e.day_index = d._index"
|
||||
+ " JOIN " + DatabaseHelper.TRACKS_TABLE_NAME + " t ON e.track_id = t.id"
|
||||
+ " LEFT JOIN " + DatabaseHelper.EVENTS_PERSONS_TABLE_NAME + " ep ON e.id = ep.event_id"
|
||||
+ " LEFT JOIN " + DatabaseHelper.PERSONS_TABLE_NAME + " p ON ep.person_id = p.rowid"
|
||||
+ " LEFT JOIN " + DatabaseHelper.BOOKMARKS_TABLE_NAME + " b ON e.id = b.event_id"
|
||||
+ " WHERE e.id IN ( "
|
||||
+ "SELECT rowid"
|
||||
+ " FROM " + DatabaseHelper.EVENTS_TITLES_TABLE_NAME
|
||||
+ " WHERE " + DatabaseHelper.EVENTS_TITLES_TABLE_NAME + " MATCH ?"
|
||||
+ " UNION "
|
||||
+ "SELECT e.id"
|
||||
+ " FROM " + DatabaseHelper.EVENTS_TABLE_NAME + " e"
|
||||
+ " JOIN " + DatabaseHelper.TRACKS_TABLE_NAME + " t ON e.track_id = t.id"
|
||||
+ " WHERE t.name LIKE ?"
|
||||
+ " UNION "
|
||||
+ "SELECT ep.event_id"
|
||||
+ " FROM " + DatabaseHelper.EVENTS_PERSONS_TABLE_NAME + " ep"
|
||||
+ " JOIN " + DatabaseHelper.PERSONS_TABLE_NAME + " p ON ep.person_id = p.rowid"
|
||||
+ " WHERE p.name MATCH ?"
|
||||
+ " )"
|
||||
+ " GROUP BY e.id"
|
||||
+ " ORDER BY e.start_time ASC", selectionArgs);
|
||||
return toEventCursor(cursor);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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)};
|
||||
// Query is similar to getSearchResults but returns different columns, does not join the Day table or the Bookmark table and limits the result set.
|
||||
return helper.getReadableDatabase().rawQuery(
|
||||
"SELECT e.id AS " + BaseColumns._ID
|
||||
+ ", et.title AS " + SearchManager.SUGGEST_COLUMN_TEXT_1
|
||||
+ ", IFNULL(GROUP_CONCAT(p.name, ', '), '') || ' - ' || t.name AS " + SearchManager.SUGGEST_COLUMN_TEXT_2
|
||||
+ ", e.id AS " + SearchManager.SUGGEST_COLUMN_INTENT_DATA
|
||||
+ " FROM " + DatabaseHelper.EVENTS_TABLE_NAME + " e"
|
||||
+ " JOIN " + DatabaseHelper.EVENTS_TITLES_TABLE_NAME + " et ON e.id = et.rowid"
|
||||
+ " JOIN " + DatabaseHelper.TRACKS_TABLE_NAME + " t ON e.track_id = t.id"
|
||||
+ " LEFT JOIN " + DatabaseHelper.EVENTS_PERSONS_TABLE_NAME + " ep ON e.id = ep.event_id"
|
||||
+ " LEFT JOIN " + DatabaseHelper.PERSONS_TABLE_NAME + " p ON ep.person_id = p.rowid"
|
||||
+ " WHERE e.id IN ( "
|
||||
+ "SELECT rowid"
|
||||
+ " FROM " + DatabaseHelper.EVENTS_TITLES_TABLE_NAME
|
||||
+ " WHERE " + DatabaseHelper.EVENTS_TITLES_TABLE_NAME + " MATCH ?"
|
||||
+ " UNION "
|
||||
+ "SELECT e.id"
|
||||
+ " FROM " + DatabaseHelper.EVENTS_TABLE_NAME + " e"
|
||||
+ " JOIN " + DatabaseHelper.TRACKS_TABLE_NAME + " t ON e.track_id = t.id"
|
||||
+ " WHERE t.name LIKE ?"
|
||||
+ " UNION "
|
||||
+ "SELECT ep.event_id"
|
||||
+ " FROM " + DatabaseHelper.EVENTS_PERSONS_TABLE_NAME + " ep"
|
||||
+ " JOIN " + DatabaseHelper.PERSONS_TABLE_NAME + " p ON ep.person_id = p.rowid"
|
||||
+ " WHERE p.name MATCH ?"
|
||||
+ " )"
|
||||
+ " GROUP BY e.id"
|
||||
+ " ORDER BY e.start_time ASC LIMIT ?", selectionArgs);
|
||||
}
|
||||
|
||||
public static Event toEvent(Cursor cursor, Event event) {
|
||||
Day day;
|
||||
Track track;
|
||||
Date startTime;
|
||||
Date endTime;
|
||||
if (event == null) {
|
||||
event = new Event();
|
||||
day = new Day();
|
||||
event.setDay(day);
|
||||
track = new Track();
|
||||
event.setTrack(track);
|
||||
|
||||
startTime = null;
|
||||
endTime = null;
|
||||
|
||||
day.setDate(new Date(cursor.getLong(11)));
|
||||
} else {
|
||||
day = event.getDay();
|
||||
track = event.getTrack();
|
||||
|
||||
startTime = event.getStartTime();
|
||||
endTime = event.getEndTime();
|
||||
|
||||
day.getDate().setTime(cursor.getLong(11));
|
||||
}
|
||||
event.setId(cursor.getLong(0));
|
||||
if (cursor.isNull(1)) {
|
||||
event.setStartTime(null);
|
||||
} else {
|
||||
if (startTime == null) {
|
||||
event.setStartTime(new Date(cursor.getLong(1)));
|
||||
} else {
|
||||
startTime.setTime(cursor.getLong(1));
|
||||
}
|
||||
}
|
||||
if (cursor.isNull(2)) {
|
||||
event.setEndTime(null);
|
||||
} else {
|
||||
if (endTime == null) {
|
||||
event.setEndTime(new Date(cursor.getLong(2)));
|
||||
} else {
|
||||
endTime.setTime(cursor.getLong(2));
|
||||
}
|
||||
}
|
||||
|
||||
event.setRoomName(cursor.getString(3));
|
||||
event.setSlug(cursor.getString(4));
|
||||
event.setTitle(cursor.getString(5));
|
||||
event.setSubTitle(cursor.getString(6));
|
||||
event.setAbstractText(cursor.getString(7));
|
||||
event.setDescription(cursor.getString(8));
|
||||
event.setPersonsSummary(cursor.getString(9));
|
||||
|
||||
day.setIndex(cursor.getInt(10));
|
||||
|
||||
track.setName(cursor.getString(12));
|
||||
track.setType(Enum.valueOf(Track.Type.class, cursor.getString(13)));
|
||||
|
||||
return event;
|
||||
}
|
||||
|
||||
public static Event toEvent(Cursor cursor) {
|
||||
return toEvent(cursor, null);
|
||||
}
|
||||
|
||||
public static long toEventId(Cursor cursor) {
|
||||
return cursor.getLong(0);
|
||||
}
|
||||
|
||||
public static long toEventStartTimeMillis(Cursor cursor) {
|
||||
return cursor.isNull(1) ? -1L : cursor.getLong(1);
|
||||
}
|
||||
|
||||
public static long toEventEndTimeMillis(Cursor cursor) {
|
||||
return cursor.isNull(2) ? -1L : cursor.getLong(2);
|
||||
}
|
||||
|
||||
public static boolean toBookmarkStatus(Cursor cursor) {
|
||||
return !cursor.isNull(14);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns all persons in alphabetical order.
|
||||
*/
|
||||
@WorkerThread
|
||||
public Cursor getPersons() {
|
||||
Cursor cursor = helper.getReadableDatabase().rawQuery(
|
||||
"SELECT rowid AS _id, name"
|
||||
+ " FROM " + DatabaseHelper.PERSONS_TABLE_NAME
|
||||
+ " ORDER BY name COLLATE NOCASE", null);
|
||||
return new LocalBroadcastCursor(cursor, context, new IntentFilter(ACTION_SCHEDULE_REFRESHED));
|
||||
}
|
||||
|
||||
public static final int PERSON_NAME_COLUMN_INDEX = 1;
|
||||
|
||||
/**
|
||||
* 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(
|
||||
"SELECT p.rowid AS _id, p.name"
|
||||
+ " FROM " + DatabaseHelper.PERSONS_TABLE_NAME + " p"
|
||||
+ " JOIN " + DatabaseHelper.EVENTS_PERSONS_TABLE_NAME + " ep ON p.rowid = ep.person_id"
|
||||
+ " WHERE ep.event_id = ?", selectionArgs);
|
||||
try {
|
||||
List<Person> result = new ArrayList<>(cursor.getCount());
|
||||
while (cursor.moveToNext()) {
|
||||
result.add(toPerson(cursor));
|
||||
}
|
||||
return result;
|
||||
} finally {
|
||||
cursor.close();
|
||||
}
|
||||
}
|
||||
|
||||
public static Person toPerson(Cursor cursor, Person person) {
|
||||
if (person == null) {
|
||||
person = new Person();
|
||||
}
|
||||
person.setId(cursor.getLong(0));
|
||||
person.setName(cursor.getString(1));
|
||||
|
||||
return person;
|
||||
}
|
||||
|
||||
public static Person toPerson(Cursor cursor) {
|
||||
return toPerson(cursor, null);
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
public List<Link> getLinks(Event event) {
|
||||
String[] selectionArgs = new String[]{String.valueOf(event.getId())};
|
||||
Cursor cursor = helper.getReadableDatabase().rawQuery(
|
||||
"SELECT url, description"
|
||||
+ " FROM " + DatabaseHelper.LINKS_TABLE_NAME
|
||||
+ " WHERE event_id = ?"
|
||||
+ " ORDER BY rowid ASC", selectionArgs);
|
||||
try {
|
||||
List<Link> result = new ArrayList<>(cursor.getCount());
|
||||
while (cursor.moveToNext()) {
|
||||
Link link = new Link();
|
||||
link.setUrl(cursor.getString(0));
|
||||
link.setDescription(cursor.getString(1));
|
||||
result.add(link);
|
||||
}
|
||||
return result;
|
||||
} finally {
|
||||
cursor.close();
|
||||
}
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
public boolean isBookmarked(Event event) {
|
||||
String[] selectionArgs = new String[]{String.valueOf(event.getId())};
|
||||
return DatabaseUtils.queryNumEntries(helper.getReadableDatabase(), DatabaseHelper.BOOKMARKS_TABLE_NAME, "event_id = ?", selectionArgs) > 0L;
|
||||
}
|
||||
|
||||
@WorkerThread
|
||||
public boolean addBookmark(Event event) {
|
||||
boolean complete = false;
|
||||
|
||||
SQLiteDatabase db = helper.getWritableDatabase();
|
||||
db.beginTransaction();
|
||||
try {
|
||||
ContentValues values = new ContentValues();
|
||||
values.put("event_id", event.getId());
|
||||
long result = db.insert(DatabaseHelper.BOOKMARKS_TABLE_NAME, null, values);
|
||||
|
||||
// If the bookmark is already present
|
||||
if (result == -1L) {
|
||||
return false;
|
||||
}
|
||||
|
||||
db.setTransactionSuccessful();
|
||||
complete = true;
|
||||
return true;
|
||||
} finally {
|
||||
db.endTransaction();
|
||||
|
||||
if (complete) {
|
||||
Intent intent = new Intent(ACTION_ADD_BOOKMARK).putExtra(EXTRA_EVENT_ID, event.getId());
|
||||
Date startTime = event.getStartTime();
|
||||
if (startTime != null) {
|
||||
intent.putExtra(EXTRA_EVENT_START_TIME, startTime.getTime());
|
||||
}
|
||||
LocalBroadcastManager.getInstance(context).sendBroadcast(intent);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@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) {
|
||||
throw new IllegalArgumentException("At least one bookmark id to remove must be passed");
|
||||
}
|
||||
String[] stringEventIds = new String[length];
|
||||
for (int i = 0; i < length; ++i) {
|
||||
stringEventIds[i] = String.valueOf(eventIds[i]);
|
||||
}
|
||||
|
||||
boolean isComplete = false;
|
||||
|
||||
SQLiteDatabase db = helper.getWritableDatabase();
|
||||
db.beginTransaction();
|
||||
try {
|
||||
String whereClause = "event_id IN (" + TextUtils.join(",", stringEventIds) + ")";
|
||||
int count = db.delete(DatabaseHelper.BOOKMARKS_TABLE_NAME, whereClause, null);
|
||||
|
||||
if (count == 0) {
|
||||
return false;
|
||||
}
|
||||
|
||||
db.setTransactionSuccessful();
|
||||
isComplete = true;
|
||||
return true;
|
||||
} finally {
|
||||
db.endTransaction();
|
||||
|
||||
if (isComplete) {
|
||||
Intent intent = new Intent(ACTION_REMOVE_BOOKMARKS).putExtra(EXTRA_EVENT_IDS, eventIds);
|
||||
LocalBroadcastManager.getInstance(context).sendBroadcast(intent);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,86 +0,0 @@
|
|||
package be.digitalia.fosdem.db;
|
||||
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.ContentResolver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.database.ContentObservable;
|
||||
import android.database.ContentObserver;
|
||||
import android.database.Cursor;
|
||||
import android.database.CursorWrapper;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
|
||||
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
|
||||
|
||||
/**
|
||||
* A cursor notifying its observers when a local broadcast matches the provided IntentFilter.
|
||||
* This is more efficient and more customizable than using the ContentResolver to notify changes.
|
||||
*
|
||||
* @author Christophe Beyls
|
||||
*/
|
||||
public class LocalBroadcastCursor extends CursorWrapper {
|
||||
|
||||
final ContentObservable contentObservable = new ContentObservable();
|
||||
|
||||
private final LocalBroadcastManager localBroadcastManager;
|
||||
private final BroadcastReceiver receiver = new BroadcastReceiver() {
|
||||
@Override
|
||||
@SuppressWarnings("deprecation")
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
if (matchIntent(context, intent)) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
|
||||
contentObservable.dispatchChange(false, null);
|
||||
} else {
|
||||
contentObservable.dispatchChange(false);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
public LocalBroadcastCursor(Cursor wrappedCursor, Context context, IntentFilter intentFilter) {
|
||||
super(wrappedCursor);
|
||||
localBroadcastManager = LocalBroadcastManager.getInstance(context);
|
||||
localBroadcastManager.registerReceiver(receiver, intentFilter);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void registerContentObserver(ContentObserver observer) {
|
||||
contentObservable.registerObserver(observer);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void unregisterContentObserver(ContentObserver observer) {
|
||||
// cursor will unregister all observers when it closes
|
||||
if (!isClosed()) {
|
||||
contentObservable.unregisterObserver(observer);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setNotificationUri(ContentResolver cr, Uri uri) {
|
||||
throw new UnsupportedOperationException();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Uri getNotificationUri() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() {
|
||||
super.close();
|
||||
contentObservable.unregisterAll();
|
||||
localBroadcastManager.unregisterReceiver(receiver);
|
||||
}
|
||||
|
||||
/**
|
||||
* Override this method to implement custom Intent matching in addition to the IntentFilter.
|
||||
*
|
||||
* @return True if the Intent matches and observers should be notified. Default is true.
|
||||
*/
|
||||
protected boolean matchIntent(Context context, Intent intent) {
|
||||
return true;
|
||||
}
|
||||
}
|
441
app/src/main/java/be/digitalia/fosdem/db/ScheduleDao.java
Normal file
441
app/src/main/java/be/digitalia/fosdem/db/ScheduleDao.java
Normal file
|
@ -0,0 +1,441 @@
|
|||
package be.digitalia.fosdem.db;
|
||||
|
||||
import android.app.SearchManager;
|
||||
import android.database.Cursor;
|
||||
import android.os.AsyncTask;
|
||||
import android.provider.BaseColumns;
|
||||
import androidx.annotation.MainThread;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.WorkerThread;
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.MutableLiveData;
|
||||
import androidx.paging.DataSource;
|
||||
import androidx.room.*;
|
||||
import be.digitalia.fosdem.alarms.FosdemAlarmManager;
|
||||
import be.digitalia.fosdem.db.entities.EventEntity;
|
||||
import be.digitalia.fosdem.db.entities.EventTitles;
|
||||
import be.digitalia.fosdem.db.entities.EventToPerson;
|
||||
import be.digitalia.fosdem.model.*;
|
||||
import be.digitalia.fosdem.utils.DateUtils;
|
||||
|
||||
import java.util.*;
|
||||
|
||||
@Dao
|
||||
public abstract class ScheduleDao {
|
||||
|
||||
private static final String LAST_UPDATE_TIME_PREF = "last_update_time";
|
||||
private static final String LAST_MODIFIED_TAG_PREF = "last_modified_tag";
|
||||
|
||||
private final AppDatabase appDatabase;
|
||||
private final MutableLiveData<Long> lastUpdateTime = new MutableLiveData<>();
|
||||
|
||||
public ScheduleDao(AppDatabase appDatabase) {
|
||||
this.appDatabase = appDatabase;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The last update time in milliseconds since EPOCH, or -1 if not available.
|
||||
* This LiveData is pre-initialized with the up-to-date value.
|
||||
*/
|
||||
@MainThread
|
||||
public LiveData<Long> getLastUpdateTime() {
|
||||
if (lastUpdateTime.getValue() == null) {
|
||||
lastUpdateTime.setValue(appDatabase.getSharedPreferences().getLong(LAST_UPDATE_TIME_PREF, -1L));
|
||||
}
|
||||
return lastUpdateTime;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return The time identifier of the current version of the database.
|
||||
*/
|
||||
public String getLastModifiedTag() {
|
||||
return appDatabase.getSharedPreferences().getString(LAST_MODIFIED_TAG_PREF, null);
|
||||
}
|
||||
|
||||
private static class EmptyScheduleException extends RuntimeException {
|
||||
}
|
||||
|
||||
/**
|
||||
* Stores the schedule in the database.
|
||||
*
|
||||
* @param events The events stream.
|
||||
* @return The number of events processed.
|
||||
*/
|
||||
@WorkerThread
|
||||
public int storeSchedule(Iterable<DetailedEvent> events, String lastModifiedTag) {
|
||||
int totalEvents;
|
||||
try {
|
||||
totalEvents = storeScheduleInternal(events, lastModifiedTag);
|
||||
} catch (EmptyScheduleException ese) {
|
||||
totalEvents = 0;
|
||||
}
|
||||
if (totalEvents > 0) {
|
||||
// Set last update time and server's last modified tag
|
||||
final long now = System.currentTimeMillis();
|
||||
appDatabase.getSharedPreferences().edit()
|
||||
.putLong(LAST_UPDATE_TIME_PREF, now)
|
||||
.putString(LAST_MODIFIED_TAG_PREF, lastModifiedTag)
|
||||
.apply();
|
||||
lastUpdateTime.postValue(now);
|
||||
|
||||
FosdemAlarmManager.getInstance().onScheduleRefreshed();
|
||||
}
|
||||
return totalEvents;
|
||||
}
|
||||
|
||||
@Transaction
|
||||
protected int storeScheduleInternal(Iterable<DetailedEvent> events, String lastModifiedTag) {
|
||||
// 1: Delete the previous schedule
|
||||
clearSchedule();
|
||||
|
||||
// 2: Insert the events
|
||||
int totalEvents = 0;
|
||||
final Map<Track, Long> tracks = new HashMap<>();
|
||||
long nextTrackId = 0L;
|
||||
long minEventId = Long.MAX_VALUE;
|
||||
final Set<Day> days = new HashSet<>(2);
|
||||
|
||||
for (DetailedEvent event : events) {
|
||||
// Retrieve or insert Track
|
||||
final Track track = event.getTrack();
|
||||
Long trackId = tracks.get(track);
|
||||
if (trackId == null) {
|
||||
// New track
|
||||
nextTrackId++;
|
||||
trackId = nextTrackId;
|
||||
track.setId(nextTrackId);
|
||||
insertTrack(track);
|
||||
tracks.put(track, trackId);
|
||||
} else {
|
||||
track.setId(trackId);
|
||||
}
|
||||
|
||||
final long eventId = event.getId();
|
||||
try {
|
||||
// Insert main event and fulltext fields
|
||||
insertEvent(new EventEntity(event), new EventTitles(event));
|
||||
} catch (Exception e) {
|
||||
// Duplicate event: skip
|
||||
continue;
|
||||
}
|
||||
|
||||
days.add(event.getDay());
|
||||
if (eventId < minEventId) {
|
||||
minEventId = eventId;
|
||||
}
|
||||
|
||||
final List<Person> persons = event.getPersons();
|
||||
insertPersons(persons);
|
||||
final int personsCount = persons.size();
|
||||
final EventToPerson[] eventsToPersons = new EventToPerson[personsCount];
|
||||
for (int i = 0; i < personsCount; ++i) {
|
||||
eventsToPersons[i] = new EventToPerson(eventId, persons.get(i).getId());
|
||||
}
|
||||
insertEventsToPersons(eventsToPersons);
|
||||
|
||||
insertLinks(event.getLinks());
|
||||
|
||||
totalEvents++;
|
||||
}
|
||||
|
||||
if (totalEvents == 0) {
|
||||
// Rollback the transaction
|
||||
throw new EmptyScheduleException();
|
||||
}
|
||||
|
||||
// 3: Insert collected days
|
||||
insertDays(days);
|
||||
|
||||
// 4: Purge outdated bookmarks
|
||||
purgeOutdatedBookmarks(minEventId);
|
||||
|
||||
return totalEvents;
|
||||
}
|
||||
|
||||
@Insert
|
||||
protected abstract void insertTrack(Track track);
|
||||
|
||||
@Insert
|
||||
protected abstract void insertEvent(EventEntity eventEntity, EventTitles eventTitles);
|
||||
|
||||
@Insert(onConflict = OnConflictStrategy.IGNORE)
|
||||
protected abstract void insertPersons(List<Person> persons);
|
||||
|
||||
@Insert
|
||||
protected abstract void insertEventsToPersons(EventToPerson[] eventsToPersons);
|
||||
|
||||
@Insert
|
||||
protected abstract void insertLinks(List<Link> links);
|
||||
|
||||
@Insert
|
||||
protected abstract void insertDays(Set<Day> days);
|
||||
|
||||
@Query("DELETE FROM bookmarks WHERE event_id < :minEventId")
|
||||
protected abstract void purgeOutdatedBookmarks(long minEventId);
|
||||
|
||||
@WorkerThread
|
||||
@Transaction
|
||||
public void clearSchedule() {
|
||||
clearEvents();
|
||||
clearEventTitles();
|
||||
clearPersons();
|
||||
clearEventToPersons();
|
||||
clearLinks();
|
||||
clearTracks();
|
||||
clearDays();
|
||||
}
|
||||
|
||||
@Query("DELETE FROM events")
|
||||
protected abstract void clearEvents();
|
||||
|
||||
@Query("DELETE FROM events_titles")
|
||||
protected abstract void clearEventTitles();
|
||||
|
||||
@Query("DELETE FROM persons")
|
||||
protected abstract void clearPersons();
|
||||
|
||||
@Query("DELETE FROM events_persons")
|
||||
protected abstract void clearEventToPersons();
|
||||
|
||||
@Query("DELETE FROM links")
|
||||
protected abstract void clearLinks();
|
||||
|
||||
@Query("DELETE FROM tracks")
|
||||
protected abstract void clearTracks();
|
||||
|
||||
@Query("DELETE FROM days")
|
||||
protected abstract void clearDays();
|
||||
|
||||
|
||||
// Cache days
|
||||
private volatile LiveData<List<Day>> daysLiveData;
|
||||
|
||||
public LiveData<List<Day>> getDays() {
|
||||
if (daysLiveData != null) {
|
||||
return daysLiveData;
|
||||
}
|
||||
synchronized (this) {
|
||||
daysLiveData = getDaysInternal();
|
||||
return daysLiveData;
|
||||
}
|
||||
}
|
||||
|
||||
@Query("SELECT `index`, date FROM days ORDER BY `index` ASC")
|
||||
protected abstract LiveData<List<Day>> getDaysInternal();
|
||||
|
||||
@WorkerThread
|
||||
public int getYear() {
|
||||
long date = 0L;
|
||||
|
||||
// Compute from cached days if available
|
||||
final LiveData<List<Day>> cache = daysLiveData;
|
||||
List<Day> days = (cache == null) ? null : cache.getValue();
|
||||
if (days != null) {
|
||||
if (days.size() > 0) {
|
||||
date = days.get(0).getDate().getTime();
|
||||
}
|
||||
} else {
|
||||
date = getConferenceStartDate();
|
||||
}
|
||||
|
||||
// Use the current year by default
|
||||
if (date == 0L) {
|
||||
date = System.currentTimeMillis();
|
||||
}
|
||||
|
||||
return DateUtils.getYear(date);
|
||||
}
|
||||
|
||||
@Query("SELECT date FROM days ORDER BY `index` ASC LIMIT 1")
|
||||
protected abstract long getConferenceStartDate();
|
||||
|
||||
@Query("SELECT t.id, t.name, t.type FROM tracks t"
|
||||
+ " JOIN events e ON t.id = e.track_id"
|
||||
+ " WHERE e.day_index = :day"
|
||||
+ " GROUP BY t.id"
|
||||
+ " ORDER BY t.name ASC")
|
||||
public abstract LiveData<List<Track>> getTracks(Day day);
|
||||
|
||||
/**
|
||||
* Returns the event with the specified id, or null if not found.
|
||||
*/
|
||||
@Query("SELECT e.id, e.start_time, e.end_time, e.room_name, e.slug, et.title, et.subtitle, e.abstract, e.description"
|
||||
+ ", GROUP_CONCAT(p.name, ', ') AS persons, e.day_index, d.date AS day_date, e.track_id, t.name AS track_name, t.type AS track_type"
|
||||
+ " FROM events e"
|
||||
+ " JOIN events_titles et ON e.id = et.`rowid`"
|
||||
+ " JOIN days d ON e.day_index = d.`index`"
|
||||
+ " JOIN tracks t ON e.track_id = t.id"
|
||||
+ " LEFT JOIN events_persons ep ON e.id = ep.event_id"
|
||||
+ " LEFT JOIN persons p ON ep.person_id = p.`rowid`"
|
||||
+ " WHERE e.id = :id"
|
||||
+ " GROUP BY e.id")
|
||||
@Nullable
|
||||
@WorkerThread
|
||||
public abstract Event getEvent(long id);
|
||||
|
||||
/**
|
||||
* Returns the events for a specified track.
|
||||
*/
|
||||
@Query("SELECT e.id, e.start_time, e.end_time, e.room_name, e.slug, et.title, et.subtitle, e.abstract, e.description"
|
||||
+ ", GROUP_CONCAT(p.name, ', ') AS persons, e.day_index, d.date AS day_date, e.track_id, t.name AS track_name, t.type AS track_type"
|
||||
+ ", b.event_id IS NOT NULL AS is_bookmarked"
|
||||
+ " FROM events e"
|
||||
+ " JOIN events_titles et ON e.id = et.`rowid`"
|
||||
+ " JOIN days d ON e.day_index = d.`index`"
|
||||
+ " JOIN tracks t ON e.track_id = t.id"
|
||||
+ " LEFT JOIN events_persons ep ON e.id = ep.event_id"
|
||||
+ " LEFT JOIN persons p ON ep.person_id = p.`rowid`"
|
||||
+ " LEFT JOIN bookmarks b ON e.id = b.event_id"
|
||||
+ " WHERE e.day_index = :day AND e.track_id = :track"
|
||||
+ " GROUP BY e.id"
|
||||
+ " ORDER BY e.start_time ASC")
|
||||
public abstract LiveData<List<StatusEvent>> getEvents(Day day, Track track);
|
||||
|
||||
/**
|
||||
* Returns events starting in the specified interval, ordered by ascending start time.
|
||||
*/
|
||||
@Query("SELECT e.id, e.start_time, e.end_time, e.room_name, e.slug, et.title, et.subtitle, e.abstract, e.description"
|
||||
+ ", GROUP_CONCAT(p.name, ', ') AS persons, e.day_index, d.date AS day_date, e.track_id, t.name AS track_name, t.type AS track_type"
|
||||
+ ", b.event_id IS NOT NULL AS is_bookmarked"
|
||||
+ " FROM events e"
|
||||
+ " JOIN events_titles et ON e.id = et.`rowid`"
|
||||
+ " JOIN days d ON e.day_index = d.`index`"
|
||||
+ " JOIN tracks t ON e.track_id = t.id"
|
||||
+ " LEFT JOIN events_persons ep ON e.id = ep.event_id"
|
||||
+ " LEFT JOIN persons p ON ep.person_id = p.`rowid`"
|
||||
+ " LEFT JOIN bookmarks b ON e.id = b.event_id"
|
||||
+ " WHERE e.start_time BETWEEN :minStartTime AND :maxStartTime"
|
||||
+ " GROUP BY e.id"
|
||||
+ " ORDER BY e.start_time ASC")
|
||||
public abstract DataSource.Factory<Integer, StatusEvent> getEventsWithStartTime(long minStartTime, long maxStartTime);
|
||||
|
||||
/**
|
||||
* Returns events in progress at the specified time, ordered by descending start time.
|
||||
*/
|
||||
@Query("SELECT e.id, e.start_time, e.end_time, e.room_name, e.slug, et.title, et.subtitle, e.abstract, e.description"
|
||||
+ ", GROUP_CONCAT(p.name, ', ') AS persons, e.day_index, d.date AS day_date, e.track_id, t.name AS track_name, t.type AS track_type"
|
||||
+ ", b.event_id IS NOT NULL AS is_bookmarked"
|
||||
+ " FROM events e"
|
||||
+ " JOIN events_titles et ON e.id = et.`rowid`"
|
||||
+ " JOIN days d ON e.day_index = d.`index`"
|
||||
+ " JOIN tracks t ON e.track_id = t.id"
|
||||
+ " LEFT JOIN events_persons ep ON e.id = ep.event_id"
|
||||
+ " LEFT JOIN persons p ON ep.person_id = p.`rowid`"
|
||||
+ " LEFT JOIN bookmarks b ON e.id = b.event_id"
|
||||
+ " WHERE e.start_time <= :time AND :time < e.end_time"
|
||||
+ " GROUP BY e.id"
|
||||
+ " ORDER BY e.start_time DESC")
|
||||
public abstract DataSource.Factory<Integer, StatusEvent> getEventsInProgress(long time);
|
||||
|
||||
/**
|
||||
* Returns the events presented by the specified person.
|
||||
*/
|
||||
@Query("SELECT e.id , e.start_time, e.end_time, e.room_name, e.slug, et.title, et.subtitle, e.abstract, e.description"
|
||||
+ ", GROUP_CONCAT(p.name, ', ') AS persons, e.day_index, d.date AS day_date, e.track_id, t.name AS track_name, t.type AS track_type"
|
||||
+ ", b.event_id IS NOT NULL AS is_bookmarked"
|
||||
+ " FROM events e"
|
||||
+ " JOIN events_titles et ON e.id = et.`rowid`"
|
||||
+ " JOIN days d ON e.day_index = d.`index`"
|
||||
+ " JOIN tracks t ON e.track_id = t.id"
|
||||
+ " LEFT JOIN events_persons ep ON e.id = ep.event_id"
|
||||
+ " LEFT JOIN persons p ON ep.person_id = p.`rowid`"
|
||||
+ " LEFT JOIN bookmarks b ON e.id = b.event_id"
|
||||
+ " JOIN events_persons ep2 ON e.id = ep2.event_id"
|
||||
+ " WHERE ep2.person_id = :person"
|
||||
+ " GROUP BY e.id"
|
||||
+ " ORDER BY e.start_time ASC")
|
||||
public abstract DataSource.Factory<Integer, StatusEvent> getEvents(Person person);
|
||||
|
||||
/**
|
||||
* Search through matching titles, subtitles, track names, person names. We need to use an union of 3 sub-queries because a "match" condition can not be
|
||||
* accompanied by other conditions in a "where" statement.
|
||||
*/
|
||||
@Query("SELECT e.id, e.start_time, e.end_time, e.room_name, e.slug, et.title, et.subtitle, e.abstract, e.description"
|
||||
+ ", GROUP_CONCAT(p.name, ', ') AS persons, e.day_index, d.date AS day_date, e.track_id, t.name AS track_name, t.type AS track_type"
|
||||
+ ", b.event_id IS NOT NULL AS is_bookmarked"
|
||||
+ " FROM events e"
|
||||
+ " JOIN events_titles et ON e.id = et.`rowid`"
|
||||
+ " JOIN days d ON e.day_index = d.`index`"
|
||||
+ " JOIN tracks t ON e.track_id = t.id"
|
||||
+ " LEFT JOIN events_persons ep ON e.id = ep.event_id"
|
||||
+ " LEFT JOIN persons p ON ep.person_id = p.`rowid`"
|
||||
+ " LEFT JOIN bookmarks b ON e.id = b.event_id"
|
||||
+ " WHERE e.id IN ( "
|
||||
+ "SELECT `rowid`"
|
||||
+ " FROM events_titles"
|
||||
+ " WHERE events_titles MATCH :query || '*'"
|
||||
+ " UNION "
|
||||
+ "SELECT e.id"
|
||||
+ " FROM events e"
|
||||
+ " JOIN tracks t ON e.track_id = t.id"
|
||||
+ " WHERE t.name LIKE '%' || :query || '%'"
|
||||
+ " UNION "
|
||||
+ "SELECT ep.event_id"
|
||||
+ " FROM events_persons ep"
|
||||
+ " JOIN persons p ON ep.person_id = p.`rowid`"
|
||||
+ " WHERE p.name MATCH :query || '*'"
|
||||
+ " )"
|
||||
+ " GROUP BY e.id"
|
||||
+ " ORDER BY e.start_time ASC")
|
||||
public abstract DataSource.Factory<Integer, StatusEvent> getSearchResults(String query);
|
||||
|
||||
/**
|
||||
* Method called by SearchSuggestionProvider to return search results in the format expected by the search framework.
|
||||
*/
|
||||
@Query("SELECT e.id AS " + BaseColumns._ID
|
||||
+ ", et.title AS " + SearchManager.SUGGEST_COLUMN_TEXT_1
|
||||
+ ", IFNULL(GROUP_CONCAT(p.name, ', '), '') || ' - ' || t.name AS " + SearchManager.SUGGEST_COLUMN_TEXT_2
|
||||
+ ", e.id AS " + SearchManager.SUGGEST_COLUMN_INTENT_DATA
|
||||
+ " FROM events e"
|
||||
+ " JOIN events_titles et ON e.id = et.`rowid`"
|
||||
+ " JOIN tracks t ON e.track_id = t.id"
|
||||
+ " LEFT JOIN events_persons ep ON e.id = ep.event_id"
|
||||
+ " LEFT JOIN persons p ON ep.person_id = p.`rowid`"
|
||||
+ " WHERE e.id IN ( "
|
||||
+ "SELECT `rowid`"
|
||||
+ " FROM events_titles"
|
||||
+ " WHERE events_titles MATCH :query || '*'"
|
||||
+ " UNION "
|
||||
+ "SELECT e.id"
|
||||
+ " FROM events e"
|
||||
+ " JOIN tracks t ON e.track_id = t.id"
|
||||
+ " WHERE t.name LIKE '%' || :query || '%'"
|
||||
+ " UNION "
|
||||
+ "SELECT ep.event_id"
|
||||
+ " FROM events_persons ep"
|
||||
+ " JOIN persons p ON ep.person_id = p.`rowid`"
|
||||
+ " WHERE p.name MATCH :query || '*'"
|
||||
+ " )"
|
||||
+ " GROUP BY e.id"
|
||||
+ " ORDER BY e.start_time ASC LIMIT :limit")
|
||||
@WorkerThread
|
||||
public abstract Cursor getSearchSuggestionResults(String query, int limit);
|
||||
|
||||
/**
|
||||
* Returns all persons in alphabetical order.
|
||||
*/
|
||||
@Query("SELECT `rowid`, name"
|
||||
+ " FROM persons"
|
||||
+ " ORDER BY name COLLATE NOCASE")
|
||||
public abstract DataSource.Factory<Integer, Person> getPersons();
|
||||
|
||||
public LiveData<EventDetails> getEventDetails(final Event event) {
|
||||
final MutableLiveData<EventDetails> result = new MutableLiveData<>();
|
||||
AsyncTask.THREAD_POOL_EXECUTOR.execute(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
result.postValue(new EventDetails(getPersons(event), getLinks(event)));
|
||||
}
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
@Query("SELECT p.`rowid`, p.name"
|
||||
+ " FROM persons p"
|
||||
+ " JOIN events_persons ep ON p.`rowid` = ep.person_id"
|
||||
+ " WHERE ep.event_id = :event")
|
||||
protected abstract List<Person> getPersons(Event event);
|
||||
|
||||
@Query("SELECT * FROM links WHERE event_id = :event ORDER BY id ASC")
|
||||
protected abstract List<Link> getLinks(Event event);
|
||||
}
|
|
@ -0,0 +1,39 @@
|
|||
package be.digitalia.fosdem.db.converters;
|
||||
|
||||
import androidx.room.TypeConverter;
|
||||
import be.digitalia.fosdem.model.Day;
|
||||
import be.digitalia.fosdem.model.Event;
|
||||
import be.digitalia.fosdem.model.Person;
|
||||
import be.digitalia.fosdem.model.Track;
|
||||
|
||||
public class GlobalTypeConverters {
|
||||
@TypeConverter
|
||||
public static Track.Type toTrackType(String value) {
|
||||
return Track.Type.valueOf(value);
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
public static String fromTrackType(Track.Type value) {
|
||||
return value.name();
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
public static long fromDay(Day day) {
|
||||
return day.getIndex();
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
public static long fromTrack(Track track) {
|
||||
return track.getId();
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
public static long fromPerson(Person person) {
|
||||
return person.getId();
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
public static long fromEvent(Event event) {
|
||||
return event.getId();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,18 @@
|
|||
package be.digitalia.fosdem.db.converters;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.room.TypeConverter;
|
||||
|
||||
public class NonNullDateTypeConverters {
|
||||
@TypeConverter
|
||||
public static Date toDate(long value) {
|
||||
return new Date(value);
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
public static long fromDate(@NonNull Date value) {
|
||||
return value.getTime();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,17 @@
|
|||
package be.digitalia.fosdem.db.converters;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
import androidx.room.TypeConverter;
|
||||
|
||||
public class NullableDateTypeConverters {
|
||||
@TypeConverter
|
||||
public static Date toDate(Long value) {
|
||||
return value == null ? null : new Date(value);
|
||||
}
|
||||
|
||||
@TypeConverter
|
||||
public static Long fromDate(Date value) {
|
||||
return (value == null) ? null : value.getTime();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,23 @@
|
|||
package be.digitalia.fosdem.db.entities;
|
||||
|
||||
import androidx.room.ColumnInfo;
|
||||
import androidx.room.Entity;
|
||||
import androidx.room.PrimaryKey;
|
||||
|
||||
@Entity(tableName = Bookmark.TABLE_NAME)
|
||||
public class Bookmark {
|
||||
|
||||
public static final String TABLE_NAME = "bookmarks";
|
||||
|
||||
@PrimaryKey
|
||||
@ColumnInfo(name = "event_id")
|
||||
private final long eventId;
|
||||
|
||||
public Bookmark(long eventId) {
|
||||
this.eventId = eventId;
|
||||
}
|
||||
|
||||
public long getEventId() {
|
||||
return eventId;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,95 @@
|
|||
package be.digitalia.fosdem.db.entities;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
import androidx.room.ColumnInfo;
|
||||
import androidx.room.Entity;
|
||||
import androidx.room.Index;
|
||||
import androidx.room.PrimaryKey;
|
||||
import androidx.room.TypeConverters;
|
||||
import be.digitalia.fosdem.db.converters.NullableDateTypeConverters;
|
||||
import be.digitalia.fosdem.model.Event;
|
||||
|
||||
@Entity(tableName = EventEntity.TABLE_NAME, indices = {
|
||||
@Index(value = {"day_index"}, name = "event_day_index_idx"),
|
||||
@Index(value = {"start_time"}, name = "event_start_time_idx"),
|
||||
@Index(value = {"end_time"}, name = "event_end_time_idx"),
|
||||
@Index(value = {"track_id"}, name = "event_track_id_idx")
|
||||
})
|
||||
public class EventEntity {
|
||||
|
||||
public static final String TABLE_NAME = "events";
|
||||
|
||||
@PrimaryKey
|
||||
private final long id;
|
||||
@ColumnInfo(name = "day_index")
|
||||
private final int dayIndex;
|
||||
@ColumnInfo(name = "start_time")
|
||||
@TypeConverters({NullableDateTypeConverters.class})
|
||||
private final Date startTime;
|
||||
@ColumnInfo(name = "end_time")
|
||||
@TypeConverters({NullableDateTypeConverters.class})
|
||||
private final Date endTime;
|
||||
@ColumnInfo(name = "room_name")
|
||||
private final String roomName;
|
||||
private final String slug;
|
||||
@ColumnInfo(name = "track_id")
|
||||
private final long trackId;
|
||||
@ColumnInfo(name = "abstract")
|
||||
private final String abstractText;
|
||||
private final String description;
|
||||
|
||||
public EventEntity(Event event) {
|
||||
this(event.getId(), event.getDay().getIndex(), event.getStartTime(), event.getEndTime(), event.getRoomName(),
|
||||
event.getSlug(), event.getTrack().getId(), event.getAbstractText(), event.getDescription());
|
||||
}
|
||||
|
||||
public EventEntity(long id, int dayIndex, Date startTime, Date endTime, String roomName,
|
||||
String slug, long trackId, String abstractText, String description) {
|
||||
this.id = id;
|
||||
this.dayIndex = dayIndex;
|
||||
this.startTime = startTime;
|
||||
this.endTime = endTime;
|
||||
this.roomName = roomName;
|
||||
this.slug = slug;
|
||||
this.trackId = trackId;
|
||||
this.abstractText = abstractText;
|
||||
this.description = description;
|
||||
}
|
||||
|
||||
public long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public int getDayIndex() {
|
||||
return dayIndex;
|
||||
}
|
||||
|
||||
public Date getStartTime() {
|
||||
return startTime;
|
||||
}
|
||||
|
||||
public Date getEndTime() {
|
||||
return endTime;
|
||||
}
|
||||
|
||||
public String getRoomName() {
|
||||
return roomName;
|
||||
}
|
||||
|
||||
public String getSlug() {
|
||||
return slug;
|
||||
}
|
||||
|
||||
public long getTrackId() {
|
||||
return trackId;
|
||||
}
|
||||
|
||||
public String getAbstractText() {
|
||||
return abstractText;
|
||||
}
|
||||
|
||||
public String getDescription() {
|
||||
return description;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,43 @@
|
|||
package be.digitalia.fosdem.db.entities;
|
||||
|
||||
import androidx.room.ColumnInfo;
|
||||
import androidx.room.Entity;
|
||||
import androidx.room.Fts3;
|
||||
import androidx.room.PrimaryKey;
|
||||
import be.digitalia.fosdem.model.Event;
|
||||
|
||||
@Fts3
|
||||
@Entity(tableName = EventTitles.TABLE_NAME)
|
||||
public class EventTitles {
|
||||
|
||||
public static final String TABLE_NAME = "events_titles";
|
||||
|
||||
@PrimaryKey
|
||||
@ColumnInfo(name = "rowid")
|
||||
private final long id;
|
||||
private final String title;
|
||||
@ColumnInfo(name = "subtitle")
|
||||
private final String subTitle;
|
||||
|
||||
public EventTitles(Event event) {
|
||||
this(event.getId(), event.getTitle(), event.getSubTitle());
|
||||
}
|
||||
|
||||
public EventTitles(long id, String title, String subTitle) {
|
||||
this.id = id;
|
||||
this.title = title;
|
||||
this.subTitle = subTitle;
|
||||
}
|
||||
|
||||
public long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public String getTitle() {
|
||||
return title;
|
||||
}
|
||||
|
||||
public String getSubTitle() {
|
||||
return subTitle;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,30 @@
|
|||
package be.digitalia.fosdem.db.entities;
|
||||
|
||||
import androidx.room.ColumnInfo;
|
||||
import androidx.room.Entity;
|
||||
import androidx.room.Index;
|
||||
|
||||
@Entity(tableName = EventToPerson.TABLE_NAME, primaryKeys = {"event_id", "person_id"},
|
||||
indices = {@Index(value = {"person_id"}, name = "event_person_person_id_idx")})
|
||||
public class EventToPerson {
|
||||
|
||||
public static final String TABLE_NAME = "events_persons";
|
||||
|
||||
@ColumnInfo(name = "event_id")
|
||||
private final long eventId;
|
||||
@ColumnInfo(name = "person_id")
|
||||
private final long personId;
|
||||
|
||||
public EventToPerson(long eventId, long personId) {
|
||||
this.eventId = eventId;
|
||||
this.personId = personId;
|
||||
}
|
||||
|
||||
public long getEventId() {
|
||||
return eventId;
|
||||
}
|
||||
|
||||
public long getPersonId() {
|
||||
return personId;
|
||||
}
|
||||
}
|
|
@ -1,28 +1,27 @@
|
|||
package be.digitalia.fosdem.fragments;
|
||||
|
||||
import android.database.Cursor;
|
||||
import android.os.Bundle;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.loader.app.LoaderManager;
|
||||
import androidx.loader.app.LoaderManager.LoaderCallbacks;
|
||||
import androidx.loader.content.Loader;
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.Observer;
|
||||
import androidx.lifecycle.ViewModelProviders;
|
||||
import androidx.paging.PagedList;
|
||||
import androidx.recyclerview.widget.DividerItemDecoration;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import be.digitalia.fosdem.adapters.EventsAdapter;
|
||||
import be.digitalia.fosdem.model.StatusEvent;
|
||||
import be.digitalia.fosdem.viewmodels.LiveViewModel;
|
||||
|
||||
public abstract class BaseLiveListFragment extends RecyclerViewFragment implements LoaderCallbacks<Cursor> {
|
||||
|
||||
private static final int EVENTS_LOADER_ID = 1;
|
||||
public abstract class BaseLiveListFragment extends RecyclerViewFragment implements Observer<PagedList<StatusEvent>> {
|
||||
|
||||
private EventsAdapter adapter;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
adapter = new EventsAdapter(getActivity(), this, false);
|
||||
adapter = new EventsAdapter(getContext(), this, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -44,22 +43,29 @@ public abstract class BaseLiveListFragment extends RecyclerViewFragment implemen
|
|||
setEmptyText(getEmptyText());
|
||||
setProgressBarVisible(true);
|
||||
|
||||
LoaderManager.getInstance(this).initLoader(EVENTS_LOADER_ID, null, this);
|
||||
final LiveViewModel viewModel = ViewModelProviders.of(getParentFragment()).get(LiveViewModel.class);
|
||||
getDataSource(viewModel).observe(getViewLifecycleOwner(), this);
|
||||
}
|
||||
|
||||
private final Runnable preserveScrollPositionRunnable = new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
// Ensure we stay at scroll position 0 so we can see the insertion animation
|
||||
final RecyclerView recyclerView = getRecyclerView();
|
||||
if (recyclerView.getScrollY() == 0) {
|
||||
recyclerView.scrollToPosition(0);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
public void onChanged(PagedList<StatusEvent> events) {
|
||||
adapter.submitList(events, preserveScrollPositionRunnable);
|
||||
setProgressBarVisible(false);
|
||||
}
|
||||
|
||||
protected abstract String getEmptyText();
|
||||
|
||||
@Override
|
||||
public void onLoadFinished(@NonNull Loader<Cursor> loader, Cursor data) {
|
||||
if (data != null) {
|
||||
adapter.swapCursor(data);
|
||||
}
|
||||
|
||||
setProgressBarVisible(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoaderReset(@NonNull Loader<Cursor> loader) {
|
||||
adapter.swapCursor(null);
|
||||
}
|
||||
@NonNull
|
||||
protected abstract LiveData<PagedList<StatusEvent>> getDataSource(@NonNull LiveViewModel viewModel);
|
||||
}
|
||||
|
|
|
@ -2,41 +2,39 @@ package be.digitalia.fosdem.fragments;
|
|||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.database.Cursor;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.text.format.DateUtils;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.loader.app.LoaderManager;
|
||||
import androidx.loader.app.LoaderManager.LoaderCallbacks;
|
||||
import androidx.loader.content.Loader;
|
||||
import androidx.appcompat.view.ActionMode;
|
||||
import androidx.lifecycle.Observer;
|
||||
import androidx.lifecycle.ViewModelProviders;
|
||||
import androidx.recyclerview.widget.DividerItemDecoration;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import be.digitalia.fosdem.R;
|
||||
import be.digitalia.fosdem.adapters.BookmarksAdapter;
|
||||
import be.digitalia.fosdem.db.DatabaseManager;
|
||||
import be.digitalia.fosdem.loaders.SimpleCursorLoader;
|
||||
import be.digitalia.fosdem.model.Event;
|
||||
import be.digitalia.fosdem.providers.BookmarksExportProvider;
|
||||
import be.digitalia.fosdem.viewmodels.BookmarksViewModel;
|
||||
import be.digitalia.fosdem.widgets.MultiChoiceHelper;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Bookmarks list, optionally filterable.
|
||||
*
|
||||
* @author Christophe Beyls
|
||||
*/
|
||||
public class BookmarksListFragment extends RecyclerViewFragment implements LoaderCallbacks<Cursor> {
|
||||
public class BookmarksListFragment extends RecyclerViewFragment implements Observer<List<Event>> {
|
||||
|
||||
private static final int BOOKMARKS_LOADER_ID = 1;
|
||||
private static final String PREF_UPCOMING_ONLY = "bookmarks_upcoming_only";
|
||||
private static final String STATE_ADAPTER = "adapter";
|
||||
|
||||
private BookmarksViewModel viewModel;
|
||||
private BookmarksAdapter adapter;
|
||||
private boolean upcomingOnly;
|
||||
|
||||
private MenuItem filterMenuItem;
|
||||
private MenuItem upcomingOnlyMenuItem;
|
||||
|
@ -45,11 +43,53 @@ public class BookmarksListFragment extends RecyclerViewFragment implements Loade
|
|||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
adapter = new BookmarksAdapter((AppCompatActivity) getActivity(), this);
|
||||
if (savedInstanceState != null) {
|
||||
adapter.onRestoreInstanceState(savedInstanceState.getParcelable(STATE_ADAPTER));
|
||||
viewModel = ViewModelProviders.of(this).get(BookmarksViewModel.class);
|
||||
final MultiChoiceHelper.MultiChoiceModeListener multiChoiceModeListener = new MultiChoiceHelper.MultiChoiceModeListener() {
|
||||
|
||||
@Override
|
||||
public boolean onCreateActionMode(ActionMode mode, Menu menu) {
|
||||
mode.getMenuInflater().inflate(R.menu.action_mode_bookmarks, menu);
|
||||
return true;
|
||||
}
|
||||
upcomingOnly = getActivity().getPreferences(Context.MODE_PRIVATE).getBoolean(PREF_UPCOMING_ONLY, false);
|
||||
|
||||
private void updateSelectedCountDisplay(ActionMode mode) {
|
||||
int count = adapter.getMultiChoiceHelper().getCheckedItemCount();
|
||||
mode.setTitle(getResources().getQuantityString(R.plurals.selected, count, count));
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onPrepareActionMode(ActionMode mode, Menu menu) {
|
||||
updateSelectedCountDisplay(mode);
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean onActionItemClicked(ActionMode mode, MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
case R.id.delete:
|
||||
// Remove multiple bookmarks at once
|
||||
viewModel.removeBookmarks(adapter.getMultiChoiceHelper().getCheckedItemIds());
|
||||
mode.finish();
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onItemCheckedStateChanged(ActionMode mode, int position, long id, boolean checked) {
|
||||
updateSelectedCountDisplay(mode);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroyActionMode(ActionMode mode) {
|
||||
}
|
||||
};
|
||||
adapter = new BookmarksAdapter((AppCompatActivity) getActivity(), this, multiChoiceModeListener);
|
||||
if (savedInstanceState != null) {
|
||||
adapter.getMultiChoiceHelper().onRestoreInstanceState(savedInstanceState.getParcelable(STATE_ADAPTER));
|
||||
}
|
||||
boolean upcomingOnly = getActivity().getPreferences(Context.MODE_PRIVATE).getBoolean(PREF_UPCOMING_ONLY, false);
|
||||
viewModel.setUpcomingOnly(upcomingOnly);
|
||||
|
||||
setHasOptionsMenu(true);
|
||||
}
|
||||
|
@ -68,18 +108,18 @@ public class BookmarksListFragment extends RecyclerViewFragment implements Loade
|
|||
setEmptyText(getString(R.string.no_bookmark));
|
||||
setProgressBarVisible(true);
|
||||
|
||||
LoaderManager.getInstance(this).initLoader(BOOKMARKS_LOADER_ID, null, this);
|
||||
viewModel.getBookmarks().observe(getViewLifecycleOwner(), this);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSaveInstanceState(@NonNull Bundle outState) {
|
||||
super.onSaveInstanceState(outState);
|
||||
outState.putParcelable(STATE_ADAPTER, adapter.onSaveInstanceState());
|
||||
outState.putParcelable(STATE_ADAPTER, adapter.getMultiChoiceHelper().onSaveInstanceState());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onDestroyView() {
|
||||
adapter.onDestroyView();
|
||||
adapter.getMultiChoiceHelper().clearChoices();
|
||||
super.onDestroyView();
|
||||
}
|
||||
|
||||
|
@ -93,6 +133,7 @@ public class BookmarksListFragment extends RecyclerViewFragment implements Loade
|
|||
|
||||
private void updateFilterMenuItem() {
|
||||
if (filterMenuItem != null) {
|
||||
final boolean upcomingOnly = viewModel.getUpcomingOnly();
|
||||
filterMenuItem.setIcon(upcomingOnly ?
|
||||
R.drawable.ic_filter_list_selected_white_24dp
|
||||
: R.drawable.ic_filter_list_white_24dp);
|
||||
|
@ -111,12 +152,12 @@ public class BookmarksListFragment extends RecyclerViewFragment implements Loade
|
|||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
case R.id.upcoming_only:
|
||||
upcomingOnly = !upcomingOnly;
|
||||
final boolean upcomingOnly = !viewModel.getUpcomingOnly();
|
||||
viewModel.setUpcomingOnly(upcomingOnly);
|
||||
updateFilterMenuItem();
|
||||
getActivity().getPreferences(Context.MODE_PRIVATE).edit()
|
||||
.putBoolean(PREF_UPCOMING_ONLY, upcomingOnly)
|
||||
.apply();
|
||||
LoaderManager.getInstance(this).restartLoader(BOOKMARKS_LOADER_ID, null, this);
|
||||
return true;
|
||||
case R.id.export_bookmarks:
|
||||
Intent exportIntent = BookmarksExportProvider.getIntent(getActivity());
|
||||
|
@ -126,78 +167,9 @@ public class BookmarksListFragment extends RecyclerViewFragment implements Loade
|
|||
return false;
|
||||
}
|
||||
|
||||
private static class BookmarksLoader extends SimpleCursorLoader {
|
||||
|
||||
// Events that just started are still shown for 5 minutes
|
||||
private static final long TIME_OFFSET = 5L * DateUtils.MINUTE_IN_MILLIS;
|
||||
|
||||
private final boolean upcomingOnly;
|
||||
private final Handler handler;
|
||||
private final Runnable timeoutRunnable = new Runnable() {
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
onContentChanged();
|
||||
}
|
||||
};
|
||||
|
||||
public BookmarksLoader(Context context, boolean upcomingOnly) {
|
||||
super(context);
|
||||
this.upcomingOnly = upcomingOnly;
|
||||
this.handler = new Handler();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void deliverResult(Cursor cursor) {
|
||||
if (upcomingOnly && !isReset()) {
|
||||
handler.removeCallbacks(timeoutRunnable);
|
||||
// The loader will be refreshed when the start time of the first bookmark in the list is reached
|
||||
if ((cursor != null) && cursor.moveToFirst()) {
|
||||
long startTime = DatabaseManager.toEventStartTimeMillis(cursor);
|
||||
if (startTime != -1L) {
|
||||
long delay = startTime - (System.currentTimeMillis() - TIME_OFFSET);
|
||||
if (delay > 0L) {
|
||||
handler.postDelayed(timeoutRunnable, delay);
|
||||
} else {
|
||||
onContentChanged();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
super.deliverResult(cursor);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onReset() {
|
||||
super.onReset();
|
||||
if (upcomingOnly) {
|
||||
handler.removeCallbacks(timeoutRunnable);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Cursor getCursor() {
|
||||
return DatabaseManager.getInstance().getBookmarks(upcomingOnly ? System.currentTimeMillis() - TIME_OFFSET : 0L);
|
||||
}
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
|
||||
return new BookmarksLoader(getActivity(), upcomingOnly);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoadFinished(@NonNull Loader<Cursor> loader, Cursor data) {
|
||||
if (data != null) {
|
||||
adapter.swapCursor(data);
|
||||
}
|
||||
|
||||
public void onChanged(List<Event> bookmarks) {
|
||||
adapter.submitList(bookmarks);
|
||||
setProgressBarVisible(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoaderReset(@NonNull Loader<Cursor> loader) {
|
||||
adapter.swapCursor(null);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,28 +9,12 @@ import android.graphics.drawable.Animatable;
|
|||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.provider.CalendarContract;
|
||||
import android.text.Spannable;
|
||||
import android.text.SpannableString;
|
||||
import android.text.SpannableStringBuilder;
|
||||
import android.text.TextPaint;
|
||||
import android.text.TextUtils;
|
||||
import android.text.*;
|
||||
import android.text.method.LinkMovementMethod;
|
||||
import android.text.style.ClickableSpan;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.view.*;
|
||||
import android.widget.ImageView;
|
||||
import android.widget.TextView;
|
||||
|
||||
import com.google.android.material.snackbar.Snackbar;
|
||||
|
||||
import java.text.DateFormat;
|
||||
import java.util.Date;
|
||||
import java.util.Map;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.browser.customtabs.CustomTabsIntent;
|
||||
|
@ -42,15 +26,17 @@ import androidx.lifecycle.ViewModelProviders;
|
|||
import be.digitalia.fosdem.R;
|
||||
import be.digitalia.fosdem.activities.PersonInfoActivity;
|
||||
import be.digitalia.fosdem.api.FosdemApi;
|
||||
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.model.RoomStatus;
|
||||
import be.digitalia.fosdem.model.*;
|
||||
import be.digitalia.fosdem.utils.ClickableArrowKeyMovementMethod;
|
||||
import be.digitalia.fosdem.utils.DateUtils;
|
||||
import be.digitalia.fosdem.utils.StringUtils;
|
||||
import be.digitalia.fosdem.viewmodels.EventDetailsViewModel;
|
||||
import com.google.android.material.snackbar.Snackbar;
|
||||
|
||||
import java.text.DateFormat;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
public class EventDetailsFragment extends Fragment {
|
||||
|
||||
|
@ -73,7 +59,6 @@ public class EventDetailsFragment extends Fragment {
|
|||
private static final String ARG_EVENT = "event";
|
||||
|
||||
Event event;
|
||||
int personsCount = 1;
|
||||
ViewHolder holder;
|
||||
EventDetailsViewModel viewModel;
|
||||
|
||||
|
@ -152,12 +137,12 @@ public class EventDetailsFragment extends Fragment {
|
|||
if (roomImageResId != 0) {
|
||||
roomText.setSpan(new ClickableSpan() {
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
public void onClick(@NonNull View view) {
|
||||
RoomImageDialogFragment.newInstance(roomName, roomImageResId).show(getFragmentManager());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateDrawState(TextPaint ds) {
|
||||
public void updateDrawState(@NonNull TextPaint ds) {
|
||||
super.updateDrawState(ds);
|
||||
ds.setUnderlineText(false);
|
||||
}
|
||||
|
@ -213,15 +198,17 @@ public class EventDetailsFragment extends Fragment {
|
|||
updateBookmarkMenuItem(isBookmarked, true);
|
||||
}
|
||||
});
|
||||
viewModel.getEventDetails().observe(getViewLifecycleOwner(), new Observer<EventDetailsViewModel.EventDetails>() {
|
||||
viewModel.getEventDetails().observe(getViewLifecycleOwner(), new Observer<EventDetails>() {
|
||||
@Override
|
||||
public void onChanged(@Nullable EventDetailsViewModel.EventDetails eventDetails) {
|
||||
public void onChanged(EventDetails eventDetails) {
|
||||
if (eventDetails != null) {
|
||||
setEventDetails(eventDetails);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// Live room status
|
||||
FosdemApi.getRoomStatuses().observe(getViewLifecycleOwner(), new Observer<Map<String, RoomStatus>>() {
|
||||
FosdemApi.getRoomStatuses(getContext()).observe(getViewLifecycleOwner(), new Observer<Map<String, RoomStatus>>() {
|
||||
@Override
|
||||
public void onChanged(Map<String, RoomStatus> roomStatuses) {
|
||||
RoomStatus roomStatus = roomStatuses.get(event.getRoomName());
|
||||
|
@ -354,6 +341,8 @@ public class EventDetailsFragment extends Fragment {
|
|||
}
|
||||
description = StringUtils.stripHtml(description);
|
||||
// Add speaker info if available
|
||||
EventDetails details = viewModel.getEventDetails().getValue();
|
||||
final int personsCount = (details == null) ? 0 : details.getPersons().size();
|
||||
if (personsCount > 0) {
|
||||
description = String.format("%1$s: %2$s\n\n%3$s", getResources().getQuantityString(R.plurals.speakers, personsCount), event.getPersonsSummary(),
|
||||
description);
|
||||
|
@ -374,15 +363,14 @@ public class EventDetailsFragment extends Fragment {
|
|||
}
|
||||
}
|
||||
|
||||
void setEventDetails(@NonNull EventDetailsViewModel.EventDetails data) {
|
||||
void setEventDetails(@NonNull EventDetails eventDetails) {
|
||||
// 1. Persons
|
||||
if (data.persons != null) {
|
||||
personsCount = data.persons.size();
|
||||
if (personsCount > 0) {
|
||||
final List<Person> persons = eventDetails.getPersons();
|
||||
if (persons.size() > 0) {
|
||||
// Build a list of clickable persons
|
||||
SpannableStringBuilder sb = new SpannableStringBuilder();
|
||||
int length = 0;
|
||||
for (Person person : data.persons) {
|
||||
for (Person person : persons) {
|
||||
if (length != 0) {
|
||||
sb.append(", ");
|
||||
}
|
||||
|
@ -394,14 +382,14 @@ public class EventDetailsFragment extends Fragment {
|
|||
holder.personsTextView.setText(sb);
|
||||
holder.personsTextView.setVisibility(View.VISIBLE);
|
||||
}
|
||||
}
|
||||
|
||||
// 2. Links
|
||||
final List<Link> links = eventDetails.getLinks();
|
||||
holder.linksContainer.removeAllViews();
|
||||
if ((data.links != null) && (data.links.size() > 0)) {
|
||||
if (links.size() > 0) {
|
||||
holder.linksHeader.setVisibility(View.VISIBLE);
|
||||
holder.linksContainer.setVisibility(View.VISIBLE);
|
||||
for (Link link : data.links) {
|
||||
for (Link link : links) {
|
||||
View view = holder.inflater.inflate(R.layout.item_link, holder.linksContainer, false);
|
||||
TextView tv = view.findViewById(R.id.description);
|
||||
tv.setText(link.getDescription());
|
||||
|
@ -423,14 +411,14 @@ public class EventDetailsFragment extends Fragment {
|
|||
}
|
||||
|
||||
@Override
|
||||
public void onClick(View v) {
|
||||
public void onClick(@NonNull View v) {
|
||||
Context context = v.getContext();
|
||||
Intent intent = new Intent(context, PersonInfoActivity.class).putExtra(PersonInfoActivity.EXTRA_PERSON, person);
|
||||
context.startActivity(intent);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void updateDrawState(TextPaint ds) {
|
||||
public void updateDrawState(@NonNull TextPaint ds) {
|
||||
super.updateDrawState(ds);
|
||||
ds.setUnderlineText(false);
|
||||
}
|
||||
|
|
|
@ -1,14 +1,11 @@
|
|||
package be.digitalia.fosdem.fragments;
|
||||
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.os.Bundle;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.loader.content.Loader;
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.paging.PagedList;
|
||||
import be.digitalia.fosdem.R;
|
||||
import be.digitalia.fosdem.db.DatabaseManager;
|
||||
import be.digitalia.fosdem.loaders.BaseLiveLoader;
|
||||
import be.digitalia.fosdem.model.StatusEvent;
|
||||
import be.digitalia.fosdem.viewmodels.LiveViewModel;
|
||||
|
||||
public class NextLiveListFragment extends BaseLiveListFragment {
|
||||
|
||||
|
@ -19,22 +16,7 @@ public class NextLiveListFragment extends BaseLiveListFragment {
|
|||
|
||||
@NonNull
|
||||
@Override
|
||||
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
|
||||
return new NextLiveLoader(getActivity());
|
||||
}
|
||||
|
||||
private static class NextLiveLoader extends BaseLiveLoader {
|
||||
|
||||
private static final long INTERVAL = 30L * 60L * 1000L; // 30 minutes
|
||||
|
||||
public NextLiveLoader(Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Cursor getCursor() {
|
||||
long now = System.currentTimeMillis();
|
||||
return DatabaseManager.getInstance().getEvents(now, now + INTERVAL, -1L, true);
|
||||
}
|
||||
protected LiveData<PagedList<StatusEvent>> getDataSource(@NonNull LiveViewModel viewModel) {
|
||||
return viewModel.getNextEvents();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,14 +1,11 @@
|
|||
package be.digitalia.fosdem.fragments;
|
||||
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.os.Bundle;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.loader.content.Loader;
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.paging.PagedList;
|
||||
import be.digitalia.fosdem.R;
|
||||
import be.digitalia.fosdem.db.DatabaseManager;
|
||||
import be.digitalia.fosdem.loaders.BaseLiveLoader;
|
||||
import be.digitalia.fosdem.model.StatusEvent;
|
||||
import be.digitalia.fosdem.viewmodels.LiveViewModel;
|
||||
|
||||
public class NowLiveListFragment extends BaseLiveListFragment {
|
||||
|
||||
|
@ -19,20 +16,7 @@ public class NowLiveListFragment extends BaseLiveListFragment {
|
|||
|
||||
@NonNull
|
||||
@Override
|
||||
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
|
||||
return new NowLiveLoader(getActivity());
|
||||
}
|
||||
|
||||
private static class NowLiveLoader extends BaseLiveLoader {
|
||||
|
||||
public NowLiveLoader(Context context) {
|
||||
super(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Cursor getCursor() {
|
||||
long now = System.currentTimeMillis();
|
||||
return DatabaseManager.getInstance().getEvents(-1L, now, now, false);
|
||||
}
|
||||
protected LiveData<PagedList<StatusEvent>> getDataSource(@NonNull LiveViewModel viewModel) {
|
||||
return viewModel.getEventsInProgress();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,36 +2,27 @@ package be.digitalia.fosdem.fragments;
|
|||
|
||||
import android.app.Activity;
|
||||
import android.content.ActivityNotFoundException;
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.Menu;
|
||||
import android.view.MenuInflater;
|
||||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
|
||||
import android.view.*;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.browser.customtabs.CustomTabsIntent;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.loader.app.LoaderManager;
|
||||
import androidx.loader.app.LoaderManager.LoaderCallbacks;
|
||||
import androidx.loader.content.Loader;
|
||||
import androidx.lifecycle.Observer;
|
||||
import androidx.lifecycle.ViewModelProviders;
|
||||
import androidx.paging.PagedList;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import be.digitalia.fosdem.R;
|
||||
import be.digitalia.fosdem.adapters.ConcatAdapter;
|
||||
import be.digitalia.fosdem.adapters.EventsAdapter;
|
||||
import be.digitalia.fosdem.db.DatabaseManager;
|
||||
import be.digitalia.fosdem.loaders.SimpleCursorLoader;
|
||||
import be.digitalia.fosdem.model.Person;
|
||||
import be.digitalia.fosdem.model.StatusEvent;
|
||||
import be.digitalia.fosdem.utils.DateUtils;
|
||||
import be.digitalia.fosdem.viewmodels.PersonInfoViewModel;
|
||||
|
||||
public class PersonInfoListFragment extends RecyclerViewFragment implements LoaderCallbacks<Cursor> {
|
||||
public class PersonInfoListFragment extends RecyclerViewFragment implements Observer<PagedList<StatusEvent>> {
|
||||
|
||||
private static final int PERSON_EVENTS_LOADER_ID = 1;
|
||||
private static final String ARG_PERSON = "person";
|
||||
|
||||
private Person person;
|
||||
|
@ -49,7 +40,7 @@ public class PersonInfoListFragment extends RecyclerViewFragment implements Load
|
|||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
|
||||
adapter = new EventsAdapter(getActivity(), this);
|
||||
adapter = new EventsAdapter(getContext(), this);
|
||||
person = getArguments().getParcelable(ARG_PERSON);
|
||||
setHasOptionsMenu(true);
|
||||
}
|
||||
|
@ -63,8 +54,18 @@ public class PersonInfoListFragment extends RecyclerViewFragment implements Load
|
|||
public boolean onOptionsItemSelected(MenuItem item) {
|
||||
switch (item.getItemId()) {
|
||||
case R.id.more_info:
|
||||
if (adapter.getItemCount() > 0) {
|
||||
final int year = DateUtils.getYear(adapter.getItem(0).getDay().getDate().getTime());
|
||||
// Look for the first non-placeholder event in the paged list
|
||||
final PagedList<StatusEvent> list = adapter.getCurrentList();
|
||||
final int size = (list == null) ? 0 : list.size();
|
||||
StatusEvent statusEvent = null;
|
||||
for (int i = 0; i < size; ++i) {
|
||||
statusEvent = list.get(i);
|
||||
if (statusEvent != null) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (statusEvent != null) {
|
||||
final int year = DateUtils.getYear(statusEvent.getEvent().getDay().getDate().getTime());
|
||||
String url = person.getUrl(year);
|
||||
if (url != null) {
|
||||
try {
|
||||
|
@ -102,44 +103,17 @@ public class PersonInfoListFragment extends RecyclerViewFragment implements Load
|
|||
setEmptyText(getString(R.string.no_data));
|
||||
setProgressBarVisible(true);
|
||||
|
||||
LoaderManager.getInstance(this).initLoader(PERSON_EVENTS_LOADER_ID, null, this);
|
||||
}
|
||||
|
||||
private static class PersonEventsLoader extends SimpleCursorLoader {
|
||||
|
||||
private final Person person;
|
||||
|
||||
public PersonEventsLoader(Context context, Person person) {
|
||||
super(context);
|
||||
this.person = person;
|
||||
final PersonInfoViewModel viewModel = ViewModelProviders.of(this).get(PersonInfoViewModel.class);
|
||||
viewModel.setPerson(person);
|
||||
viewModel.getEvents().observe(getViewLifecycleOwner(), this);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Cursor getCursor() {
|
||||
return DatabaseManager.getInstance().getEvents(person);
|
||||
}
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
|
||||
return new PersonEventsLoader(getActivity(), person);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoadFinished(@NonNull Loader<Cursor> loader, Cursor data) {
|
||||
if (data != null) {
|
||||
adapter.swapCursor(data);
|
||||
}
|
||||
|
||||
public void onChanged(PagedList<StatusEvent> events) {
|
||||
adapter.submitList(events);
|
||||
setProgressBarVisible(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoaderReset(@NonNull Loader<Cursor> loader) {
|
||||
adapter.swapCursor(null);
|
||||
}
|
||||
|
||||
static class HeaderAdapter extends RecyclerView.Adapter<HeaderAdapter.ViewHolder> {
|
||||
|
||||
@Override
|
||||
|
|
|
@ -2,38 +2,36 @@ package be.digitalia.fosdem.fragments;
|
|||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.database.Cursor;
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.loader.app.LoaderManager;
|
||||
import androidx.loader.app.LoaderManager.LoaderCallbacks;
|
||||
import androidx.loader.content.Loader;
|
||||
import androidx.core.util.ObjectsCompat;
|
||||
import androidx.lifecycle.Observer;
|
||||
import androidx.lifecycle.ViewModelProviders;
|
||||
import androidx.paging.PagedList;
|
||||
import androidx.paging.PagedListAdapter;
|
||||
import androidx.recyclerview.widget.DiffUtil;
|
||||
import androidx.recyclerview.widget.DividerItemDecoration;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import be.digitalia.fosdem.R;
|
||||
import be.digitalia.fosdem.activities.PersonInfoActivity;
|
||||
import be.digitalia.fosdem.adapters.RecyclerViewCursorAdapter;
|
||||
import be.digitalia.fosdem.db.DatabaseManager;
|
||||
import be.digitalia.fosdem.loaders.SimpleCursorLoader;
|
||||
import be.digitalia.fosdem.adapters.SimpleItemCallback;
|
||||
import be.digitalia.fosdem.model.Person;
|
||||
import be.digitalia.fosdem.viewmodels.PersonsViewModel;
|
||||
|
||||
public class PersonsListFragment extends RecyclerViewFragment implements LoaderCallbacks<Cursor> {
|
||||
|
||||
private static final int PERSONS_LOADER_ID = 1;
|
||||
public class PersonsListFragment extends RecyclerViewFragment implements Observer<PagedList<Person>> {
|
||||
|
||||
private PersonsAdapter adapter;
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
adapter = new PersonsAdapter(getActivity());
|
||||
adapter = new PersonsAdapter();
|
||||
}
|
||||
|
||||
@NonNull
|
||||
|
@ -56,70 +54,49 @@ public class PersonsListFragment extends RecyclerViewFragment implements LoaderC
|
|||
setEmptyText(getString(R.string.no_data));
|
||||
setProgressBarVisible(true);
|
||||
|
||||
LoaderManager.getInstance(this).initLoader(PERSONS_LOADER_ID, null, this);
|
||||
}
|
||||
|
||||
private static class PersonsLoader extends SimpleCursorLoader {
|
||||
|
||||
PersonsLoader(Context context) {
|
||||
super(context);
|
||||
final PersonsViewModel viewModel = ViewModelProviders.of(this).get(PersonsViewModel.class);
|
||||
viewModel.getPersons().observe(getViewLifecycleOwner(), this);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Cursor getCursor() {
|
||||
return DatabaseManager.getInstance().getPersons();
|
||||
}
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
|
||||
return new PersonsLoader(getActivity());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoadFinished(@NonNull Loader<Cursor> loader, Cursor data) {
|
||||
if (data != null) {
|
||||
adapter.swapCursor(data);
|
||||
}
|
||||
|
||||
public void onChanged(PagedList<Person> persons) {
|
||||
adapter.submitList(persons);
|
||||
setProgressBarVisible(false);
|
||||
}
|
||||
|
||||
private static class PersonsAdapter extends PagedListAdapter<Person, PersonViewHolder> {
|
||||
|
||||
private static final DiffUtil.ItemCallback<Person> DIFF_CALLBACK = new SimpleItemCallback<Person>() {
|
||||
@Override
|
||||
public void onLoaderReset(@NonNull Loader<Cursor> loader) {
|
||||
adapter.swapCursor(null);
|
||||
public boolean areContentsTheSame(@NonNull Person oldItem, @NonNull Person newItem) {
|
||||
return ObjectsCompat.equals(oldItem.getName(), newItem.getName());
|
||||
}
|
||||
};
|
||||
|
||||
private static class PersonsAdapter extends RecyclerViewCursorAdapter<PersonViewHolder> {
|
||||
|
||||
private final LayoutInflater inflater;
|
||||
|
||||
PersonsAdapter(Context context) {
|
||||
inflater = LayoutInflater.from(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Person getItem(int position) {
|
||||
return DatabaseManager.toPerson((Cursor) super.getItem(position));
|
||||
PersonsAdapter() {
|
||||
super(DIFF_CALLBACK);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public PersonViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||
View view = inflater.inflate(R.layout.simple_list_item_1_material, parent, false);
|
||||
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.simple_list_item_1_material, parent, false);
|
||||
return new PersonViewHolder(view);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(PersonViewHolder holder, Cursor cursor) {
|
||||
holder.person = DatabaseManager.toPerson(cursor, holder.person);
|
||||
holder.textView.setText(holder.person.getName());
|
||||
public void onBindViewHolder(@NonNull PersonViewHolder holder, int position) {
|
||||
final Person person = getItem(position);
|
||||
if (person == null) {
|
||||
holder.clear();
|
||||
} else {
|
||||
holder.bind(person);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static class PersonViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener {
|
||||
TextView textView;
|
||||
final TextView textView;
|
||||
|
||||
Person person;
|
||||
|
||||
|
@ -129,8 +106,19 @@ public class PersonsListFragment extends RecyclerViewFragment implements LoaderC
|
|||
itemView.setOnClickListener(this);
|
||||
}
|
||||
|
||||
void clear() {
|
||||
this.person = null;
|
||||
textView.setText(null);
|
||||
}
|
||||
|
||||
void bind(@NonNull Person person) {
|
||||
this.person = person;
|
||||
textView.setText(person.getName());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
if (person != null) {
|
||||
final Context context = view.getContext();
|
||||
Intent intent = new Intent(context, PersonInfoActivity.class)
|
||||
.putExtra(PersonInfoActivity.EXTRA_PERSON, person);
|
||||
|
@ -138,3 +126,4 @@ public class PersonsListFragment extends RecyclerViewFragment implements LoaderC
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,40 +1,29 @@
|
|||
package be.digitalia.fosdem.fragments;
|
||||
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.os.Bundle;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.loader.app.LoaderManager;
|
||||
import androidx.loader.app.LoaderManager.LoaderCallbacks;
|
||||
import androidx.loader.content.Loader;
|
||||
import androidx.lifecycle.Observer;
|
||||
import androidx.lifecycle.ViewModelProviders;
|
||||
import androidx.paging.PagedList;
|
||||
import androidx.recyclerview.widget.DividerItemDecoration;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import be.digitalia.fosdem.R;
|
||||
import be.digitalia.fosdem.adapters.EventsAdapter;
|
||||
import be.digitalia.fosdem.db.DatabaseManager;
|
||||
import be.digitalia.fosdem.loaders.SimpleCursorLoader;
|
||||
import be.digitalia.fosdem.model.StatusEvent;
|
||||
import be.digitalia.fosdem.viewmodels.SearchViewModel;
|
||||
|
||||
public class SearchResultListFragment extends RecyclerViewFragment implements LoaderCallbacks<Cursor> {
|
||||
|
||||
private static final int EVENTS_LOADER_ID = 1;
|
||||
private static final String ARG_QUERY = "query";
|
||||
public class SearchResultListFragment extends RecyclerViewFragment implements Observer<PagedList<StatusEvent>> {
|
||||
|
||||
private EventsAdapter adapter;
|
||||
|
||||
public static SearchResultListFragment newInstance(String query) {
|
||||
SearchResultListFragment f = new SearchResultListFragment();
|
||||
Bundle args = new Bundle();
|
||||
args.putString(ARG_QUERY, query);
|
||||
f.setArguments(args);
|
||||
return f;
|
||||
public static SearchResultListFragment newInstance() {
|
||||
return new SearchResultListFragment();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
adapter = new EventsAdapter(getActivity(), this);
|
||||
adapter = new EventsAdapter(getContext(), this);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -51,42 +40,13 @@ public class SearchResultListFragment extends RecyclerViewFragment implements Lo
|
|||
setEmptyText(getString(R.string.no_search_result));
|
||||
setProgressBarVisible(true);
|
||||
|
||||
LoaderManager.getInstance(this).initLoader(EVENTS_LOADER_ID, null, this);
|
||||
}
|
||||
|
||||
private static class TextSearchLoader extends SimpleCursorLoader {
|
||||
|
||||
private final String query;
|
||||
|
||||
public TextSearchLoader(Context context, String query) {
|
||||
super(context);
|
||||
this.query = query;
|
||||
final SearchViewModel viewModel = ViewModelProviders.of(getActivity()).get(SearchViewModel.class);
|
||||
viewModel.getResults().observe(getViewLifecycleOwner(), this);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Cursor getCursor() {
|
||||
return DatabaseManager.getInstance().getSearchResults(query);
|
||||
}
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
|
||||
String query = getArguments().getString(ARG_QUERY);
|
||||
return new TextSearchLoader(getActivity(), query);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoadFinished(@NonNull Loader<Cursor> loader, Cursor data) {
|
||||
if (data != null) {
|
||||
adapter.swapCursor(data);
|
||||
}
|
||||
|
||||
public void onChanged(PagedList<StatusEvent> results) {
|
||||
adapter.submitList(results);
|
||||
setProgressBarVisible(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoaderReset(@NonNull Loader<Cursor> loader) {
|
||||
adapter.swapCursor(null);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,29 +1,29 @@
|
|||
package be.digitalia.fosdem.fragments;
|
||||
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
import android.os.Bundle;
|
||||
import android.os.Handler;
|
||||
import android.os.Message;
|
||||
import android.text.format.DateUtils;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.loader.app.LoaderManager;
|
||||
import androidx.loader.app.LoaderManager.LoaderCallbacks;
|
||||
import androidx.loader.content.Loader;
|
||||
import androidx.lifecycle.Observer;
|
||||
import androidx.lifecycle.ViewModelProviders;
|
||||
import androidx.recyclerview.widget.DividerItemDecoration;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import be.digitalia.fosdem.R;
|
||||
import be.digitalia.fosdem.adapters.TrackScheduleAdapter;
|
||||
import be.digitalia.fosdem.loaders.TrackScheduleLoader;
|
||||
import be.digitalia.fosdem.model.Day;
|
||||
import be.digitalia.fosdem.model.Event;
|
||||
import be.digitalia.fosdem.model.StatusEvent;
|
||||
import be.digitalia.fosdem.model.Track;
|
||||
import be.digitalia.fosdem.viewmodels.TrackScheduleViewModel;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class TrackScheduleListFragment extends RecyclerViewFragment
|
||||
implements TrackScheduleAdapter.EventClickListener, Handler.Callback, LoaderCallbacks<Cursor> {
|
||||
implements TrackScheduleAdapter.EventClickListener, Handler.Callback, Observer<List<StatusEvent>> {
|
||||
|
||||
/**
|
||||
* Interface implemented by container activities
|
||||
|
@ -32,7 +32,6 @@ public class TrackScheduleListFragment extends RecyclerViewFragment
|
|||
void onEventSelected(int position, Event event);
|
||||
}
|
||||
|
||||
private static final int EVENTS_LOADER_ID = 1;
|
||||
private static final int REFRESH_TIME_WHAT = 1;
|
||||
private static final long REFRESH_TIME_INTERVAL = DateUtils.MINUTE_IN_MILLIS;
|
||||
|
||||
|
@ -137,7 +136,10 @@ public class TrackScheduleListFragment extends RecyclerViewFragment
|
|||
setEmptyText(getString(R.string.no_data));
|
||||
setProgressBarVisible(true);
|
||||
|
||||
LoaderManager.getInstance(this).initLoader(EVENTS_LOADER_ID, null, this);
|
||||
Track track = getArguments().getParcelable(ARG_TRACK);
|
||||
final TrackScheduleViewModel viewModel = ViewModelProviders.of(this).get(TrackScheduleViewModel.class);
|
||||
viewModel.setTrack(day, track);
|
||||
viewModel.getSchedule().observe(getViewLifecycleOwner(), this);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -184,17 +186,9 @@ public class TrackScheduleListFragment extends RecyclerViewFragment
|
|||
return false;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
|
||||
Track track = getArguments().getParcelable(ARG_TRACK);
|
||||
return new TrackScheduleLoader(getActivity(), day, track);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoadFinished(@NonNull Loader<Cursor> loader, Cursor data) {
|
||||
if (data != null) {
|
||||
adapter.swapCursor(data);
|
||||
public void onChanged(List<StatusEvent> schedule) {
|
||||
adapter.submitList(schedule);
|
||||
|
||||
if (selectionEnabled) {
|
||||
int selectedPosition = adapter.getPositionForId(selectedId);
|
||||
|
@ -209,7 +203,7 @@ public class TrackScheduleListFragment extends RecyclerViewFragment
|
|||
getRecyclerView().scrollToPosition(selectedPosition);
|
||||
}
|
||||
// Notify the parent of the current selection to synchronize its state
|
||||
notifyEventSelected(selectedPosition, (selectedPosition == RecyclerView.NO_POSITION) ? null : adapter.getItem(selectedPosition));
|
||||
notifyEventSelected(selectedPosition, (selectedPosition == RecyclerView.NO_POSITION) ? null : schedule.get(selectedPosition).getEvent());
|
||||
|
||||
} else if (!isListAlreadyShown) {
|
||||
final int position = adapter.getPositionForId(selectedId);
|
||||
|
@ -218,13 +212,7 @@ public class TrackScheduleListFragment extends RecyclerViewFragment
|
|||
}
|
||||
}
|
||||
isListAlreadyShown = true;
|
||||
}
|
||||
|
||||
setProgressBarVisible(false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoaderReset(@NonNull Loader<Cursor> loader) {
|
||||
adapter.swapCursor(null);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -20,7 +20,7 @@ import androidx.lifecycle.Observer;
|
|||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import androidx.viewpager.widget.ViewPager;
|
||||
import be.digitalia.fosdem.R;
|
||||
import be.digitalia.fosdem.db.DatabaseManager;
|
||||
import be.digitalia.fosdem.db.AppDatabase;
|
||||
import be.digitalia.fosdem.model.Day;
|
||||
|
||||
public class TracksFragment extends Fragment implements RecycledViewPoolProvider, Observer<List<Day>> {
|
||||
|
@ -74,7 +74,7 @@ public class TracksFragment extends Fragment implements RecycledViewPoolProvider
|
|||
public void onActivityCreated(Bundle savedInstanceState) {
|
||||
super.onActivityCreated(savedInstanceState);
|
||||
|
||||
DatabaseManager.getInstance().getDays()
|
||||
AppDatabase.getInstance(getContext()).getScheduleDao().getDays()
|
||||
.observe(getViewLifecycleOwner(), this);
|
||||
}
|
||||
|
||||
|
|
|
@ -2,36 +2,31 @@ package be.digitalia.fosdem.fragments;
|
|||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.database.Cursor;
|
||||
import android.os.Bundle;
|
||||
import android.view.LayoutInflater;
|
||||
import android.view.View;
|
||||
import android.view.ViewGroup;
|
||||
import android.widget.TextView;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.fragment.app.Fragment;
|
||||
import androidx.loader.app.LoaderManager;
|
||||
import androidx.loader.app.LoaderManager.LoaderCallbacks;
|
||||
import androidx.loader.content.Loader;
|
||||
import androidx.recyclerview.widget.DividerItemDecoration;
|
||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||
import androidx.recyclerview.widget.RecyclerView;
|
||||
import androidx.lifecycle.Observer;
|
||||
import androidx.lifecycle.ViewModelProviders;
|
||||
import androidx.recyclerview.widget.*;
|
||||
import be.digitalia.fosdem.R;
|
||||
import be.digitalia.fosdem.activities.TrackScheduleActivity;
|
||||
import be.digitalia.fosdem.adapters.RecyclerViewCursorAdapter;
|
||||
import be.digitalia.fosdem.db.DatabaseManager;
|
||||
import be.digitalia.fosdem.loaders.SimpleCursorLoader;
|
||||
import be.digitalia.fosdem.adapters.SimpleItemCallback;
|
||||
import be.digitalia.fosdem.model.Day;
|
||||
import be.digitalia.fosdem.model.Track;
|
||||
import be.digitalia.fosdem.viewmodels.TracksViewModel;
|
||||
|
||||
public class TracksListFragment extends RecyclerViewFragment implements LoaderCallbacks<Cursor> {
|
||||
import java.util.List;
|
||||
|
||||
public class TracksListFragment extends RecyclerViewFragment implements Observer<List<Track>> {
|
||||
|
||||
private static final int TRACKS_LOADER_ID = 1;
|
||||
private static final String ARG_DAY = "day";
|
||||
|
||||
Day day;
|
||||
private Day day;
|
||||
private TracksAdapter adapter;
|
||||
|
||||
public static TracksListFragment newInstance(Day day) {
|
||||
|
@ -45,8 +40,8 @@ public class TracksListFragment extends RecyclerViewFragment implements LoaderCa
|
|||
@Override
|
||||
public void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
adapter = new TracksAdapter();
|
||||
day = getArguments().getParcelable(ARG_DAY);
|
||||
adapter = new TracksAdapter(day);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
@ -68,77 +63,50 @@ public class TracksListFragment extends RecyclerViewFragment implements LoaderCa
|
|||
setEmptyText(getString(R.string.no_data));
|
||||
setProgressBarVisible(true);
|
||||
|
||||
LoaderManager.getInstance(this).initLoader(TRACKS_LOADER_ID, null, this);
|
||||
}
|
||||
|
||||
private static class TracksLoader extends SimpleCursorLoader {
|
||||
|
||||
private final Day day;
|
||||
|
||||
public TracksLoader(Context context, Day day) {
|
||||
super(context);
|
||||
this.day = day;
|
||||
final TracksViewModel viewModel = ViewModelProviders.of(this).get(TracksViewModel.class);
|
||||
viewModel.setDay(day);
|
||||
viewModel.getTracks().observe(getViewLifecycleOwner(), this);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Cursor getCursor() {
|
||||
return DatabaseManager.getInstance().getTracks(day);
|
||||
}
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
|
||||
return new TracksLoader(getActivity(), day);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onLoadFinished(@NonNull Loader<Cursor> loader, Cursor data) {
|
||||
if (data != null) {
|
||||
adapter.swapCursor(data);
|
||||
}
|
||||
|
||||
public void onChanged(List<Track> tracks) {
|
||||
adapter.submitList(tracks);
|
||||
setProgressBarVisible(false);
|
||||
}
|
||||
|
||||
private static class TracksAdapter extends ListAdapter<Track, TrackViewHolder> {
|
||||
|
||||
private static final DiffUtil.ItemCallback<Track> DIFF_CALLBACK = new SimpleItemCallback<Track>() {
|
||||
@Override
|
||||
public void onLoaderReset(@NonNull Loader<Cursor> loader) {
|
||||
adapter.swapCursor(null);
|
||||
public boolean areContentsTheSame(@NonNull Track oldItem, @NonNull Track newItem) {
|
||||
return oldItem.getName().equals(newItem.getName())
|
||||
&& oldItem.getType() == newItem.getType();
|
||||
}
|
||||
};
|
||||
|
||||
private class TracksAdapter extends RecyclerViewCursorAdapter<TrackViewHolder> {
|
||||
private final Day day;
|
||||
|
||||
private final LayoutInflater inflater;
|
||||
|
||||
public TracksAdapter() {
|
||||
inflater = LayoutInflater.from(getContext());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Track getItem(int position) {
|
||||
return DatabaseManager.toTrack((Cursor) super.getItem(position));
|
||||
TracksAdapter(Day day) {
|
||||
super(DIFF_CALLBACK);
|
||||
this.day = day;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public TrackViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||
View view = inflater.inflate(R.layout.simple_list_item_2_material, parent, false);
|
||||
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.simple_list_item_2_material, parent, false);
|
||||
return new TrackViewHolder(view);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onBindViewHolder(TrackViewHolder holder, Cursor cursor) {
|
||||
holder.day = day;
|
||||
holder.track = DatabaseManager.toTrack(cursor, holder.track);
|
||||
holder.name.setText(holder.track.getName());
|
||||
holder.type.setText(holder.track.getType().getNameResId());
|
||||
holder.type.setTextColor(ContextCompat.getColor(holder.type.getContext(), holder.track.getType().getColorResId()));
|
||||
public void onBindViewHolder(@NonNull TrackViewHolder holder, int position) {
|
||||
holder.bind(day, getItem(position));
|
||||
}
|
||||
}
|
||||
|
||||
static class TrackViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener {
|
||||
TextView name;
|
||||
TextView type;
|
||||
final TextView name;
|
||||
final TextView type;
|
||||
|
||||
Day day;
|
||||
Track track;
|
||||
|
@ -150,6 +118,14 @@ public class TracksListFragment extends RecyclerViewFragment implements LoaderCa
|
|||
itemView.setOnClickListener(this);
|
||||
}
|
||||
|
||||
void bind(@NonNull Day day, @NonNull Track track) {
|
||||
this.day = day;
|
||||
this.track = track;
|
||||
name.setText(track.getName());
|
||||
type.setText(track.getType().getNameResId());
|
||||
type.setTextColor(ContextCompat.getColor(type.getContext(), track.getType().getColorResId()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
Context context = view.getContext();
|
||||
|
|
|
@ -1,100 +0,0 @@
|
|||
package be.digitalia.fosdem.livedata;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.os.AsyncTask;
|
||||
|
||||
import androidx.annotation.MainThread;
|
||||
import androidx.annotation.WorkerThread;
|
||||
import androidx.lifecycle.MutableLiveData;
|
||||
|
||||
/**
|
||||
* 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();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
package be.digitalia.fosdem.livedata;
|
||||
|
||||
import androidx.annotation.MainThread;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.MediatorLiveData;
|
||||
import androidx.lifecycle.Observer;
|
||||
|
||||
public class ExtraTransformations {
|
||||
|
||||
private ExtraTransformations() {
|
||||
}
|
||||
|
||||
@MainThread
|
||||
@NonNull
|
||||
public static <X> LiveData<X> distinctUntilChanged(@NonNull LiveData<X> source) {
|
||||
final MediatorLiveData<X> outputLiveData = new MediatorLiveData<>();
|
||||
outputLiveData.addSource(source, new Observer<X>() {
|
||||
|
||||
boolean mFirstTime = true;
|
||||
|
||||
@Override
|
||||
public void onChanged(X currentValue) {
|
||||
final X previousValue = outputLiveData.getValue();
|
||||
if (mFirstTime
|
||||
|| (previousValue == null && currentValue != null)
|
||||
|| (previousValue != null && !previousValue.equals(currentValue))) {
|
||||
mFirstTime = false;
|
||||
outputLiveData.setValue(currentValue);
|
||||
}
|
||||
}
|
||||
});
|
||||
return outputLiveData;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,58 @@
|
|||
package be.digitalia.fosdem.livedata;
|
||||
|
||||
import android.os.Handler;
|
||||
import android.os.Looper;
|
||||
import android.os.SystemClock;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.lifecycle.LiveData;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public class LiveDataFactory {
|
||||
|
||||
static final Handler handler = new Handler(Looper.getMainLooper());
|
||||
|
||||
private LiveDataFactory() {
|
||||
}
|
||||
|
||||
private static class IntervalLiveData extends LiveData<Long> implements Runnable {
|
||||
|
||||
private final long periodInMillis;
|
||||
private long updateTime = 0L;
|
||||
private long version = 0L;
|
||||
|
||||
IntervalLiveData(long periodInMillis) {
|
||||
this.periodInMillis = periodInMillis;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onActive() {
|
||||
final long now = SystemClock.elapsedRealtime();
|
||||
if (now >= updateTime) {
|
||||
update(now);
|
||||
} else {
|
||||
handler.postDelayed(this, updateTime - now);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onInactive() {
|
||||
handler.removeCallbacks(this);
|
||||
}
|
||||
|
||||
private void update(long now) {
|
||||
setValue(version++);
|
||||
updateTime = now + periodInMillis;
|
||||
handler.postDelayed(this, periodInMillis);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
update(SystemClock.elapsedRealtime());
|
||||
}
|
||||
}
|
||||
|
||||
public static LiveData<Long> interval(long period, @NonNull TimeUnit unit) {
|
||||
return new IntervalLiveData(unit.toMillis(period));
|
||||
}
|
||||
}
|
|
@ -1,42 +0,0 @@
|
|||
package be.digitalia.fosdem.loaders;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Handler;
|
||||
|
||||
/**
|
||||
* A cursor loader which also automatically refreshes its data at a specified interval.
|
||||
*
|
||||
* @author Christophe Beyls
|
||||
*
|
||||
*/
|
||||
public abstract class BaseLiveLoader extends SimpleCursorLoader {
|
||||
|
||||
private static final long REFRESH_INTERVAL = 60L * 1000L; // 1 minute
|
||||
|
||||
private final Handler handler;
|
||||
private final Runnable timeoutRunnable = new Runnable() {
|
||||
|
||||
@Override
|
||||
public void run() {
|
||||
onContentChanged();
|
||||
}
|
||||
};
|
||||
|
||||
public BaseLiveLoader(Context context) {
|
||||
super(context);
|
||||
this.handler = new Handler();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onForceLoad() {
|
||||
super.onForceLoad();
|
||||
handler.removeCallbacks(timeoutRunnable);
|
||||
handler.postDelayed(timeoutRunnable, REFRESH_INTERVAL);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onReset() {
|
||||
super.onReset();
|
||||
handler.removeCallbacks(timeoutRunnable);
|
||||
}
|
||||
}
|
|
@ -1,105 +0,0 @@
|
|||
package be.digitalia.fosdem.loaders;
|
||||
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
|
||||
import androidx.loader.content.AsyncTaskLoader;
|
||||
|
||||
/**
|
||||
* A CursorLoader that doesn't need a ContentProvider.
|
||||
*
|
||||
* @author Christophe Beyls
|
||||
*/
|
||||
public abstract class SimpleCursorLoader extends AsyncTaskLoader<Cursor> {
|
||||
private final ForceLoadContentObserver mObserver;
|
||||
|
||||
private Cursor mCursor;
|
||||
|
||||
/* Runs on a worker thread */
|
||||
@Override
|
||||
public Cursor loadInBackground() {
|
||||
Cursor cursor = getCursor();
|
||||
if (cursor != null) {
|
||||
// Ensure the cursor window is filled
|
||||
cursor.getCount();
|
||||
cursor.registerContentObserver(mObserver);
|
||||
}
|
||||
return cursor;
|
||||
}
|
||||
|
||||
/* Runs on the UI thread */
|
||||
@Override
|
||||
public void deliverResult(Cursor cursor) {
|
||||
if (isReset()) {
|
||||
// An async query came in while the loader is stopped
|
||||
if (cursor != null) {
|
||||
cursor.close();
|
||||
}
|
||||
return;
|
||||
}
|
||||
Cursor oldCursor = mCursor;
|
||||
mCursor = cursor;
|
||||
|
||||
if (isStarted()) {
|
||||
super.deliverResult(cursor);
|
||||
}
|
||||
|
||||
if (oldCursor != null && oldCursor != cursor && !oldCursor.isClosed()) {
|
||||
oldCursor.close();
|
||||
}
|
||||
}
|
||||
|
||||
public SimpleCursorLoader(Context context) {
|
||||
super(context);
|
||||
mObserver = new ForceLoadContentObserver();
|
||||
}
|
||||
|
||||
/**
|
||||
* Starts an asynchronous load of the data. When the result is ready the callbacks will be called on the UI thread. If a previous load has been completed
|
||||
* and is still valid the result may be passed to the callbacks immediately.
|
||||
*
|
||||
* Must be called from the UI thread
|
||||
*/
|
||||
@Override
|
||||
protected void onStartLoading() {
|
||||
if (mCursor != null) {
|
||||
deliverResult(mCursor);
|
||||
}
|
||||
if (takeContentChanged() || mCursor == null) {
|
||||
forceLoad();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Must be called from the UI thread
|
||||
*/
|
||||
@Override
|
||||
protected void onStopLoading() {
|
||||
// Attempt to cancel the current load task if possible.
|
||||
cancelLoad();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onCanceled(Cursor cursor) {
|
||||
if (cursor != null && !cursor.isClosed()) {
|
||||
cursor.close();
|
||||
}
|
||||
// Retry a refresh the next time the loader is started
|
||||
onContentChanged();
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void onReset() {
|
||||
super.onReset();
|
||||
|
||||
// Ensure the loader is stopped
|
||||
onStopLoading();
|
||||
|
||||
if (mCursor != null && !mCursor.isClosed()) {
|
||||
mCursor.close();
|
||||
}
|
||||
mCursor = null;
|
||||
}
|
||||
|
||||
protected abstract Cursor getCursor();
|
||||
}
|
|
@ -1,25 +0,0 @@
|
|||
package be.digitalia.fosdem.loaders;
|
||||
|
||||
import android.content.Context;
|
||||
import android.database.Cursor;
|
||||
|
||||
import be.digitalia.fosdem.db.DatabaseManager;
|
||||
import be.digitalia.fosdem.model.Day;
|
||||
import be.digitalia.fosdem.model.Track;
|
||||
|
||||
public class TrackScheduleLoader extends SimpleCursorLoader {
|
||||
|
||||
private final Day day;
|
||||
private final Track track;
|
||||
|
||||
public TrackScheduleLoader(Context context, Day day, Track track) {
|
||||
super(context);
|
||||
this.day = day;
|
||||
this.track = track;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected Cursor getCursor() {
|
||||
return DatabaseManager.getInstance().getEvents(day, track);
|
||||
}
|
||||
}
|
29
app/src/main/java/be/digitalia/fosdem/model/AlarmInfo.java
Normal file
29
app/src/main/java/be/digitalia/fosdem/model/AlarmInfo.java
Normal file
|
@ -0,0 +1,29 @@
|
|||
package be.digitalia.fosdem.model;
|
||||
|
||||
import androidx.room.ColumnInfo;
|
||||
import androidx.room.TypeConverters;
|
||||
import be.digitalia.fosdem.db.converters.NullableDateTypeConverters;
|
||||
|
||||
import java.util.Date;
|
||||
|
||||
public class AlarmInfo {
|
||||
|
||||
@ColumnInfo(name = "event_id")
|
||||
private long eventId;
|
||||
@ColumnInfo(name = "start_time")
|
||||
@TypeConverters({NullableDateTypeConverters.class})
|
||||
private Date startTime;
|
||||
|
||||
public AlarmInfo(long eventId, Date startTime) {
|
||||
this.eventId = eventId;
|
||||
this.startTime = startTime;
|
||||
}
|
||||
|
||||
public long getEventId() {
|
||||
return eventId;
|
||||
}
|
||||
|
||||
public Date getStartTime() {
|
||||
return startTime;
|
||||
}
|
||||
}
|
|
@ -2,20 +2,29 @@ package be.digitalia.fosdem.model;
|
|||
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.room.Entity;
|
||||
import androidx.room.PrimaryKey;
|
||||
import androidx.room.TypeConverters;
|
||||
import be.digitalia.fosdem.db.converters.NonNullDateTypeConverters;
|
||||
import be.digitalia.fosdem.utils.DateUtils;
|
||||
|
||||
import java.text.DateFormat;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Date;
|
||||
import java.util.Locale;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import be.digitalia.fosdem.utils.DateUtils;
|
||||
|
||||
@Entity(tableName = Day.TABLE_NAME)
|
||||
public class Day implements Comparable<Day>, Parcelable {
|
||||
|
||||
public static final String TABLE_NAME = "days";
|
||||
|
||||
private static final DateFormat DAY_DATE_FORMAT = DateUtils.withBelgiumTimeZone(new SimpleDateFormat("EEEE", Locale.US));
|
||||
|
||||
@PrimaryKey
|
||||
private int index;
|
||||
@TypeConverters({NonNullDateTypeConverters.class})
|
||||
@NonNull
|
||||
private Date date;
|
||||
|
||||
public Day() {
|
||||
|
@ -29,11 +38,12 @@ public class Day implements Comparable<Day>, Parcelable {
|
|||
this.index = index;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public Date getDate() {
|
||||
return date;
|
||||
}
|
||||
|
||||
public void setDate(Date date) {
|
||||
public void setDate(@NonNull Date date) {
|
||||
this.date = date;
|
||||
}
|
||||
|
||||
|
@ -45,6 +55,7 @@ public class Day implements Comparable<Day>, Parcelable {
|
|||
return DAY_DATE_FORMAT.format(date);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String toString() {
|
||||
return getName();
|
||||
|
@ -78,7 +89,7 @@ public class Day implements Comparable<Day>, Parcelable {
|
|||
@Override
|
||||
public void writeToParcel(Parcel out, int flags) {
|
||||
out.writeInt(index);
|
||||
out.writeLong((date == null) ? 0L : date.getTime());
|
||||
out.writeLong(date.getTime());
|
||||
}
|
||||
|
||||
public static final Parcelable.Creator<Day> CREATOR = new Parcelable.Creator<Day>() {
|
||||
|
@ -93,9 +104,6 @@ public class Day implements Comparable<Day>, Parcelable {
|
|||
|
||||
Day(Parcel in) {
|
||||
index = in.readInt();
|
||||
long time = in.readLong();
|
||||
if (time != 0L) {
|
||||
date = new Date(time);
|
||||
}
|
||||
date = new Date(in.readLong());
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,39 @@
|
|||
package be.digitalia.fosdem.model;
|
||||
|
||||
import android.text.TextUtils;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
public class DetailedEvent extends Event {
|
||||
|
||||
@NonNull
|
||||
private List<Person> persons;
|
||||
@NonNull
|
||||
private List<Link> links;
|
||||
|
||||
@Override
|
||||
@NonNull
|
||||
public String getPersonsSummary() {
|
||||
return TextUtils.join(", ", persons);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public List<Person> getPersons() {
|
||||
return persons;
|
||||
}
|
||||
|
||||
public void setPersons(@NonNull List<Person> persons) {
|
||||
this.persons = persons;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public List<Link> getLinks() {
|
||||
return links;
|
||||
}
|
||||
|
||||
public void setLinks(@NonNull List<Link> links) {
|
||||
this.links = links;
|
||||
}
|
||||
}
|
|
@ -2,30 +2,42 @@ package be.digitalia.fosdem.model;
|
|||
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
import android.text.TextUtils;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.room.ColumnInfo;
|
||||
import androidx.room.Embedded;
|
||||
import androidx.room.TypeConverters;
|
||||
import be.digitalia.fosdem.api.FosdemUrls;
|
||||
import be.digitalia.fosdem.db.converters.NullableDateTypeConverters;
|
||||
import be.digitalia.fosdem.utils.DateUtils;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
|
||||
import be.digitalia.fosdem.api.FosdemUrls;
|
||||
import be.digitalia.fosdem.utils.DateUtils;
|
||||
|
||||
public class Event implements Parcelable {
|
||||
|
||||
private long id;
|
||||
@Embedded(prefix = "day_")
|
||||
@NonNull
|
||||
private Day day;
|
||||
@ColumnInfo(name = "start_time")
|
||||
@TypeConverters({NullableDateTypeConverters.class})
|
||||
private Date startTime;
|
||||
@ColumnInfo(name = "end_time")
|
||||
@TypeConverters({NullableDateTypeConverters.class})
|
||||
private Date endTime;
|
||||
@ColumnInfo(name = "room_name")
|
||||
private String roomName;
|
||||
private String slug;
|
||||
private String title;
|
||||
@ColumnInfo(name = "subtitle")
|
||||
private String subTitle;
|
||||
@Embedded(prefix = "track_")
|
||||
@NonNull
|
||||
private Track track;
|
||||
@ColumnInfo(name = "abstract")
|
||||
private String abstractText;
|
||||
private String description;
|
||||
@ColumnInfo(name = "persons")
|
||||
private String personsSummary;
|
||||
private List<Person> persons; // Optional
|
||||
private List<Link> links; // Optional
|
||||
|
||||
public Event() {
|
||||
}
|
||||
|
@ -38,11 +50,12 @@ public class Event implements Parcelable {
|
|||
this.id = id;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public Day getDay() {
|
||||
return day;
|
||||
}
|
||||
|
||||
public void setDay(Day day) {
|
||||
public void setDay(@NonNull Day day) {
|
||||
this.day = day;
|
||||
}
|
||||
|
||||
|
@ -112,11 +125,12 @@ public class Event implements Parcelable {
|
|||
this.subTitle = subTitle;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public Track getTrack() {
|
||||
return track;
|
||||
}
|
||||
|
||||
public void setTrack(Track track) {
|
||||
public void setTrack(@NonNull Track track) {
|
||||
this.track = track;
|
||||
}
|
||||
|
||||
|
@ -136,13 +150,11 @@ public class Event implements Parcelable {
|
|||
this.description = description;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public String getPersonsSummary() {
|
||||
if (personsSummary != null) {
|
||||
return personsSummary;
|
||||
}
|
||||
if (persons != null) {
|
||||
return TextUtils.join(", ", persons);
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
|
@ -150,22 +162,7 @@ public class Event implements Parcelable {
|
|||
this.personsSummary = personsSummary;
|
||||
}
|
||||
|
||||
public List<Person> getPersons() {
|
||||
return persons;
|
||||
}
|
||||
|
||||
public void setPersons(List<Person> persons) {
|
||||
this.persons = persons;
|
||||
}
|
||||
|
||||
public List<Link> getLinks() {
|
||||
return links;
|
||||
}
|
||||
|
||||
public void setLinks(List<Link> links) {
|
||||
this.links = links;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String toString() {
|
||||
return title;
|
||||
|
@ -205,8 +202,6 @@ public class Event implements Parcelable {
|
|||
out.writeString(abstractText);
|
||||
out.writeString(description);
|
||||
out.writeString(personsSummary);
|
||||
out.writeTypedList(persons);
|
||||
out.writeTypedList(links);
|
||||
}
|
||||
|
||||
public static final Parcelable.Creator<Event> CREATOR = new Parcelable.Creator<Event>() {
|
||||
|
@ -238,7 +233,5 @@ public class Event implements Parcelable {
|
|||
abstractText = in.readString();
|
||||
description = in.readString();
|
||||
personsSummary = in.readString();
|
||||
persons = in.createTypedArrayList(Person.CREATOR);
|
||||
links = in.createTypedArrayList(Link.CREATOR);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,28 @@
|
|||
package be.digitalia.fosdem.model;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class EventDetails {
|
||||
|
||||
@NonNull
|
||||
private final List<Person> persons;
|
||||
@NonNull
|
||||
private final List<Link> links;
|
||||
|
||||
public EventDetails(@NonNull List<Person> persons, @NonNull List<Link> links) {
|
||||
this.persons = persons;
|
||||
this.links = links;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public List<Person> getPersons() {
|
||||
return persons;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public List<Link> getLinks() {
|
||||
return links;
|
||||
}
|
||||
}
|
|
@ -2,20 +2,50 @@ package be.digitalia.fosdem.model;
|
|||
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.room.ColumnInfo;
|
||||
import androidx.room.Entity;
|
||||
import androidx.room.Index;
|
||||
import androidx.room.PrimaryKey;
|
||||
|
||||
@Entity(tableName = "links", indices = {@Index(value = {"event_id"}, name = "link_event_id_idx")})
|
||||
public class Link implements Parcelable {
|
||||
|
||||
public static final String TABLE_NAME = "links";
|
||||
|
||||
@PrimaryKey(autoGenerate = true)
|
||||
private long id;
|
||||
@ColumnInfo(name = "event_id")
|
||||
private long eventId;
|
||||
@NonNull
|
||||
private String url;
|
||||
private String description;
|
||||
|
||||
public Link() {
|
||||
}
|
||||
|
||||
public long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(long id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
public long getEventId() {
|
||||
return eventId;
|
||||
}
|
||||
|
||||
public void setEventId(long eventId) {
|
||||
this.eventId = eventId;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public String getUrl() {
|
||||
return url;
|
||||
}
|
||||
|
||||
public void setUrl(String url) {
|
||||
public void setUrl(@NonNull String url) {
|
||||
this.url = url;
|
||||
}
|
||||
|
||||
|
@ -27,6 +57,7 @@ public class Link implements Parcelable {
|
|||
this.description = description;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String toString() {
|
||||
return description;
|
||||
|
@ -54,6 +85,8 @@ public class Link implements Parcelable {
|
|||
|
||||
@Override
|
||||
public void writeToParcel(Parcel out, int flags) {
|
||||
out.writeLong(id);
|
||||
out.writeLong(eventId);
|
||||
out.writeString(url);
|
||||
out.writeString(description);
|
||||
}
|
||||
|
@ -69,6 +102,8 @@ public class Link implements Parcelable {
|
|||
};
|
||||
|
||||
Link(Parcel in) {
|
||||
id = in.readLong();
|
||||
eventId = in.readLong();
|
||||
url = in.readString();
|
||||
description = in.readString();
|
||||
}
|
||||
|
|
|
@ -2,12 +2,22 @@ package be.digitalia.fosdem.model;
|
|||
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.room.ColumnInfo;
|
||||
import androidx.room.Entity;
|
||||
import androidx.room.Fts3;
|
||||
import androidx.room.PrimaryKey;
|
||||
import be.digitalia.fosdem.api.FosdemUrls;
|
||||
import be.digitalia.fosdem.utils.StringUtils;
|
||||
|
||||
@Fts3
|
||||
@Entity(tableName = Person.TABLE_NAME)
|
||||
public class Person implements Parcelable {
|
||||
|
||||
public static final String TABLE_NAME = "persons";
|
||||
|
||||
@PrimaryKey
|
||||
@ColumnInfo(name = "rowid")
|
||||
private long id;
|
||||
private String name;
|
||||
|
||||
|
@ -34,6 +44,7 @@ public class Person implements Parcelable {
|
|||
return FosdemUrls.getPerson(StringUtils.toSlug(name), year);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String toString() {
|
||||
return name;
|
||||
|
|
39
app/src/main/java/be/digitalia/fosdem/model/StatusEvent.java
Normal file
39
app/src/main/java/be/digitalia/fosdem/model/StatusEvent.java
Normal file
|
@ -0,0 +1,39 @@
|
|||
package be.digitalia.fosdem.model;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.room.ColumnInfo;
|
||||
import androidx.room.Embedded;
|
||||
|
||||
public class StatusEvent {
|
||||
|
||||
@Embedded
|
||||
@NonNull
|
||||
private Event event;
|
||||
@ColumnInfo(name = "is_bookmarked")
|
||||
private boolean isBookmarked;
|
||||
|
||||
public StatusEvent(@NonNull Event event, boolean isBookmarked) {
|
||||
this.event = event;
|
||||
this.isBookmarked = isBookmarked;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public Event getEvent() {
|
||||
return event;
|
||||
}
|
||||
|
||||
public boolean isBookmarked() {
|
||||
return isBookmarked;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(@Nullable Object obj) {
|
||||
if (this == obj)
|
||||
return true;
|
||||
if (obj == null)
|
||||
return false;
|
||||
StatusEvent other = (StatusEvent) obj;
|
||||
return event.equals(other.event);
|
||||
}
|
||||
}
|
|
@ -2,13 +2,19 @@ package be.digitalia.fosdem.model;
|
|||
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
|
||||
import androidx.annotation.ColorRes;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.StringRes;
|
||||
import androidx.room.Entity;
|
||||
import androidx.room.Index;
|
||||
import androidx.room.PrimaryKey;
|
||||
import be.digitalia.fosdem.R;
|
||||
|
||||
@Entity(tableName = Track.TABLE_NAME, indices = {@Index(value = {"name", "type"}, name = "track_main_idx", unique = true)})
|
||||
public class Track implements Parcelable {
|
||||
|
||||
public static final String TABLE_NAME = "tracks";
|
||||
|
||||
public enum Type {
|
||||
other(R.string.other, R.color.track_type_other, R.color.track_type_other_dark),
|
||||
keynote(R.string.keynote, R.color.track_type_keynote, R.color.track_type_keynote_dark),
|
||||
|
@ -43,33 +49,37 @@ public class Track implements Parcelable {
|
|||
}
|
||||
}
|
||||
|
||||
@PrimaryKey
|
||||
private long id;
|
||||
@NonNull
|
||||
private String name;
|
||||
@NonNull
|
||||
private Type type;
|
||||
|
||||
public Track() {
|
||||
}
|
||||
|
||||
public Track(String name, Type type) {
|
||||
public Track(@NonNull String name, @NonNull Type type) {
|
||||
this.name = name;
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
public long getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public void setId(long id) {
|
||||
this.id = id;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public Type getType() {
|
||||
return type;
|
||||
}
|
||||
|
||||
public void setType(Type type) {
|
||||
this.type = type;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
@Override
|
||||
public String toString() {
|
||||
return name;
|
||||
|
@ -101,6 +111,7 @@ public class Track implements Parcelable {
|
|||
|
||||
@Override
|
||||
public void writeToParcel(Parcel out, int flags) {
|
||||
out.writeLong(id);
|
||||
out.writeString(name);
|
||||
out.writeInt(type.ordinal());
|
||||
}
|
||||
|
@ -116,6 +127,7 @@ public class Track implements Parcelable {
|
|||
};
|
||||
|
||||
Track(Parcel in) {
|
||||
id = in.readLong();
|
||||
name = in.readString();
|
||||
type = Type.values()[in.readInt()];
|
||||
}
|
||||
|
|
|
@ -12,7 +12,7 @@ import java.util.List;
|
|||
import java.util.Locale;
|
||||
|
||||
import be.digitalia.fosdem.model.Day;
|
||||
import be.digitalia.fosdem.model.Event;
|
||||
import be.digitalia.fosdem.model.DetailedEvent;
|
||||
import be.digitalia.fosdem.model.Link;
|
||||
import be.digitalia.fosdem.model.Person;
|
||||
import be.digitalia.fosdem.model.Track;
|
||||
|
@ -23,7 +23,7 @@ import be.digitalia.fosdem.utils.DateUtils;
|
|||
*
|
||||
* @author Christophe Beyls
|
||||
*/
|
||||
public class EventsParser extends IterableAbstractPullParser<Event> {
|
||||
public class EventsParser extends IterableAbstractPullParser<DetailedEvent> {
|
||||
|
||||
private final DateFormat DATE_FORMAT = DateUtils.withBelgiumTimeZone(new SimpleDateFormat("yyyy-MM-dd", Locale.US));
|
||||
|
||||
|
@ -67,7 +67,7 @@ public class EventsParser extends IterableAbstractPullParser<Event> {
|
|||
}
|
||||
|
||||
@Override
|
||||
protected Event parseNext(XmlPullParser parser) throws Exception {
|
||||
protected DetailedEvent parseNext(XmlPullParser parser) throws Exception {
|
||||
while (!isNextEndTag("schedule")) {
|
||||
if (isStartTag()) {
|
||||
|
||||
|
@ -81,7 +81,7 @@ public class EventsParser extends IterableAbstractPullParser<Event> {
|
|||
currentRoom = parser.getAttributeValue(null, "name");
|
||||
break;
|
||||
case "event":
|
||||
Event event = new Event();
|
||||
DetailedEvent event = new DetailedEvent();
|
||||
event.setId(Long.parseLong(parser.getAttributeValue(null, "id")));
|
||||
event.setDay(currentDay);
|
||||
event.setRoomName(currentRoom);
|
||||
|
@ -151,6 +151,7 @@ public class EventsParser extends IterableAbstractPullParser<Event> {
|
|||
while (!isNextEndTag("links")) {
|
||||
if (isStartTag("link")) {
|
||||
Link link = new Link();
|
||||
link.setEventId(event.getId());
|
||||
link.setUrl(parser.getAttributeValue(null, "href"));
|
||||
link.setDescription(parser.nextText());
|
||||
|
||||
|
|
|
@ -11,30 +11,25 @@ import android.os.Build;
|
|||
import android.os.ParcelFileDescriptor;
|
||||
import android.provider.OpenableColumns;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import java.io.BufferedWriter;
|
||||
import java.io.FileNotFoundException;
|
||||
import java.io.IOException;
|
||||
import java.io.OutputStream;
|
||||
import java.io.OutputStreamWriter;
|
||||
import java.text.DateFormat;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Calendar;
|
||||
import java.util.Locale;
|
||||
import java.util.TimeZone;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.core.app.ShareCompat;
|
||||
import be.digitalia.fosdem.BuildConfig;
|
||||
import be.digitalia.fosdem.R;
|
||||
import be.digitalia.fosdem.api.FosdemUrls;
|
||||
import be.digitalia.fosdem.db.DatabaseManager;
|
||||
import be.digitalia.fosdem.db.AppDatabase;
|
||||
import be.digitalia.fosdem.model.Event;
|
||||
import be.digitalia.fosdem.utils.DateUtils;
|
||||
import be.digitalia.fosdem.utils.ICalendarWriter;
|
||||
import be.digitalia.fosdem.utils.StringUtils;
|
||||
|
||||
import java.io.*;
|
||||
import java.text.DateFormat;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.Calendar;
|
||||
import java.util.Locale;
|
||||
import java.util.TimeZone;
|
||||
|
||||
/**
|
||||
* Content Provider generating the current bookmarks list in iCalendar format.
|
||||
*/
|
||||
|
@ -105,7 +100,7 @@ public class BookmarksExportProvider extends ContentProvider {
|
|||
for (String col : projection) {
|
||||
if (OpenableColumns.DISPLAY_NAME.equals(col)) {
|
||||
cols[i] = OpenableColumns.DISPLAY_NAME;
|
||||
values[i++] = getContext().getString(R.string.export_bookmarks_file_name, DatabaseManager.getInstance().getYear());
|
||||
values[i++] = getContext().getString(R.string.export_bookmarks_file_name, AppDatabase.getInstance(getContext()).getScheduleDao().getYear());
|
||||
} else if (OpenableColumns.SIZE.equals(col)) {
|
||||
cols[i] = OpenableColumns.SIZE;
|
||||
// Unknown size, content will be generated on-the-fly
|
||||
|
@ -126,7 +121,10 @@ public class BookmarksExportProvider extends ContentProvider {
|
|||
public ParcelFileDescriptor openFile(@NonNull Uri uri, @NonNull String mode) throws FileNotFoundException {
|
||||
try {
|
||||
ParcelFileDescriptor[] pipe = ParcelFileDescriptor.createPipe();
|
||||
new DownloadThread(new ParcelFileDescriptor.AutoCloseOutputStream(pipe[1])).start();
|
||||
new DownloadThread(
|
||||
new ParcelFileDescriptor.AutoCloseOutputStream(pipe[1]),
|
||||
AppDatabase.getInstance(getContext())
|
||||
).start();
|
||||
return pipe[0];
|
||||
} catch (IOException e) {
|
||||
throw new FileNotFoundException("Could not open pipe");
|
||||
|
@ -149,15 +147,17 @@ public class BookmarksExportProvider extends ContentProvider {
|
|||
static class DownloadThread extends Thread {
|
||||
private final ICalendarWriter writer;
|
||||
|
||||
private final AppDatabase appDatabase;
|
||||
private final Calendar calendar = Calendar.getInstance(DateUtils.getBelgiumTimeZone(), Locale.US);
|
||||
private final DateFormat dateFormat;
|
||||
private final String dtStamp;
|
||||
private final TextUtils.StringSplitter personsSplitter = new StringUtils.SimpleStringSplitter(", ");
|
||||
|
||||
DownloadThread(OutputStream out) {
|
||||
DownloadThread(OutputStream out, AppDatabase appDatabase) {
|
||||
this.writer = new ICalendarWriter(new BufferedWriter(new OutputStreamWriter(out)));
|
||||
|
||||
// Format all times in GMT
|
||||
this.appDatabase = appDatabase;
|
||||
this.dateFormat = new SimpleDateFormat("yyyyMMdd'T'HHmmss'Z'", Locale.US);
|
||||
this.dateFormat.setTimeZone(TimeZone.getTimeZone("GMT+0"));
|
||||
this.dtStamp = dateFormat.format(System.currentTimeMillis());
|
||||
|
@ -166,23 +166,16 @@ public class BookmarksExportProvider extends ContentProvider {
|
|||
@Override
|
||||
public void run() {
|
||||
try {
|
||||
final Cursor cursor = DatabaseManager.getInstance().getBookmarks(0L);
|
||||
try {
|
||||
final Event[] bookmarks = appDatabase.getBookmarksDao().getBookmarks();
|
||||
writer.write("BEGIN", "VCALENDAR");
|
||||
writer.write("VERSION", "2.0");
|
||||
writer.write("PRODID", "-//" + BuildConfig.APPLICATION_ID + "//NONSGML " + BuildConfig.VERSION_NAME + "//EN");
|
||||
|
||||
Event event = null;
|
||||
while (cursor.moveToNext()) {
|
||||
event = DatabaseManager.toEvent(cursor, event);
|
||||
for (Event event : bookmarks) {
|
||||
writeEvent(event);
|
||||
}
|
||||
|
||||
writer.write("END", "VCALENDAR");
|
||||
|
||||
} finally {
|
||||
cursor.close();
|
||||
}
|
||||
} catch (Exception ignore) {
|
||||
} finally {
|
||||
try {
|
||||
|
|
|
@ -6,9 +6,8 @@ import android.content.ContentValues;
|
|||
import android.database.Cursor;
|
||||
import android.net.Uri;
|
||||
import android.text.TextUtils;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import be.digitalia.fosdem.db.DatabaseManager;
|
||||
import be.digitalia.fosdem.db.AppDatabase;
|
||||
|
||||
/**
|
||||
* Simple content provider responsible for search suggestions.
|
||||
|
@ -60,6 +59,6 @@ public class SearchSuggestionProvider extends ContentProvider {
|
|||
String limitParam = uri.getQueryParameter("limit");
|
||||
int limit = TextUtils.isEmpty(limitParam) ? DEFAULT_MAX_RESULTS : Integer.parseInt(limitParam);
|
||||
|
||||
return DatabaseManager.getInstance().getSearchSuggestionResults(query, limit);
|
||||
return AppDatabase.getInstance(getContext()).getScheduleDao().getSearchSuggestionResults(query, limit);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,16 +1,11 @@
|
|||
package be.digitalia.fosdem.services;
|
||||
|
||||
import android.app.AlarmManager;
|
||||
import android.app.Notification;
|
||||
import android.app.NotificationChannel;
|
||||
import android.app.NotificationManager;
|
||||
import android.app.PendingIntent;
|
||||
import android.app.*;
|
||||
import android.content.ComponentName;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.SharedPreferences;
|
||||
import android.content.pm.PackageManager;
|
||||
import android.database.Cursor;
|
||||
import android.graphics.Typeface;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
|
@ -20,13 +15,9 @@ import android.text.SpannableString;
|
|||
import android.text.TextUtils;
|
||||
import android.text.format.DateUtils;
|
||||
import android.text.style.StyleSpan;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.RequiresApi;
|
||||
import androidx.core.app.AlarmManagerCompat;
|
||||
import androidx.core.app.JobIntentService;
|
||||
import androidx.core.app.NotificationCompat;
|
||||
import androidx.core.app.NotificationManagerCompat;
|
||||
import androidx.core.app.*;
|
||||
import androidx.core.app.TaskStackBuilder;
|
||||
import androidx.core.content.ContextCompat;
|
||||
import androidx.preference.PreferenceManager;
|
||||
|
@ -35,8 +26,9 @@ import be.digitalia.fosdem.R;
|
|||
import be.digitalia.fosdem.activities.EventDetailsActivity;
|
||||
import be.digitalia.fosdem.activities.MainActivity;
|
||||
import be.digitalia.fosdem.activities.RoomImageDialogActivity;
|
||||
import be.digitalia.fosdem.db.DatabaseManager;
|
||||
import be.digitalia.fosdem.db.AppDatabase;
|
||||
import be.digitalia.fosdem.fragments.SettingsFragment;
|
||||
import be.digitalia.fosdem.model.AlarmInfo;
|
||||
import be.digitalia.fosdem.model.Event;
|
||||
import be.digitalia.fosdem.receivers.AlarmReceiver;
|
||||
import be.digitalia.fosdem.utils.StringUtils;
|
||||
|
@ -56,6 +48,12 @@ public class AlarmIntentService extends JobIntentService {
|
|||
|
||||
public static final String ACTION_UPDATE_ALARMS = BuildConfig.APPLICATION_ID + ".action.UPDATE_ALARMS";
|
||||
public static final String ACTION_DISABLE_ALARMS = BuildConfig.APPLICATION_ID + ".action.DISABLE_ALARMS";
|
||||
public static final String ACTION_ADD_BOOKMARK = BuildConfig.APPLICATION_ID + ".action.ADD_BOOKMARK";
|
||||
public static final String EXTRA_EVENT_ID = "event_id";
|
||||
public static final String EXTRA_EVENT_START_TIME = "event_start";
|
||||
public static final String ACTION_REMOVE_BOOKMARKS = BuildConfig.APPLICATION_ID + ".action.REMOVE_BOOKMARKS";
|
||||
public static final String EXTRA_EVENT_IDS = "event_ids";
|
||||
|
||||
|
||||
private AlarmManager alarmManager;
|
||||
|
||||
|
@ -89,12 +87,9 @@ public class AlarmIntentService extends JobIntentService {
|
|||
final long delay = getDelay();
|
||||
final long now = System.currentTimeMillis();
|
||||
boolean hasAlarms = false;
|
||||
Cursor cursor = DatabaseManager.getInstance().getBookmarks(0L);
|
||||
try {
|
||||
while (cursor.moveToNext()) {
|
||||
long eventId = DatabaseManager.toEventId(cursor);
|
||||
long notificationTime = DatabaseManager.toEventStartTimeMillis(cursor) - delay;
|
||||
PendingIntent pi = getAlarmPendingIntent(eventId);
|
||||
for (AlarmInfo info : AppDatabase.getInstance(this).getBookmarksDao().getBookmarksAlarmInfo(0L)) {
|
||||
final long notificationTime = info.getStartTime() == null ? -1L : info.getStartTime().getTime() - delay;
|
||||
PendingIntent pi = getAlarmPendingIntent(info.getEventId());
|
||||
if (notificationTime < now) {
|
||||
// Cancel pending alarms that are now scheduled in the past, if any
|
||||
alarmManager.cancel(pi);
|
||||
|
@ -103,10 +98,6 @@ public class AlarmIntentService extends JobIntentService {
|
|||
hasAlarms = true;
|
||||
}
|
||||
}
|
||||
|
||||
} finally {
|
||||
cursor.close();
|
||||
}
|
||||
setAlarmReceiverEnabled(hasAlarms);
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && hasAlarms) {
|
||||
createNotificationChannel(this);
|
||||
|
@ -117,24 +108,18 @@ public class AlarmIntentService extends JobIntentService {
|
|||
case ACTION_DISABLE_ALARMS: {
|
||||
|
||||
// Cancel alarms of every bookmark in the future
|
||||
Cursor cursor = DatabaseManager.getInstance().getBookmarks(System.currentTimeMillis());
|
||||
try {
|
||||
while (cursor.moveToNext()) {
|
||||
long eventId = DatabaseManager.toEventId(cursor);
|
||||
alarmManager.cancel(getAlarmPendingIntent(eventId));
|
||||
}
|
||||
} finally {
|
||||
cursor.close();
|
||||
for (AlarmInfo info : AppDatabase.getInstance(this).getBookmarksDao().getBookmarksAlarmInfo(System.currentTimeMillis())) {
|
||||
alarmManager.cancel(getAlarmPendingIntent(info.getEventId()));
|
||||
}
|
||||
setAlarmReceiverEnabled(false);
|
||||
|
||||
break;
|
||||
}
|
||||
case DatabaseManager.ACTION_ADD_BOOKMARK: {
|
||||
case ACTION_ADD_BOOKMARK: {
|
||||
|
||||
long delay = getDelay();
|
||||
long eventId = intent.getLongExtra(DatabaseManager.EXTRA_EVENT_ID, -1L);
|
||||
long startTime = intent.getLongExtra(DatabaseManager.EXTRA_EVENT_START_TIME, -1L);
|
||||
long eventId = intent.getLongExtra(EXTRA_EVENT_ID, -1L);
|
||||
long startTime = intent.getLongExtra(EXTRA_EVENT_START_TIME, -1L);
|
||||
// Only schedule future events. If they start before the delay, the alarm will go off immediately
|
||||
if ((startTime == -1L) || (startTime < System.currentTimeMillis())) {
|
||||
break;
|
||||
|
@ -147,10 +132,10 @@ public class AlarmIntentService extends JobIntentService {
|
|||
|
||||
break;
|
||||
}
|
||||
case DatabaseManager.ACTION_REMOVE_BOOKMARKS: {
|
||||
case ACTION_REMOVE_BOOKMARKS: {
|
||||
|
||||
// Cancel matching alarms, might they exist or not
|
||||
long[] eventIds = intent.getLongArrayExtra(DatabaseManager.EXTRA_EVENT_IDS);
|
||||
long[] eventIds = intent.getLongArrayExtra(EXTRA_EVENT_IDS);
|
||||
for (long eventId : eventIds) {
|
||||
alarmManager.cancel(getAlarmPendingIntent(eventId));
|
||||
}
|
||||
|
@ -160,7 +145,7 @@ public class AlarmIntentService extends JobIntentService {
|
|||
case AlarmReceiver.ACTION_NOTIFY_EVENT: {
|
||||
|
||||
long eventId = Long.parseLong(intent.getDataString());
|
||||
Event event = DatabaseManager.getInstance().getEvent(eventId);
|
||||
Event event = AppDatabase.getInstance(this).getScheduleDao().getEvent(eventId);
|
||||
if (event != null) {
|
||||
NotificationManagerCompat.from(this).notify((int) eventId, buildNotification(event));
|
||||
}
|
||||
|
|
|
@ -1,13 +0,0 @@
|
|||
package be.digitalia.fosdem.utils;
|
||||
|
||||
public class ArrayUtils {
|
||||
|
||||
public static int indexOf(long[] array, long value) {
|
||||
for (int i = 0; i < array.length; ++i) {
|
||||
if (array[i] == value) {
|
||||
return i;
|
||||
}
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,73 @@
|
|||
package be.digitalia.fosdem.viewmodels;
|
||||
|
||||
import android.app.Application;
|
||||
import android.os.AsyncTask;
|
||||
import android.text.format.DateUtils;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.arch.core.util.Function;
|
||||
import androidx.lifecycle.AndroidViewModel;
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.MutableLiveData;
|
||||
import androidx.lifecycle.Transformations;
|
||||
import be.digitalia.fosdem.db.AppDatabase;
|
||||
import be.digitalia.fosdem.livedata.LiveDataFactory;
|
||||
import be.digitalia.fosdem.model.Event;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public class BookmarksViewModel extends AndroidViewModel {
|
||||
|
||||
// In upcomingOnly mode, events that just started are still shown for 5 minutes
|
||||
static final long TIME_OFFSET = 5L * DateUtils.MINUTE_IN_MILLIS;
|
||||
|
||||
private final AppDatabase appDatabase = AppDatabase.getInstance(getApplication());
|
||||
private final MutableLiveData<Boolean> upcomingOnly = new MutableLiveData<>();
|
||||
private final LiveData<List<Event>> bookmarks = Transformations.switchMap(upcomingOnly,
|
||||
new Function<Boolean, LiveData<List<Event>>>() {
|
||||
@Override
|
||||
public LiveData<List<Event>> apply(Boolean upcomingOnly) {
|
||||
if (upcomingOnly == Boolean.TRUE) {
|
||||
// Refresh upcoming bookmarks every 2 minutes
|
||||
final LiveData<Long> heartbeat = LiveDataFactory.interval(2L, TimeUnit.MINUTES);
|
||||
return Transformations.switchMap(heartbeat,
|
||||
new Function<Long, LiveData<List<Event>>>() {
|
||||
@Override
|
||||
public LiveData<List<Event>> apply(Long version) {
|
||||
return appDatabase.getBookmarksDao().getBookmarks(System.currentTimeMillis() - TIME_OFFSET);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return appDatabase.getBookmarksDao().getBookmarks(-1L);
|
||||
}
|
||||
});
|
||||
|
||||
public BookmarksViewModel(@NonNull Application application) {
|
||||
super(application);
|
||||
}
|
||||
|
||||
public void setUpcomingOnly(boolean upcomingOnly) {
|
||||
final Boolean boxedUpcomingOnly = upcomingOnly;
|
||||
if (!boxedUpcomingOnly.equals(this.upcomingOnly.getValue())) {
|
||||
this.upcomingOnly.setValue(boxedUpcomingOnly);
|
||||
}
|
||||
}
|
||||
|
||||
public boolean getUpcomingOnly() {
|
||||
return Boolean.TRUE.equals(this.upcomingOnly.getValue());
|
||||
}
|
||||
|
||||
public LiveData<List<Event>> getBookmarks() {
|
||||
return bookmarks;
|
||||
}
|
||||
|
||||
public void removeBookmarks(final long[] eventIds) {
|
||||
AsyncTask.SERIAL_EXECUTOR.execute(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
appDatabase.getBookmarksDao().removeBookmarks(eventIds);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
|
@ -1,87 +1,47 @@
|
|||
package be.digitalia.fosdem.viewmodels;
|
||||
|
||||
import android.app.Application;
|
||||
import android.content.BroadcastReceiver;
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
import android.content.IntentFilter;
|
||||
import android.os.AsyncTask;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.arch.core.util.Function;
|
||||
import androidx.lifecycle.AndroidViewModel;
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
|
||||
import be.digitalia.fosdem.db.DatabaseManager;
|
||||
import be.digitalia.fosdem.livedata.AsyncTaskLiveData;
|
||||
import androidx.lifecycle.MutableLiveData;
|
||||
import androidx.lifecycle.Transformations;
|
||||
import be.digitalia.fosdem.db.AppDatabase;
|
||||
import be.digitalia.fosdem.livedata.ExtraTransformations;
|
||||
import be.digitalia.fosdem.model.Event;
|
||||
import be.digitalia.fosdem.model.Link;
|
||||
import be.digitalia.fosdem.model.Person;
|
||||
import be.digitalia.fosdem.utils.ArrayUtils;
|
||||
import be.digitalia.fosdem.model.EventDetails;
|
||||
|
||||
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>() {
|
||||
|
||||
private final AppDatabase appDatabase = AppDatabase.getInstance(getApplication());
|
||||
private final MutableLiveData<Event> event = new MutableLiveData<>();
|
||||
private final LiveData<Boolean> bookmarkStatus = Transformations.switchMap(event,
|
||||
new Function<Event, LiveData<Boolean>>() {
|
||||
@Override
|
||||
protected Boolean loadInBackground() throws Exception {
|
||||
return DatabaseManager.getInstance().isBookmarked(event);
|
||||
public LiveData<Boolean> apply(Event event) {
|
||||
// Prevent animating the UI when a bookmark is added back or removed back
|
||||
return ExtraTransformations.distinctUntilChanged(
|
||||
appDatabase.getBookmarksDao().getBookmarkStatus(event)
|
||||
);
|
||||
}
|
||||
};
|
||||
private final AsyncTaskLiveData<EventDetails> eventDetails = new AsyncTaskLiveData<EventDetails>() {
|
||||
|
||||
});
|
||||
private final LiveData<EventDetails> eventDetails = Transformations.switchMap(event,
|
||||
new Function<Event, LiveData<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;
|
||||
public LiveData<EventDetails> apply(Event event) {
|
||||
return appDatabase.getScheduleDao().getEventDetails(event);
|
||||
}
|
||||
};
|
||||
|
||||
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();
|
||||
if (!event.equals(this.event.getValue())) {
|
||||
this.event.setValue(event);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -90,39 +50,23 @@ public class EventDetailsViewModel extends AndroidViewModel {
|
|||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
final Event event = this.event.getValue();
|
||||
final Boolean isBookmarked = bookmarkStatus.getValue();
|
||||
if (event != null && isBookmarked != null) {
|
||||
AsyncTask.SERIAL_EXECUTOR.execute(new Runnable() {
|
||||
@Override
|
||||
protected Void doInBackground(Boolean... remove) {
|
||||
if (remove[0]) {
|
||||
DatabaseManager.getInstance().removeBookmark(event);
|
||||
public void run() {
|
||||
if (isBookmarked) {
|
||||
appDatabase.getBookmarksDao().removeBookmark(event);
|
||||
} else {
|
||||
DatabaseManager.getInstance().addBookmark(event);
|
||||
appDatabase.getBookmarksDao().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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,30 +1,49 @@
|
|||
package be.digitalia.fosdem.viewmodels;
|
||||
|
||||
import android.app.Application;
|
||||
import android.os.AsyncTask;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.arch.core.util.Function;
|
||||
import androidx.lifecycle.AndroidViewModel;
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.ViewModel;
|
||||
import be.digitalia.fosdem.db.DatabaseManager;
|
||||
import be.digitalia.fosdem.livedata.AsyncTaskLiveData;
|
||||
import androidx.lifecycle.MutableLiveData;
|
||||
import androidx.lifecycle.Transformations;
|
||||
import be.digitalia.fosdem.db.AppDatabase;
|
||||
import be.digitalia.fosdem.model.Event;
|
||||
|
||||
public class EventViewModel extends ViewModel {
|
||||
public class EventViewModel extends AndroidViewModel {
|
||||
|
||||
private long eventId = -1L;
|
||||
|
||||
private final AsyncTaskLiveData<Event> event = new AsyncTaskLiveData<Event>() {
|
||||
private final AppDatabase appDatabase = AppDatabase.getInstance(getApplication());
|
||||
private final MutableLiveData<Long> eventId = new MutableLiveData<>();
|
||||
private final LiveData<Event> event = Transformations.switchMap(eventId,
|
||||
new Function<Long, LiveData<Event>>() {
|
||||
@Override
|
||||
protected Event loadInBackground() throws Exception {
|
||||
return DatabaseManager.getInstance().getEvent(eventId);
|
||||
public LiveData<Event> apply(final Long id) {
|
||||
final MutableLiveData<Event> resultLiveData = new MutableLiveData<>();
|
||||
AsyncTask.THREAD_POOL_EXECUTOR.execute(new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
final Event result = appDatabase.getScheduleDao().getEvent(id);
|
||||
resultLiveData.postValue(result);
|
||||
}
|
||||
});
|
||||
return resultLiveData;
|
||||
}
|
||||
});
|
||||
|
||||
public EventViewModel(@NonNull Application application) {
|
||||
super(application);
|
||||
}
|
||||
};
|
||||
|
||||
public boolean hasEventId() {
|
||||
return this.eventId != -1L;
|
||||
return this.eventId.getValue() != null;
|
||||
}
|
||||
|
||||
public void setEventId(long eventId) {
|
||||
if (this.eventId != eventId) {
|
||||
this.eventId = eventId;
|
||||
event.forceLoad();
|
||||
Long newEventId = eventId;
|
||||
if (!newEventId.equals(this.eventId.getValue())) {
|
||||
this.eventId.setValue(newEventId);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,54 @@
|
|||
package be.digitalia.fosdem.viewmodels;
|
||||
|
||||
import android.app.Application;
|
||||
import android.text.format.DateUtils;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.arch.core.util.Function;
|
||||
import androidx.lifecycle.AndroidViewModel;
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.Transformations;
|
||||
import androidx.paging.LivePagedListBuilder;
|
||||
import androidx.paging.PagedList;
|
||||
import be.digitalia.fosdem.db.AppDatabase;
|
||||
import be.digitalia.fosdem.livedata.LiveDataFactory;
|
||||
import be.digitalia.fosdem.model.StatusEvent;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
public class LiveViewModel extends AndroidViewModel {
|
||||
|
||||
static final long NEXT_EVENTS_INTERVAL = 30L * DateUtils.MINUTE_IN_MILLIS;
|
||||
|
||||
private final AppDatabase appDatabase = AppDatabase.getInstance(getApplication());
|
||||
private final LiveData<Long> heartbeat = LiveDataFactory.interval(1L, TimeUnit.MINUTES);
|
||||
private final LiveData<PagedList<StatusEvent>> nextEvents = Transformations.switchMap(heartbeat,
|
||||
new Function<Long, LiveData<PagedList<StatusEvent>>>() {
|
||||
@Override
|
||||
public LiveData<PagedList<StatusEvent>> apply(Long version) {
|
||||
final long now = System.currentTimeMillis();
|
||||
return new LivePagedListBuilder<>(appDatabase.getScheduleDao().getEventsWithStartTime(now, now + NEXT_EVENTS_INTERVAL), 20)
|
||||
.build();
|
||||
}
|
||||
});
|
||||
private final LiveData<PagedList<StatusEvent>> eventsInProgress = Transformations.switchMap(heartbeat,
|
||||
new Function<Long, LiveData<PagedList<StatusEvent>>>() {
|
||||
@Override
|
||||
public LiveData<PagedList<StatusEvent>> apply(Long version) {
|
||||
final long now = System.currentTimeMillis();
|
||||
return new LivePagedListBuilder<>(appDatabase.getScheduleDao().getEventsInProgress(now), 20)
|
||||
.build();
|
||||
}
|
||||
});
|
||||
|
||||
public LiveViewModel(@NonNull Application application) {
|
||||
super(application);
|
||||
}
|
||||
|
||||
public LiveData<PagedList<StatusEvent>> getNextEvents() {
|
||||
return nextEvents;
|
||||
}
|
||||
|
||||
public LiveData<PagedList<StatusEvent>> getEventsInProgress() {
|
||||
return eventsInProgress;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,42 @@
|
|||
package be.digitalia.fosdem.viewmodels;
|
||||
|
||||
import android.app.Application;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.arch.core.util.Function;
|
||||
import androidx.lifecycle.AndroidViewModel;
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.MutableLiveData;
|
||||
import androidx.lifecycle.Transformations;
|
||||
import androidx.paging.LivePagedListBuilder;
|
||||
import androidx.paging.PagedList;
|
||||
import be.digitalia.fosdem.db.AppDatabase;
|
||||
import be.digitalia.fosdem.model.Person;
|
||||
import be.digitalia.fosdem.model.StatusEvent;
|
||||
|
||||
public class PersonInfoViewModel extends AndroidViewModel {
|
||||
|
||||
private final AppDatabase appDatabase = AppDatabase.getInstance(getApplication());
|
||||
private final MutableLiveData<Person> person = new MutableLiveData<>();
|
||||
private final LiveData<PagedList<StatusEvent>> events = Transformations.switchMap(person,
|
||||
new Function<Person, LiveData<PagedList<StatusEvent>>>() {
|
||||
@Override
|
||||
public LiveData<PagedList<StatusEvent>> apply(Person person) {
|
||||
return new LivePagedListBuilder<>(appDatabase.getScheduleDao().getEvents(person), 20)
|
||||
.build();
|
||||
}
|
||||
});
|
||||
|
||||
public PersonInfoViewModel(@NonNull Application application) {
|
||||
super(application);
|
||||
}
|
||||
|
||||
public void setPerson(@NonNull Person person) {
|
||||
if (!person.equals(this.person.getValue())) {
|
||||
this.person.setValue(person);
|
||||
}
|
||||
}
|
||||
|
||||
public LiveData<PagedList<StatusEvent>> getEvents() {
|
||||
return events;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,27 @@
|
|||
package be.digitalia.fosdem.viewmodels;
|
||||
|
||||
import android.app.Application;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.lifecycle.AndroidViewModel;
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.paging.LivePagedListBuilder;
|
||||
import androidx.paging.PagedList;
|
||||
import be.digitalia.fosdem.db.AppDatabase;
|
||||
import be.digitalia.fosdem.model.Person;
|
||||
|
||||
public class PersonsViewModel extends AndroidViewModel {
|
||||
|
||||
private final AppDatabase appDatabase = AppDatabase.getInstance(getApplication());
|
||||
private final LiveData<PagedList<Person>> persons
|
||||
= new LivePagedListBuilder<>(appDatabase.getScheduleDao().getPersons(), 100)
|
||||
.build();
|
||||
|
||||
public PersonsViewModel(@NonNull Application application) {
|
||||
super(application);
|
||||
}
|
||||
|
||||
public LiveData<PagedList<Person>> getPersons() {
|
||||
return persons;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,58 @@
|
|||
package be.digitalia.fosdem.viewmodels;
|
||||
|
||||
import android.app.Application;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.arch.core.util.Function;
|
||||
import androidx.lifecycle.AndroidViewModel;
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.MutableLiveData;
|
||||
import androidx.lifecycle.Transformations;
|
||||
import androidx.paging.LivePagedListBuilder;
|
||||
import androidx.paging.PagedList;
|
||||
import be.digitalia.fosdem.db.AppDatabase;
|
||||
import be.digitalia.fosdem.model.StatusEvent;
|
||||
|
||||
public class SearchViewModel extends AndroidViewModel {
|
||||
|
||||
private static final int MIN_SEARCH_LENGTH = 3;
|
||||
|
||||
private final AppDatabase appDatabase = AppDatabase.getInstance(getApplication());
|
||||
private final MutableLiveData<String> query = new MutableLiveData<>();
|
||||
private final LiveData<PagedList<StatusEvent>> results = Transformations.switchMap(query,
|
||||
new Function<String, LiveData<PagedList<StatusEvent>>>() {
|
||||
@Override
|
||||
public LiveData<PagedList<StatusEvent>> apply(String query) {
|
||||
if (isQueryTooShort(query)) {
|
||||
MutableLiveData<PagedList<StatusEvent>> emptyResult = new MutableLiveData<>();
|
||||
emptyResult.setValue(null);
|
||||
return emptyResult;
|
||||
}
|
||||
return new LivePagedListBuilder<>(appDatabase.getScheduleDao().getSearchResults(query), 20)
|
||||
.build();
|
||||
}
|
||||
});
|
||||
|
||||
public SearchViewModel(@NonNull Application application) {
|
||||
super(application);
|
||||
}
|
||||
|
||||
public void setQuery(@NonNull String query) {
|
||||
if (!query.equals(this.query.getValue())) {
|
||||
this.query.setValue(query);
|
||||
}
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public String getQuery() {
|
||||
return query.getValue();
|
||||
}
|
||||
|
||||
public static boolean isQueryTooShort(String value) {
|
||||
return (value == null) || (value.length() < MIN_SEARCH_LENGTH);
|
||||
}
|
||||
|
||||
public LiveData<PagedList<StatusEvent>> getResults() {
|
||||
return results;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,44 @@
|
|||
package be.digitalia.fosdem.viewmodels;
|
||||
|
||||
import android.app.Application;
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.arch.core.util.Function;
|
||||
import androidx.core.util.Pair;
|
||||
import androidx.lifecycle.AndroidViewModel;
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.MutableLiveData;
|
||||
import androidx.lifecycle.Transformations;
|
||||
import be.digitalia.fosdem.db.AppDatabase;
|
||||
import be.digitalia.fosdem.model.Day;
|
||||
import be.digitalia.fosdem.model.StatusEvent;
|
||||
import be.digitalia.fosdem.model.Track;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
public class TrackScheduleViewModel extends AndroidViewModel {
|
||||
|
||||
private final AppDatabase appDatabase = AppDatabase.getInstance(getApplication());
|
||||
private final MutableLiveData<Pair<Day, Track>> dayTrack = new MutableLiveData<>();
|
||||
private final LiveData<List<StatusEvent>> schedule = Transformations.switchMap(dayTrack,
|
||||
new Function<Pair<Day, Track>, LiveData<List<StatusEvent>>>() {
|
||||
@Override
|
||||
public LiveData<List<StatusEvent>> apply(Pair<Day, Track> dayTrack) {
|
||||
return appDatabase.getScheduleDao().getEvents(dayTrack.first, dayTrack.second);
|
||||
}
|
||||
});
|
||||
|
||||
public TrackScheduleViewModel(@NonNull Application application) {
|
||||
super(application);
|
||||
}
|
||||
|
||||
public void setTrack(@NonNull Day day, @NonNull Track track) {
|
||||
Pair<Day, Track> dayTrack = Pair.create(day, track);
|
||||
if (!dayTrack.equals(this.dayTrack.getValue())) {
|
||||
this.dayTrack.setValue(dayTrack);
|
||||
}
|
||||
}
|
||||
|
||||
public LiveData<List<StatusEvent>> getSchedule() {
|
||||
return schedule;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,42 @@
|
|||
package be.digitalia.fosdem.viewmodels;
|
||||
|
||||
import android.app.Application;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.arch.core.util.Function;
|
||||
import androidx.lifecycle.AndroidViewModel;
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.MutableLiveData;
|
||||
import androidx.lifecycle.Transformations;
|
||||
import be.digitalia.fosdem.db.AppDatabase;
|
||||
import be.digitalia.fosdem.model.Day;
|
||||
import be.digitalia.fosdem.model.Track;
|
||||
|
||||
public class TracksViewModel extends AndroidViewModel {
|
||||
|
||||
private final AppDatabase appDatabase = AppDatabase.getInstance(getApplication());
|
||||
private final MutableLiveData<Day> day = new MutableLiveData<>();
|
||||
private final LiveData<List<Track>> tracks = Transformations.switchMap(day,
|
||||
new Function<Day, LiveData<List<Track>>>() {
|
||||
@Override
|
||||
public LiveData<List<Track>> apply(Day day) {
|
||||
return appDatabase.getScheduleDao().getTracks(day);
|
||||
}
|
||||
});
|
||||
|
||||
public TracksViewModel(@NonNull Application application) {
|
||||
super(application);
|
||||
}
|
||||
|
||||
public void setDay(@NonNull Day day) {
|
||||
if (!day.equals(this.day.getValue())) {
|
||||
this.day.setValue(day);
|
||||
}
|
||||
}
|
||||
|
||||
public LiveData<List<Track>> getTracks() {
|
||||
return tracks;
|
||||
}
|
||||
}
|
|
@ -1,6 +1,5 @@
|
|||
package be.digitalia.fosdem.widgets;
|
||||
|
||||
import android.content.Context;
|
||||
import android.os.Parcel;
|
||||
import android.os.Parcelable;
|
||||
import android.util.SparseBooleanArray;
|
||||
|
@ -8,7 +7,6 @@ import android.view.Menu;
|
|||
import android.view.MenuItem;
|
||||
import android.view.View;
|
||||
import android.widget.Checkable;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import androidx.appcompat.view.ActionMode;
|
||||
|
@ -31,18 +29,18 @@ public class MultiChoiceHelper {
|
|||
public static abstract class ViewHolder extends RecyclerView.ViewHolder {
|
||||
|
||||
View.OnClickListener clickListener;
|
||||
MultiChoiceHelper multiChoiceHelper;
|
||||
final MultiChoiceHelper multiChoiceHelper;
|
||||
|
||||
public ViewHolder(View itemView) {
|
||||
public ViewHolder(@NonNull View itemView, @NonNull MultiChoiceHelper helper) {
|
||||
super(itemView);
|
||||
multiChoiceHelper = helper;
|
||||
itemView.setOnClickListener(new View.OnClickListener() {
|
||||
@Override
|
||||
public void onClick(View view) {
|
||||
if (isMultiChoiceActive()) {
|
||||
int position = getAdapterPosition();
|
||||
if (position != RecyclerView.NO_POSITION) {
|
||||
multiChoiceHelper.toggleItemChecked(position, false);
|
||||
updateCheckedState(position);
|
||||
multiChoiceHelper.toggleItemChecked(position);
|
||||
}
|
||||
} else {
|
||||
if (clickListener != null) {
|
||||
|
@ -54,20 +52,25 @@ public class MultiChoiceHelper {
|
|||
itemView.setOnLongClickListener(new View.OnLongClickListener() {
|
||||
@Override
|
||||
public boolean onLongClick(View view) {
|
||||
if ((multiChoiceHelper == null) || isMultiChoiceActive()) {
|
||||
if (isMultiChoiceActive()) {
|
||||
return false;
|
||||
}
|
||||
int position = getAdapterPosition();
|
||||
if (position != RecyclerView.NO_POSITION) {
|
||||
multiChoiceHelper.setItemChecked(position, true, false);
|
||||
updateCheckedState(position);
|
||||
multiChoiceHelper.setItemChecked(position, true);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void updateCheckedState(int position) {
|
||||
public void setOnClickListener(View.OnClickListener clickListener) {
|
||||
this.clickListener = clickListener;
|
||||
}
|
||||
|
||||
public void bindSelection() {
|
||||
int position = getAdapterPosition();
|
||||
if (position != RecyclerView.NO_POSITION) {
|
||||
final boolean isChecked = multiChoiceHelper.isItemChecked(position);
|
||||
if (itemView instanceof Checkable) {
|
||||
((Checkable) itemView).setChecked(isChecked);
|
||||
|
@ -75,20 +78,10 @@ public class MultiChoiceHelper {
|
|||
itemView.setActivated(isChecked);
|
||||
}
|
||||
}
|
||||
|
||||
public void setOnClickListener(View.OnClickListener clickListener) {
|
||||
this.clickListener = clickListener;
|
||||
}
|
||||
|
||||
public void bind(MultiChoiceHelper multiChoiceHelper, int position) {
|
||||
this.multiChoiceHelper = multiChoiceHelper;
|
||||
if (multiChoiceHelper != null) {
|
||||
updateCheckedState(position);
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isMultiChoiceActive() {
|
||||
return (multiChoiceHelper != null) && (multiChoiceHelper.getCheckedItemCount() > 0);
|
||||
return multiChoiceHelper.getCheckedItemCount() > 0;
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -105,6 +98,8 @@ public class MultiChoiceHelper {
|
|||
void onItemCheckedStateChanged(ActionMode mode, int position, long id, boolean checked);
|
||||
}
|
||||
|
||||
public static final Object SELECTION_PAYLOAD = new Object();
|
||||
|
||||
private static final int CHECK_POSITION_SEARCH_DISTANCE = 20;
|
||||
|
||||
private final AppCompatActivity activity;
|
||||
|
@ -129,10 +124,6 @@ public class MultiChoiceHelper {
|
|||
}
|
||||
}
|
||||
|
||||
public Context getContext() {
|
||||
return activity;
|
||||
}
|
||||
|
||||
public void setMultiChoiceModeListener(MultiChoiceModeListener listener) {
|
||||
if (listener == null) {
|
||||
multiChoiceModeCallback = null;
|
||||
|
@ -182,7 +173,7 @@ public class MultiChoiceHelper {
|
|||
}
|
||||
checkedItemCount = 0;
|
||||
|
||||
adapter.notifyItemRangeChanged(start, end - start + 1);
|
||||
adapter.notifyItemRangeChanged(start, end - start + 1, SELECTION_PAYLOAD);
|
||||
|
||||
if (choiceActionMode != null) {
|
||||
choiceActionMode.finish();
|
||||
|
@ -190,7 +181,7 @@ public class MultiChoiceHelper {
|
|||
}
|
||||
}
|
||||
|
||||
public void setItemChecked(int position, boolean value, boolean notifyChanged) {
|
||||
public void setItemChecked(int position, boolean value) {
|
||||
// Start selection mode if needed. We don't need to if we're unchecking something.
|
||||
if (value) {
|
||||
startSupportActionModeIfNeeded();
|
||||
|
@ -216,9 +207,7 @@ public class MultiChoiceHelper {
|
|||
checkedItemCount--;
|
||||
}
|
||||
|
||||
if (notifyChanged) {
|
||||
adapter.notifyItemChanged(position);
|
||||
}
|
||||
adapter.notifyItemChanged(position, SELECTION_PAYLOAD);
|
||||
|
||||
if (choiceActionMode != null) {
|
||||
multiChoiceModeCallback.onItemCheckedStateChanged(choiceActionMode, position, id, value);
|
||||
|
@ -229,8 +218,8 @@ public class MultiChoiceHelper {
|
|||
}
|
||||
}
|
||||
|
||||
public void toggleItemChecked(int position, boolean notifyChanged) {
|
||||
setItemChecked(position, !isItemChecked(position), notifyChanged);
|
||||
public void toggleItemChecked(int position) {
|
||||
setItemChecked(position, !isItemChecked(position));
|
||||
}
|
||||
|
||||
public Parcelable onSaveInstanceState() {
|
||||
|
|
Loading…
Reference in a new issue