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 {
|
defaultConfig {
|
||||||
applicationId "be.digitalia.fosdem"
|
applicationId "be.digitalia.fosdem"
|
||||||
minSdkVersion 15
|
minSdkVersion 16
|
||||||
targetSdkVersion 28
|
targetSdkVersion 28
|
||||||
versionCode 1500160
|
versionCode 1600160
|
||||||
versionName "1.6.0"
|
versionName "1.6.0"
|
||||||
// Supported languages
|
// Supported languages
|
||||||
resConfigs "en"
|
resConfigs "en"
|
||||||
|
@ -25,6 +25,8 @@ android {
|
||||||
}
|
}
|
||||||
|
|
||||||
dependencies {
|
dependencies {
|
||||||
|
def room_version = "2.1.0-alpha03"
|
||||||
|
|
||||||
implementation 'androidx.appcompat:appcompat:1.0.2'
|
implementation 'androidx.appcompat:appcompat:1.0.2'
|
||||||
implementation 'com.google.android.material:material:1.0.0'
|
implementation 'com.google.android.material:material:1.0.0'
|
||||||
implementation 'androidx.recyclerview:recyclerview:1.0.0'
|
implementation 'androidx.recyclerview:recyclerview:1.0.0'
|
||||||
|
@ -36,5 +38,8 @@ dependencies {
|
||||||
}
|
}
|
||||||
implementation 'androidx.browser:browser:1.0.0'
|
implementation 'androidx.browser:browser:1.0.0'
|
||||||
implementation 'androidx.lifecycle:lifecycle-extensions:2.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'
|
implementation 'com.github.chrisbanes:PhotoView:2.3.0'
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,10 +1,8 @@
|
||||||
package be.digitalia.fosdem;
|
package be.digitalia.fosdem;
|
||||||
|
|
||||||
import android.app.Application;
|
import android.app.Application;
|
||||||
|
|
||||||
import androidx.preference.PreferenceManager;
|
import androidx.preference.PreferenceManager;
|
||||||
import be.digitalia.fosdem.alarms.FosdemAlarmManager;
|
import be.digitalia.fosdem.alarms.FosdemAlarmManager;
|
||||||
import be.digitalia.fosdem.db.DatabaseManager;
|
|
||||||
|
|
||||||
public class FosdemApplication extends Application {
|
public class FosdemApplication extends Application {
|
||||||
|
|
||||||
|
@ -12,7 +10,6 @@ public class FosdemApplication extends Application {
|
||||||
public void onCreate() {
|
public void onCreate() {
|
||||||
super.onCreate();
|
super.onCreate();
|
||||||
|
|
||||||
DatabaseManager.init(this);
|
|
||||||
// Initialize settings
|
// Initialize settings
|
||||||
PreferenceManager.setDefaultValues(this, R.xml.settings, false);
|
PreferenceManager.setDefaultValues(this, R.xml.settings, false);
|
||||||
// Alarms (requires settings)
|
// Alarms (requires settings)
|
||||||
|
|
|
@ -36,12 +36,11 @@ import androidx.fragment.app.Fragment;
|
||||||
import androidx.fragment.app.FragmentManager;
|
import androidx.fragment.app.FragmentManager;
|
||||||
import androidx.fragment.app.FragmentTransaction;
|
import androidx.fragment.app.FragmentTransaction;
|
||||||
import androidx.lifecycle.Observer;
|
import androidx.lifecycle.Observer;
|
||||||
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
|
|
||||||
import be.digitalia.fosdem.BuildConfig;
|
import be.digitalia.fosdem.BuildConfig;
|
||||||
import be.digitalia.fosdem.R;
|
import be.digitalia.fosdem.R;
|
||||||
import be.digitalia.fosdem.api.FosdemApi;
|
import be.digitalia.fosdem.api.FosdemApi;
|
||||||
import be.digitalia.fosdem.api.FosdemUrls;
|
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.fragments.*;
|
||||||
import be.digitalia.fosdem.livedata.SingleEvent;
|
import be.digitalia.fosdem.livedata.SingleEvent;
|
||||||
import be.digitalia.fosdem.model.DownloadScheduleResult;
|
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 {
|
public static class DownloadScheduleReminderDialogFragment extends DialogFragment {
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
|
@ -213,6 +204,7 @@ public class MainActivity extends AppCompatActivity {
|
||||||
progressBar.setProgress(100);
|
progressBar.setProgress(100);
|
||||||
progressBar.animate()
|
progressBar.animate()
|
||||||
.alpha(0f)
|
.alpha(0f)
|
||||||
|
.withLayer()
|
||||||
.setListener(new AnimatorListenerAdapter() {
|
.setListener(new AnimatorListenerAdapter() {
|
||||||
@Override
|
@Override
|
||||||
public void onAnimationEnd(Animator animation) {
|
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
|
// Last update date, below the list
|
||||||
lastUpdateTextView = mainMenu.findViewById(R.id.last_update);
|
lastUpdateTextView = mainMenu.findViewById(R.id.last_update);
|
||||||
updateLastUpdateTime();
|
AppDatabase.getInstance(this).getScheduleDao().getLastUpdateTime()
|
||||||
|
.observe(this, lastUpdateTimeObserver);
|
||||||
|
|
||||||
if (savedInstanceState == null) {
|
if (savedInstanceState == null) {
|
||||||
// Select initial section
|
// Select initial section
|
||||||
|
@ -319,13 +310,15 @@ public class MainActivity extends AppCompatActivity {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void updateLastUpdateTime() {
|
private final Observer<Long> lastUpdateTimeObserver = new Observer<Long>() {
|
||||||
long lastUpdateTime = DatabaseManager.getInstance().getLastUpdateTime();
|
@Override
|
||||||
lastUpdateTextView.setText(getString(R.string.last_update,
|
public void onChanged(Long lastUpdateTime) {
|
||||||
(lastUpdateTime == -1L)
|
lastUpdateTextView.setText(getString(R.string.last_update,
|
||||||
? getString(R.string.never)
|
(lastUpdateTime == -1L)
|
||||||
: android.text.format.DateFormat.format(LAST_UPDATE_DATE_FORMAT, lastUpdateTime)));
|
? getString(R.string.never)
|
||||||
}
|
: android.text.format.DateFormat.format(LAST_UPDATE_DATE_FORMAT, lastUpdateTime)));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onPostCreate(Bundle savedInstanceState) {
|
protected void onPostCreate(Bundle savedInstanceState) {
|
||||||
|
@ -371,7 +364,8 @@ public class MainActivity extends AppCompatActivity {
|
||||||
|
|
||||||
// Download reminder
|
// Download reminder
|
||||||
long now = System.currentTimeMillis();
|
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))) {
|
if ((time == -1L) || (time < (now - DATABASE_VALIDITY_DURATION))) {
|
||||||
SharedPreferences prefs = getPreferences(Context.MODE_PRIVATE);
|
SharedPreferences prefs = getPreferences(Context.MODE_PRIVATE);
|
||||||
time = prefs.getLong(PREF_LAST_DOWNLOAD_REMINDER_TIME, -1L);
|
time = prefs.getLong(PREF_LAST_DOWNLOAD_REMINDER_TIME, -1L);
|
||||||
|
@ -397,12 +391,6 @@ public class MainActivity extends AppCompatActivity {
|
||||||
super.onStop();
|
super.onStop();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
protected void onDestroy() {
|
|
||||||
super.onDestroy();
|
|
||||||
LocalBroadcastManager.getInstance(this).unregisterReceiver(scheduleRefreshedReceiver);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean onCreateOptionsMenu(Menu menu) {
|
public boolean onCreateOptionsMenu(Menu menu) {
|
||||||
getMenuInflater().inflate(R.menu.main, menu);
|
getMenuInflater().inflate(R.menu.main, menu);
|
||||||
|
|
|
@ -77,7 +77,7 @@ public class RoomImageDialogActivity extends AppCompatActivity {
|
||||||
});
|
});
|
||||||
|
|
||||||
// Display the room status as subtitle
|
// 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
|
@Override
|
||||||
public void onChanged(Map<String, RoomStatus> roomStatuses) {
|
public void onChanged(Map<String, RoomStatus> roomStatuses) {
|
||||||
RoomStatus roomStatus = roomStatuses.get(roomName);
|
RoomStatus roomStatus = roomStatuses.get(roomName);
|
||||||
|
|
|
@ -13,22 +13,19 @@ import android.view.MenuItem;
|
||||||
import androidx.appcompat.app.AppCompatActivity;
|
import androidx.appcompat.app.AppCompatActivity;
|
||||||
import androidx.appcompat.widget.SearchView;
|
import androidx.appcompat.widget.SearchView;
|
||||||
import androidx.core.content.ContextCompat;
|
import androidx.core.content.ContextCompat;
|
||||||
import androidx.core.util.ObjectsCompat;
|
import androidx.lifecycle.ViewModelProviders;
|
||||||
import androidx.fragment.app.Fragment;
|
|
||||||
import androidx.fragment.app.FragmentManager;
|
|
||||||
import be.digitalia.fosdem.R;
|
import be.digitalia.fosdem.R;
|
||||||
import be.digitalia.fosdem.fragments.SearchResultListFragment;
|
import be.digitalia.fosdem.fragments.SearchResultListFragment;
|
||||||
|
import be.digitalia.fosdem.viewmodels.SearchViewModel;
|
||||||
import com.google.android.material.snackbar.Snackbar;
|
import com.google.android.material.snackbar.Snackbar;
|
||||||
|
|
||||||
public class SearchResultActivity extends AppCompatActivity {
|
public class SearchResultActivity extends AppCompatActivity {
|
||||||
|
|
||||||
public static final int MIN_SEARCH_LENGTH = 3;
|
|
||||||
|
|
||||||
private static final String STATE_CURRENT_QUERY = "current_query";
|
private static final String STATE_CURRENT_QUERY = "current_query";
|
||||||
// Search Intent sent by Google Now
|
// Search Intent sent by Google Now
|
||||||
private static final String GMS_ACTION_SEARCH = "com.google.android.gms.actions.SEARCH_ACTION";
|
private static final String GMS_ACTION_SEARCH = "com.google.android.gms.actions.SEARCH_ACTION";
|
||||||
|
|
||||||
private String currentQuery;
|
private SearchViewModel viewModel;
|
||||||
private SearchView searchView;
|
private SearchView searchView;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -38,17 +35,22 @@ public class SearchResultActivity extends AppCompatActivity {
|
||||||
|
|
||||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||||
|
|
||||||
|
viewModel = ViewModelProviders.of(this).get(SearchViewModel.class);
|
||||||
|
|
||||||
if (savedInstanceState == null) {
|
if (savedInstanceState == null) {
|
||||||
|
SearchResultListFragment f = SearchResultListFragment.newInstance();
|
||||||
|
getSupportFragmentManager().beginTransaction().replace(R.id.content, f).commit();
|
||||||
|
|
||||||
handleIntent(getIntent(), false);
|
handleIntent(getIntent(), false);
|
||||||
} else {
|
} else {
|
||||||
currentQuery = savedInstanceState.getString(STATE_CURRENT_QUERY);
|
viewModel.setQuery(savedInstanceState.getString(STATE_CURRENT_QUERY, ""));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void onSaveInstanceState(Bundle outState) {
|
protected void onSaveInstanceState(Bundle outState) {
|
||||||
super.onSaveInstanceState(outState);
|
super.onSaveInstanceState(outState);
|
||||||
outState.putString(STATE_CURRENT_QUERY, currentQuery);
|
outState.putString(STATE_CURRENT_QUERY, viewModel.getQuery());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -62,29 +64,18 @@ public class SearchResultActivity extends AppCompatActivity {
|
||||||
if (Intent.ACTION_SEARCH.equals(intentAction) || GMS_ACTION_SEARCH.equals(intentAction)) {
|
if (Intent.ACTION_SEARCH.equals(intentAction) || GMS_ACTION_SEARCH.equals(intentAction)) {
|
||||||
// Normal search, results are displayed here
|
// Normal search, results are displayed here
|
||||||
String query = intent.getStringExtra(SearchManager.QUERY);
|
String query = intent.getStringExtra(SearchManager.QUERY);
|
||||||
if (query != null) {
|
if (query == null) {
|
||||||
|
query = "";
|
||||||
|
} else {
|
||||||
query = query.trim();
|
query = query.trim();
|
||||||
}
|
}
|
||||||
if (searchView != null) {
|
if (searchView != null) {
|
||||||
setSearchViewQuery(query);
|
setSearchViewQuery(query);
|
||||||
}
|
}
|
||||||
final boolean isQueryTooShort = (query == null) || (query.length() < MIN_SEARCH_LENGTH);
|
|
||||||
|
|
||||||
if (!ObjectsCompat.equals(currentQuery, query)) {
|
viewModel.setQuery(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();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isQueryTooShort) {
|
if (SearchViewModel.isQueryTooShort(query)) {
|
||||||
SpannableString errorMessage = new SpannableString(getString(R.string.search_length_error));
|
SpannableString errorMessage = new SpannableString(getString(R.string.search_length_error));
|
||||||
int textColor = ContextCompat.getColor(this, R.color.error_material);
|
int textColor = ContextCompat.getColor(this, R.color.error_material);
|
||||||
errorMessage.setSpan(new ForegroundColorSpan(textColor), 0, errorMessage.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
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 = (SearchView) searchMenuItem.getActionView();
|
||||||
searchView.setSearchableInfo(searchManager.getSearchableInfo(getComponentName()));
|
searchView.setSearchableInfo(searchManager.getSearchableInfo(getComponentName()));
|
||||||
searchView.setIconifiedByDefault(false); // Always show the search view
|
searchView.setIconifiedByDefault(false); // Always show the search view
|
||||||
setSearchViewQuery(currentQuery);
|
setSearchViewQuery(viewModel.getQuery());
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,47 +1,42 @@
|
||||||
package be.digitalia.fosdem.activities;
|
package be.digitalia.fosdem.activities;
|
||||||
|
|
||||||
import android.database.Cursor;
|
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.provider.BaseColumns;
|
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import androidx.annotation.NonNull;
|
|
||||||
import androidx.appcompat.app.ActionBar;
|
import androidx.appcompat.app.ActionBar;
|
||||||
import androidx.appcompat.app.AppCompatActivity;
|
import androidx.appcompat.app.AppCompatActivity;
|
||||||
import androidx.core.content.ContextCompat;
|
import androidx.core.content.ContextCompat;
|
||||||
import androidx.fragment.app.Fragment;
|
import androidx.fragment.app.Fragment;
|
||||||
import androidx.fragment.app.FragmentManager;
|
import androidx.fragment.app.FragmentManager;
|
||||||
import androidx.fragment.app.FragmentStatePagerAdapter;
|
import androidx.fragment.app.FragmentStatePagerAdapter;
|
||||||
import androidx.loader.app.LoaderManager;
|
import androidx.lifecycle.Observer;
|
||||||
import androidx.loader.app.LoaderManager.LoaderCallbacks;
|
import androidx.lifecycle.ViewModelProviders;
|
||||||
import androidx.loader.content.Loader;
|
|
||||||
import androidx.viewpager.widget.ViewPager;
|
import androidx.viewpager.widget.ViewPager;
|
||||||
import be.digitalia.fosdem.R;
|
import be.digitalia.fosdem.R;
|
||||||
import be.digitalia.fosdem.db.DatabaseManager;
|
|
||||||
import be.digitalia.fosdem.fragments.EventDetailsFragment;
|
import be.digitalia.fosdem.fragments.EventDetailsFragment;
|
||||||
import be.digitalia.fosdem.loaders.TrackScheduleLoader;
|
|
||||||
import be.digitalia.fosdem.model.Day;
|
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.model.Track;
|
||||||
import be.digitalia.fosdem.utils.NfcUtils;
|
import be.digitalia.fosdem.utils.NfcUtils;
|
||||||
import be.digitalia.fosdem.utils.NfcUtils.CreateNfcAppDataCallback;
|
import be.digitalia.fosdem.utils.NfcUtils.CreateNfcAppDataCallback;
|
||||||
import be.digitalia.fosdem.utils.ThemeUtils;
|
import be.digitalia.fosdem.utils.ThemeUtils;
|
||||||
|
import be.digitalia.fosdem.viewmodels.TrackScheduleViewModel;
|
||||||
import be.digitalia.fosdem.widgets.ContentLoadingProgressBar;
|
import be.digitalia.fosdem.widgets.ContentLoadingProgressBar;
|
||||||
import com.viewpagerindicator.UnderlinePageIndicator;
|
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.
|
* Event view of the track schedule; allows to slide between events of the same track using a ViewPager.
|
||||||
*
|
*
|
||||||
* @author Christophe Beyls
|
* @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_DAY = "day";
|
||||||
public static final String EXTRA_TRACK = "track";
|
public static final String EXTRA_TRACK = "track";
|
||||||
public static final String EXTRA_POSITION = "position";
|
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 int initialPosition = -1;
|
||||||
private ContentLoadingProgressBar progress;
|
private ContentLoadingProgressBar progress;
|
||||||
private ViewPager pager;
|
private ViewPager pager;
|
||||||
|
@ -55,8 +50,8 @@ public class TrackScheduleEventActivity extends AppCompatActivity implements Loa
|
||||||
setContentView(R.layout.track_schedule_event);
|
setContentView(R.layout.track_schedule_event);
|
||||||
|
|
||||||
Bundle extras = getIntent().getExtras();
|
Bundle extras = getIntent().getExtras();
|
||||||
day = extras.getParcelable(EXTRA_DAY);
|
final Day day = extras.getParcelable(EXTRA_DAY);
|
||||||
track = extras.getParcelable(EXTRA_TRACK);
|
final Track track = extras.getParcelable(EXTRA_TRACK);
|
||||||
|
|
||||||
progress = findViewById(R.id.progress);
|
progress = findViewById(R.id.progress);
|
||||||
pager = findViewById(R.id.pager);
|
pager = findViewById(R.id.pager);
|
||||||
|
@ -78,7 +73,9 @@ public class TrackScheduleEventActivity extends AppCompatActivity implements Loa
|
||||||
NfcUtils.setAppDataPushMessageCallbackIfAvailable(this, this);
|
NfcUtils.setAppDataPushMessageCallbackIfAvailable(this, this);
|
||||||
|
|
||||||
setCustomProgressVisibility(true);
|
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) {
|
private void setCustomProgressVisibility(boolean isVisible) {
|
||||||
|
@ -94,11 +91,11 @@ public class TrackScheduleEventActivity extends AppCompatActivity implements Loa
|
||||||
if (adapter.getCount() == 0) {
|
if (adapter.getCount() == 0) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
long eventId = adapter.getItemId(pager.getCurrentItem());
|
Event event = adapter.getEvent(pager.getCurrentItem());
|
||||||
if (eventId == -1L) {
|
if (event == null) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
return String.valueOf(eventId).getBytes();
|
return String.valueOf(event.getId()).getBytes();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -107,19 +104,13 @@ public class TrackScheduleEventActivity extends AppCompatActivity implements Loa
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@NonNull
|
|
||||||
@Override
|
@Override
|
||||||
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
|
public void onChanged(List<StatusEvent> schedule) {
|
||||||
return new TrackScheduleLoader(this, day, track);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onLoadFinished(@NonNull Loader<Cursor> loader, Cursor data) {
|
|
||||||
setCustomProgressVisibility(false);
|
setCustomProgressVisibility(false);
|
||||||
|
|
||||||
if (data != null) {
|
if (schedule != null) {
|
||||||
pager.setVisibility(View.VISIBLE);
|
pager.setVisibility(View.VISIBLE);
|
||||||
adapter.setCursor(data);
|
adapter.setSchedule(schedule);
|
||||||
|
|
||||||
// Delay setting the adapter
|
// Delay setting the adapter
|
||||||
// to ensure the current position is restored properly
|
// 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 {
|
public static class TrackScheduleEventAdapter extends FragmentStatePagerAdapter {
|
||||||
|
|
||||||
private Cursor cursor;
|
private List<StatusEvent> events = null;
|
||||||
|
|
||||||
public TrackScheduleEventAdapter(FragmentManager fm) {
|
public TrackScheduleEventAdapter(FragmentManager fm) {
|
||||||
super(fm);
|
super(fm);
|
||||||
}
|
}
|
||||||
|
|
||||||
public Cursor getCursor() {
|
public void setSchedule(List<StatusEvent> schedule) {
|
||||||
return cursor;
|
this.events = schedule;
|
||||||
}
|
|
||||||
|
|
||||||
public void setCursor(Cursor cursor) {
|
|
||||||
this.cursor = cursor;
|
|
||||||
notifyDataSetChanged();
|
notifyDataSetChanged();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int getCount() {
|
public int getCount() {
|
||||||
return (cursor == null) ? 0 : cursor.getCount();
|
return (events == null) ? 0 : events.size();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Fragment getItem(int position) {
|
public Fragment getItem(int position) {
|
||||||
cursor.moveToPosition(position);
|
return EventDetailsFragment.newInstance(events.get(position).getEvent());
|
||||||
return EventDetailsFragment.newInstance(DatabaseManager.toEvent(cursor));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public long getItemId(int position) {
|
public Event getEvent(int position) {
|
||||||
if (!cursor.moveToPosition(position)) {
|
if (position < 0 || position >= getCount()) {
|
||||||
return -1L;
|
return null;
|
||||||
}
|
}
|
||||||
return cursor.getLong(cursor.getColumnIndexOrThrow(BaseColumns._ID));
|
return events.get(position).getEvent();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,187 +1,314 @@
|
||||||
package be.digitalia.fosdem.adapters;
|
package be.digitalia.fosdem.adapters;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.database.Cursor;
|
import android.content.Intent;
|
||||||
import android.graphics.Typeface;
|
import android.graphics.Typeface;
|
||||||
import android.os.AsyncTask;
|
|
||||||
import android.os.Parcelable;
|
|
||||||
import android.text.SpannableString;
|
import android.text.SpannableString;
|
||||||
import android.text.Spanned;
|
import android.text.Spanned;
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
import android.text.style.ForegroundColorSpan;
|
import android.text.style.ForegroundColorSpan;
|
||||||
import android.text.style.StyleSpan;
|
import android.text.style.StyleSpan;
|
||||||
import android.view.Menu;
|
import android.view.LayoutInflater;
|
||||||
import android.view.MenuItem;
|
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
|
import android.view.ViewGroup;
|
||||||
import java.util.Date;
|
import android.widget.TextView;
|
||||||
|
|
||||||
import androidx.annotation.ColorInt;
|
import androidx.annotation.ColorInt;
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
import androidx.appcompat.app.AppCompatActivity;
|
import androidx.appcompat.app.AppCompatActivity;
|
||||||
import androidx.appcompat.view.ActionMode;
|
import androidx.collection.SimpleArrayMap;
|
||||||
import androidx.core.content.ContextCompat;
|
import androidx.core.content.ContextCompat;
|
||||||
|
import androidx.core.util.ObjectsCompat;
|
||||||
import androidx.lifecycle.LifecycleOwner;
|
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.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.Event;
|
||||||
import be.digitalia.fosdem.model.RoomStatus;
|
import be.digitalia.fosdem.model.RoomStatus;
|
||||||
import be.digitalia.fosdem.model.Track;
|
import be.digitalia.fosdem.model.Track;
|
||||||
|
import be.digitalia.fosdem.utils.DateUtils;
|
||||||
import be.digitalia.fosdem.widgets.MultiChoiceHelper;
|
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
|
@ColorInt
|
||||||
private final int errorColor;
|
private final int errorColor;
|
||||||
|
private final SimpleArrayMap<RecyclerView.AdapterDataObserver, BookmarksDataObserverWrapper> observers = new SimpleArrayMap<>();
|
||||||
final MultiChoiceHelper multiChoiceHelper;
|
final MultiChoiceHelper multiChoiceHelper;
|
||||||
|
private Map<String, RoomStatus> roomStatuses;
|
||||||
|
|
||||||
public BookmarksAdapter(AppCompatActivity activity, LifecycleOwner owner) {
|
public BookmarksAdapter(@NonNull AppCompatActivity activity, @NonNull LifecycleOwner owner,
|
||||||
super(activity, owner);
|
@NonNull MultiChoiceHelper.MultiChoiceModeListener multiChoiceModeListener) {
|
||||||
|
super(DIFF_CALLBACK);
|
||||||
|
setHasStableIds(true);
|
||||||
|
timeDateFormat = DateUtils.getTimeDateFormat(activity);
|
||||||
errorColor = ContextCompat.getColor(activity, R.color.error_material);
|
errorColor = ContextCompat.getColor(activity, R.color.error_material);
|
||||||
|
|
||||||
multiChoiceHelper = new MultiChoiceHelper(activity, this);
|
multiChoiceHelper = new MultiChoiceHelper(activity, this);
|
||||||
multiChoiceHelper.setMultiChoiceModeListener(new MultiChoiceHelper.MultiChoiceModeListener() {
|
multiChoiceHelper.setMultiChoiceModeListener(multiChoiceModeListener);
|
||||||
|
|
||||||
@Override
|
FosdemApi.getRoomStatuses(activity).observe(owner, this);
|
||||||
public boolean onCreateActionMode(ActionMode mode, Menu menu) {
|
|
||||||
mode.getMenuInflater().inflate(R.menu.action_mode_bookmarks, menu);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void updateSelectedCountDisplay(ActionMode mode) {
|
|
||||||
int count = multiChoiceHelper.getCheckedItemCount();
|
|
||||||
mode.setTitle(multiChoiceHelper.getContext().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
|
|
||||||
new RemoveBookmarksAsyncTask().execute(multiChoiceHelper.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) {
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public Parcelable onSaveInstanceState() {
|
@NonNull
|
||||||
return multiChoiceHelper.onSaveInstanceState();
|
public MultiChoiceHelper getMultiChoiceHelper() {
|
||||||
}
|
return multiChoiceHelper;
|
||||||
|
|
||||||
public void onRestoreInstanceState(Parcelable state) {
|
|
||||||
multiChoiceHelper.onRestoreInstanceState(state);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void onDestroyView() {
|
|
||||||
multiChoiceHelper.clearChoices();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onBindViewHolder(ViewHolder holder, Cursor cursor) {
|
public void onChanged(@Nullable Map<String, RoomStatus> roomStatuses) {
|
||||||
final int position = cursor.getPosition();
|
this.roomStatuses = roomStatuses;
|
||||||
Context context = holder.itemView.getContext();
|
notifyItemRangeChanged(0, getItemCount(), DETAILS_PAYLOAD);
|
||||||
Event event = DatabaseManager.toEvent(cursor, holder.event);
|
|
||||||
holder.event = event;
|
|
||||||
holder.isOverlapping = isOverlapping(cursor, event.getStartTime(), event.getEndTime());
|
|
||||||
|
|
||||||
holder.title.setText(event.getTitle());
|
|
||||||
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);
|
|
||||||
|
|
||||||
// Enable MultiChoice selection and update checked state
|
|
||||||
holder.bind(multiChoiceHelper, position);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void bindDetails(ViewHolder holder, Event event) {
|
public long getItemId(int position) {
|
||||||
Context context = holder.details.getContext();
|
return getItem(position).getId();
|
||||||
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;
|
|
||||||
|
|
||||||
// Highlight the date and time with error color in case of conflicting schedules
|
@NonNull
|
||||||
if (holder.isOverlapping) {
|
@Override
|
||||||
int endPosition = details.indexOf(" | ");
|
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||||
detailsSpannable.setSpan(new ForegroundColorSpan(errorColor), 0, endPosition, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_event, parent, false);
|
||||||
detailsSpannable.setSpan(new StyleSpan(Typeface.BOLD), 0, endPosition, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
return new ViewHolder(view, multiChoiceHelper, timeDateFormat, errorColor);
|
||||||
holder.details.setText(detailsSpannable);
|
}
|
||||||
detailsContentDescription = context.getString(R.string.bookmark_conflict_content_description, detailsContentDescription);
|
|
||||||
|
private RoomStatus getRoomStatus(Event event) {
|
||||||
|
return (roomStatuses == null) ? null : roomStatuses.get(event.getRoomName());
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
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 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();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (roomStatuses != null) {
|
}
|
||||||
RoomStatus roomStatus = roomStatuses.get(roomName);
|
|
||||||
|
@Override
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@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();
|
||||||
|
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, @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 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 (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);
|
||||||
|
details.setText(detailsSpannable);
|
||||||
|
detailsDescription = context.getString(R.string.bookmark_conflict_content_description, detailsDescription);
|
||||||
|
}
|
||||||
if (roomStatus != null) {
|
if (roomStatus != null) {
|
||||||
int color = ContextCompat.getColor(context, roomStatus.getColorResId());
|
int color = ContextCompat.getColor(context, roomStatus.getColorResId());
|
||||||
detailsSpannable.setSpan(new ForegroundColorSpan(color),
|
detailsSpannable.setSpan(new ForegroundColorSpan(color),
|
||||||
details.length() - roomName.length(),
|
detailsText.length() - roomName.length(),
|
||||||
details.length(),
|
detailsText.length(),
|
||||||
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||||
}
|
}
|
||||||
|
details.setText(detailsSpannable);
|
||||||
|
details.setContentDescription(context.getString(R.string.details_content_description, detailsDescription));
|
||||||
}
|
}
|
||||||
holder.details.setText(detailsSpannable);
|
|
||||||
holder.details.setContentDescription(context.getString(R.string.details_content_description, detailsContentDescription));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks if the current event is overlapping with the previous or next one.
|
* 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(@NonNull Event event, @Nullable Event previous, @Nullable Event next) {
|
||||||
private static boolean isOverlapping(Cursor cursor, Date startTime, Date endTime) {
|
final Date startTime = event.getStartTime();
|
||||||
final int position = cursor.getPosition();
|
final Date previousEndTime = (previous == null) ? null : previous.getEndTime();
|
||||||
|
if (startTime != null && previousEndTime != null && previousEndTime.getTime() > startTime.getTime()) {
|
||||||
if ((startTime != null) && (position > 0) && cursor.moveToPosition(position - 1)) {
|
|
||||||
long previousEndTime = DatabaseManager.toEventEndTimeMillis(cursor);
|
|
||||||
if ((previousEndTime != -1L) && (previousEndTime > startTime.getTime())) {
|
|
||||||
// The event overlaps with the previous one
|
// The event overlaps with the previous one
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
final Date endTime = event.getEndTime();
|
||||||
|
final Date nextStartTime = (next == null) ? null : next.getStartTime();
|
||||||
|
// The event overlaps with the next one
|
||||||
|
return endTime != null && nextStartTime != null && nextStartTime.getTime() < endTime.getTime();
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((endTime != null) && (position < (cursor.getCount() - 1)) && cursor.moveToPosition(position + 1)) {
|
@Override
|
||||||
long nextStartTime = DatabaseManager.toEventStartTimeMillis(cursor);
|
public void onClick(View view) {
|
||||||
if ((nextStartTime != -1L) && (nextStartTime < endTime.getTime())) {
|
if (event != null) {
|
||||||
// The event overlaps with the next one
|
Context context = view.getContext();
|
||||||
return true;
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
private void updateNext(int position) {
|
||||||
}
|
if (position < adapter.getItemCount()) {
|
||||||
|
observer.onItemRangeChanged(position, 1, DETAILS_PAYLOAD);
|
||||||
static class RemoveBookmarksAsyncTask extends AsyncTask<long[], Void, Void> {
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
protected Void doInBackground(long[]... params) {
|
|
||||||
DatabaseManager.getInstance().removeBookmarks(params[0]);
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@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.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.database.Cursor;
|
|
||||||
import android.graphics.drawable.Drawable;
|
import android.graphics.drawable.Drawable;
|
||||||
import android.text.SpannableString;
|
import android.text.SpannableString;
|
||||||
import android.text.Spanned;
|
import android.text.Spanned;
|
||||||
|
@ -12,49 +11,65 @@ import android.view.LayoutInflater;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
import android.widget.TextView;
|
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.text.DateFormat;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
public class EventsAdapter extends PagedListAdapter<StatusEvent, EventsAdapter.ViewHolder>
|
||||||
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>
|
|
||||||
implements Observer<Map<String, RoomStatus>> {
|
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();
|
private static final Object DETAILS_PAYLOAD = new Object();
|
||||||
|
|
||||||
protected final LayoutInflater inflater;
|
private final DateFormat timeDateFormat;
|
||||||
protected final DateFormat timeDateFormat;
|
|
||||||
private final boolean showDay;
|
private final boolean showDay;
|
||||||
protected Map<String, RoomStatus> roomStatuses;
|
private Map<String, RoomStatus> roomStatuses;
|
||||||
|
|
||||||
public EventsAdapter(Context context, LifecycleOwner owner) {
|
public EventsAdapter(Context context, LifecycleOwner owner) {
|
||||||
this(context, owner, true);
|
this(context, owner, true);
|
||||||
}
|
}
|
||||||
|
|
||||||
public EventsAdapter(Context context, LifecycleOwner owner, boolean showDay) {
|
public EventsAdapter(Context context, LifecycleOwner owner, boolean showDay) {
|
||||||
inflater = LayoutInflater.from(context);
|
super(DIFF_CALLBACK);
|
||||||
timeDateFormat = DateUtils.getTimeDateFormat(context);
|
timeDateFormat = DateUtils.getTimeDateFormat(context);
|
||||||
this.showDay = showDay;
|
this.showDay = showDay;
|
||||||
|
|
||||||
FosdemApi.getRoomStatuses().observe(owner, this);
|
FosdemApi.getRoomStatuses(context).observe(owner, this);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -63,11 +78,6 @@ public class EventsAdapter extends RecyclerViewCursorAdapter<EventsAdapter.ViewH
|
||||||
notifyItemRangeChanged(0, getItemCount(), DETAILS_PAYLOAD);
|
notifyItemRangeChanged(0, getItemCount(), DETAILS_PAYLOAD);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public Event getItem(int position) {
|
|
||||||
return DatabaseManager.toEvent((Cursor) super.getItem(position));
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int getItemViewType(int position) {
|
public int getItemViewType(int position) {
|
||||||
return R.layout.item_event;
|
return R.layout.item_event;
|
||||||
|
@ -76,67 +86,24 @@ public class EventsAdapter extends RecyclerViewCursorAdapter<EventsAdapter.ViewH
|
||||||
@NonNull
|
@NonNull
|
||||||
@Override
|
@Override
|
||||||
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||||
View view = inflater.inflate(R.layout.item_event, parent, false);
|
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_event, parent, false);
|
||||||
return new ViewHolder(view);
|
return new ViewHolder(view, timeDateFormat);
|
||||||
|
}
|
||||||
|
|
||||||
|
private RoomStatus getRoomStatus(Event event) {
|
||||||
|
return (roomStatuses == null) ? null : roomStatuses.get(event.getRoomName());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onBindViewHolder(ViewHolder holder, Cursor cursor) {
|
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
|
||||||
Context context = holder.itemView.getContext();
|
final StatusEvent statusEvent = getItem(position);
|
||||||
Event event = DatabaseManager.toEvent(cursor, holder.event);
|
if (statusEvent == null) {
|
||||||
holder.event = event;
|
holder.clear();
|
||||||
|
|
||||||
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);
|
|
||||||
} else {
|
} 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
|
@Override
|
||||||
|
@ -144,36 +111,104 @@ public class EventsAdapter extends RecyclerViewCursorAdapter<EventsAdapter.ViewH
|
||||||
if (payloads.isEmpty()) {
|
if (payloads.isEmpty()) {
|
||||||
onBindViewHolder(holder, position);
|
onBindViewHolder(holder, position);
|
||||||
} else {
|
} else {
|
||||||
if (payloads.contains(DETAILS_PAYLOAD) && (holder.event != null)) {
|
final StatusEvent statusEvent = getItem(position);
|
||||||
bindDetails(holder, holder.event);
|
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 {
|
static class ViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener {
|
||||||
TextView title;
|
final TextView title;
|
||||||
TextView persons;
|
final TextView persons;
|
||||||
TextView trackName;
|
final TextView trackName;
|
||||||
TextView details;
|
final TextView details;
|
||||||
|
|
||||||
|
private final DateFormat timeDateFormat;
|
||||||
|
|
||||||
Event event;
|
Event event;
|
||||||
boolean isOverlapping;
|
|
||||||
|
|
||||||
public ViewHolder(View itemView) {
|
ViewHolder(View itemView, @NonNull DateFormat timeDateFormat) {
|
||||||
super(itemView);
|
super(itemView);
|
||||||
title = itemView.findViewById(R.id.title);
|
title = itemView.findViewById(R.id.title);
|
||||||
persons = itemView.findViewById(R.id.persons);
|
persons = itemView.findViewById(R.id.persons);
|
||||||
trackName = itemView.findViewById(R.id.track_name);
|
trackName = itemView.findViewById(R.id.track_name);
|
||||||
details = itemView.findViewById(R.id.details);
|
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
|
@Override
|
||||||
public void onClick(View view) {
|
public void onClick(View view) {
|
||||||
Context context = view.getContext();
|
if (event != null) {
|
||||||
Intent intent = new Intent(context, EventDetailsActivity.class)
|
Context context = view.getContext();
|
||||||
.putExtra(EventDetailsActivity.EXTRA_EVENT, event);
|
Intent intent = new Intent(context, EventDetailsActivity.class)
|
||||||
context.startActivity(intent);
|
.putExtra(EventDetailsActivity.EXTRA_EVENT, event);
|
||||||
|
context.startActivity(intent);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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.Context;
|
||||||
import android.content.res.TypedArray;
|
import android.content.res.TypedArray;
|
||||||
import android.database.Cursor;
|
|
||||||
import android.graphics.drawable.Drawable;
|
import android.graphics.drawable.Drawable;
|
||||||
import android.graphics.drawable.LayerDrawable;
|
import android.graphics.drawable.LayerDrawable;
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
|
@ -10,46 +9,60 @@ import android.view.LayoutInflater;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
|
|
||||||
import java.text.DateFormat;
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import androidx.annotation.DrawableRes;
|
import androidx.annotation.DrawableRes;
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.appcompat.content.res.AppCompatResources;
|
import androidx.appcompat.content.res.AppCompatResources;
|
||||||
import androidx.core.content.ContextCompat;
|
import androidx.core.content.ContextCompat;
|
||||||
|
import androidx.core.util.ObjectsCompat;
|
||||||
import androidx.core.view.ViewCompat;
|
import androidx.core.view.ViewCompat;
|
||||||
import androidx.core.widget.TextViewCompat;
|
import androidx.core.widget.TextViewCompat;
|
||||||
|
import androidx.recyclerview.widget.DiffUtil;
|
||||||
|
import androidx.recyclerview.widget.ListAdapter;
|
||||||
import androidx.recyclerview.widget.RecyclerView;
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
import be.digitalia.fosdem.R;
|
import be.digitalia.fosdem.R;
|
||||||
import be.digitalia.fosdem.db.DatabaseManager;
|
|
||||||
import be.digitalia.fosdem.model.Event;
|
import be.digitalia.fosdem.model.Event;
|
||||||
|
import be.digitalia.fosdem.model.StatusEvent;
|
||||||
import be.digitalia.fosdem.utils.DateUtils;
|
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 {
|
public interface EventClickListener {
|
||||||
void onEventClick(int position, Event event);
|
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 TIME_COLORS_PAYLOAD = new Object();
|
||||||
private static final Object SELECTION_PAYLOAD = new Object();
|
private static final Object SELECTION_PAYLOAD = new Object();
|
||||||
|
|
||||||
private final LayoutInflater inflater;
|
final DateFormat timeDateFormat;
|
||||||
private final DateFormat timeDateFormat;
|
final int timeBackgroundColor;
|
||||||
private final int timeBackgroundColor;
|
final int timeForegroundColor;
|
||||||
private final int timeForegroundColor;
|
final int timeRunningBackgroundColor;
|
||||||
private final int timeRunningBackgroundColor;
|
final int timeRunningForegroundColor;
|
||||||
private final int timeRunningForegroundColor;
|
|
||||||
@Nullable
|
@Nullable
|
||||||
private final EventClickListener listener;
|
final EventClickListener listener;
|
||||||
|
|
||||||
private long currentTime = -1L;
|
private long currentTime = -1L;
|
||||||
private long selectedId = -1L;
|
private long selectedId = -1L;
|
||||||
|
|
||||||
public TrackScheduleAdapter(Context context, @Nullable EventClickListener listener) {
|
public TrackScheduleAdapter(Context context, @Nullable EventClickListener listener) {
|
||||||
inflater = LayoutInflater.from(context);
|
super(DIFF_CALLBACK);
|
||||||
|
setHasStableIds(true);
|
||||||
timeDateFormat = DateUtils.getTimeDateFormat(context);
|
timeDateFormat = DateUtils.getTimeDateFormat(context);
|
||||||
timeBackgroundColor = ContextCompat.getColor(context, R.color.schedule_time_background);
|
timeBackgroundColor = ContextCompat.getColor(context, R.color.schedule_time_background);
|
||||||
timeRunningBackgroundColor = ContextCompat.getColor(context, R.color.schedule_time_running_background);
|
timeRunningBackgroundColor = ContextCompat.getColor(context, R.color.schedule_time_running_background);
|
||||||
|
@ -99,63 +112,24 @@ public class TrackScheduleAdapter extends RecyclerViewCursorAdapter<TrackSchedul
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Event getItem(int position) {
|
public long getItemId(int position) {
|
||||||
return DatabaseManager.toEvent((Cursor) super.getItem(position));
|
return getItem(position).getEvent().getId();
|
||||||
}
|
}
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
@Override
|
@Override
|
||||||
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
public ViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
||||||
View view = inflater.inflate(R.layout.item_schedule_event, parent, false);
|
View view = LayoutInflater.from(parent.getContext()).inflate(R.layout.item_schedule_event, parent, false);
|
||||||
return new ViewHolder(view, R.drawable.activated_background, listener);
|
return new ViewHolder(view, R.drawable.activated_background);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onBindViewHolder(ViewHolder holder, Cursor cursor) {
|
public void onBindViewHolder(@NonNull ViewHolder holder, int position) {
|
||||||
Context context = holder.itemView.getContext();
|
final StatusEvent statusEvent = getItem(position);
|
||||||
Event event = DatabaseManager.toEvent(cursor, holder.event);
|
final Event event = statusEvent.getEvent();
|
||||||
holder.event = event;
|
holder.bind(event, statusEvent.isBookmarked());
|
||||||
|
holder.bindTimeColors(event, currentTime);
|
||||||
holder.time.setText(timeDateFormat.format(event.getStartTime()));
|
holder.bindSelection(event.getId() == selectedId);
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -163,28 +137,25 @@ public class TrackScheduleAdapter extends RecyclerViewCursorAdapter<TrackSchedul
|
||||||
if (payloads.isEmpty()) {
|
if (payloads.isEmpty()) {
|
||||||
onBindViewHolder(holder, position);
|
onBindViewHolder(holder, position);
|
||||||
} else {
|
} else {
|
||||||
if (holder.event != null) {
|
final StatusEvent statusEvent = getItem(position);
|
||||||
if (payloads.contains(TIME_COLORS_PAYLOAD)) {
|
if (payloads.contains(TIME_COLORS_PAYLOAD)) {
|
||||||
bindTimeColors(holder, holder.event);
|
holder.bindTimeColors(statusEvent.getEvent(), currentTime);
|
||||||
}
|
}
|
||||||
if (payloads.contains(SELECTION_PAYLOAD)) {
|
if (payloads.contains(SELECTION_PAYLOAD)) {
|
||||||
bindSelection(holder, holder.event);
|
holder.bindSelection(statusEvent.getEvent().getId() == selectedId);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static class ViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener {
|
class ViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener {
|
||||||
TextView time;
|
final TextView time;
|
||||||
TextView title;
|
final TextView title;
|
||||||
TextView persons;
|
final TextView persons;
|
||||||
TextView room;
|
final TextView room;
|
||||||
@Nullable
|
|
||||||
EventClickListener listener;
|
|
||||||
|
|
||||||
Event event;
|
Event event;
|
||||||
|
|
||||||
public ViewHolder(@NonNull View itemView, @DrawableRes int activatedBackgroundResId, @Nullable EventClickListener listener) {
|
ViewHolder(@NonNull View itemView, @DrawableRes int activatedBackgroundResId) {
|
||||||
super(itemView);
|
super(itemView);
|
||||||
time = itemView.findViewById(R.id.time);
|
time = itemView.findViewById(R.id.time);
|
||||||
title = itemView.findViewById(R.id.title);
|
title = itemView.findViewById(R.id.title);
|
||||||
|
@ -206,7 +177,46 @@ public class TrackScheduleAdapter extends RecyclerViewCursorAdapter<TrackSchedul
|
||||||
}
|
}
|
||||||
ViewCompat.setBackground(itemView, newBackground);
|
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
|
@Override
|
||||||
|
|
|
@ -1,18 +1,16 @@
|
||||||
package be.digitalia.fosdem.alarms;
|
package be.digitalia.fosdem.alarms;
|
||||||
|
|
||||||
import android.content.BroadcastReceiver;
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.content.IntentFilter;
|
|
||||||
import android.content.SharedPreferences;
|
import android.content.SharedPreferences;
|
||||||
import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
|
import android.content.SharedPreferences.OnSharedPreferenceChangeListener;
|
||||||
|
|
||||||
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
|
|
||||||
import androidx.preference.PreferenceManager;
|
import androidx.preference.PreferenceManager;
|
||||||
import be.digitalia.fosdem.db.DatabaseManager;
|
|
||||||
import be.digitalia.fosdem.fragments.SettingsFragment;
|
import be.digitalia.fosdem.fragments.SettingsFragment;
|
||||||
|
import be.digitalia.fosdem.model.Event;
|
||||||
import be.digitalia.fosdem.services.AlarmIntentService;
|
import be.digitalia.fosdem.services.AlarmIntentService;
|
||||||
|
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This class monitors bookmarks and preferences changes to dispatch alarm update work to AlarmIntentService.
|
* 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 static FosdemAlarmManager instance;
|
||||||
|
|
||||||
private final Context context;
|
private final Context context;
|
||||||
private boolean isEnabled;
|
private volatile 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);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
public static void init(Context context) {
|
public static void init(Context context) {
|
||||||
if (instance == null) {
|
if (instance == null) {
|
||||||
|
@ -58,9 +37,6 @@ public class FosdemAlarmManager implements OnSharedPreferenceChangeListener {
|
||||||
this.context = context;
|
this.context = context;
|
||||||
SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context);
|
SharedPreferences sharedPreferences = PreferenceManager.getDefaultSharedPreferences(context);
|
||||||
isEnabled = sharedPreferences.getBoolean(SettingsFragment.KEY_PREF_NOTIFICATIONS_ENABLED, false);
|
isEnabled = sharedPreferences.getBoolean(SettingsFragment.KEY_PREF_NOTIFICATIONS_ENABLED, false);
|
||||||
if (isEnabled) {
|
|
||||||
registerReceivers();
|
|
||||||
}
|
|
||||||
sharedPreferences.registerOnSharedPreferenceChangeListener(this);
|
sharedPreferences.registerOnSharedPreferenceChangeListener(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -68,15 +44,40 @@ public class FosdemAlarmManager implements OnSharedPreferenceChangeListener {
|
||||||
return isEnabled;
|
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
|
@Override
|
||||||
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
|
public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) {
|
||||||
if (SettingsFragment.KEY_PREF_NOTIFICATIONS_ENABLED.equals(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) {
|
if (isEnabled) {
|
||||||
registerReceivers();
|
|
||||||
startUpdateAlarms();
|
startUpdateAlarms();
|
||||||
} else {
|
} else {
|
||||||
unregisterReceivers();
|
|
||||||
startDisableAlarms();
|
startDisableAlarms();
|
||||||
}
|
}
|
||||||
} else if (SettingsFragment.KEY_PREF_NOTIFICATIONS_DELAY.equals(key)) {
|
} else if (SettingsFragment.KEY_PREF_NOTIFICATIONS_DELAY.equals(key)) {
|
||||||
|
@ -84,22 +85,7 @@ public class FosdemAlarmManager implements OnSharedPreferenceChangeListener {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void registerReceivers() {
|
private void startUpdateAlarms() {
|
||||||
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() {
|
|
||||||
Intent serviceIntent = new Intent(AlarmIntentService.ACTION_UPDATE_ALARMS);
|
Intent serviceIntent = new Intent(AlarmIntentService.ACTION_UPDATE_ALARMS);
|
||||||
AlarmIntentService.enqueueWork(context, serviceIntent);
|
AlarmIntentService.enqueueWork(context, serviceIntent);
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,10 +6,11 @@ import androidx.annotation.MainThread;
|
||||||
import androidx.annotation.WorkerThread;
|
import androidx.annotation.WorkerThread;
|
||||||
import androidx.lifecycle.LiveData;
|
import androidx.lifecycle.LiveData;
|
||||||
import androidx.lifecycle.MutableLiveData;
|
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.livedata.SingleEvent;
|
||||||
|
import be.digitalia.fosdem.model.DetailedEvent;
|
||||||
import be.digitalia.fosdem.model.DownloadScheduleResult;
|
import be.digitalia.fosdem.model.DownloadScheduleResult;
|
||||||
import be.digitalia.fosdem.model.Event;
|
|
||||||
import be.digitalia.fosdem.model.RoomStatus;
|
import be.digitalia.fosdem.model.RoomStatus;
|
||||||
import be.digitalia.fosdem.parsers.EventsParser;
|
import be.digitalia.fosdem.parsers.EventsParser;
|
||||||
import be.digitalia.fosdem.utils.HttpUtils;
|
import be.digitalia.fosdem.utils.HttpUtils;
|
||||||
|
@ -55,10 +56,10 @@ public class FosdemApi {
|
||||||
progress.postValue(-1);
|
progress.postValue(-1);
|
||||||
DownloadScheduleResult res = DownloadScheduleResult.error();
|
DownloadScheduleResult res = DownloadScheduleResult.error();
|
||||||
try {
|
try {
|
||||||
DatabaseManager dbManager = DatabaseManager.getInstance();
|
ScheduleDao scheduleDao = AppDatabase.getInstance(context).getScheduleDao();
|
||||||
HttpUtils.HttpResult httpResult = HttpUtils.get(
|
HttpUtils.HttpResult httpResult = HttpUtils.get(
|
||||||
FosdemUrls.getSchedule(),
|
FosdemUrls.getSchedule(),
|
||||||
dbManager.getLastModifiedTag(),
|
scheduleDao.getLastModifiedTag(),
|
||||||
new HttpUtils.ProgressUpdateListener() {
|
new HttpUtils.ProgressUpdateListener() {
|
||||||
@Override
|
@Override
|
||||||
public void onProgressUpdate(int percent) {
|
public void onProgressUpdate(int percent) {
|
||||||
|
@ -72,8 +73,8 @@ public class FosdemApi {
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
Iterable<Event> events = new EventsParser().parse(httpResult.inputStream);
|
Iterable<DetailedEvent> events = new EventsParser().parse(httpResult.inputStream);
|
||||||
int count = dbManager.storeSchedule(events, httpResult.lastModified);
|
int count = scheduleDao.storeSchedule(events, httpResult.lastModified);
|
||||||
res = DownloadScheduleResult.success(count);
|
res = DownloadScheduleResult.success(count);
|
||||||
} finally {
|
} finally {
|
||||||
try {
|
try {
|
||||||
|
@ -106,12 +107,12 @@ public class FosdemApi {
|
||||||
}
|
}
|
||||||
|
|
||||||
@MainThread
|
@MainThread
|
||||||
public static LiveData<Map<String, RoomStatus>> getRoomStatuses() {
|
public static LiveData<Map<String, RoomStatus>> getRoomStatuses(Context context) {
|
||||||
if (roomStatuses == null) {
|
if (roomStatuses == null) {
|
||||||
// The room statuses will only be loaded when the event is live.
|
// The room statuses will only be loaded when the event is live.
|
||||||
// RoomStatusesLiveData uses the days from the database to determine it.
|
// RoomStatusesLiveData uses the days from the database to determine it.
|
||||||
roomStatuses = new RoomStatusesLiveData(DatabaseManager.getInstance().getDays());
|
roomStatuses = new RoomStatusesLiveData(AppDatabase.getInstance(context).getScheduleDao().getDays());
|
||||||
// Implementors: replace the above live with the next one to disable room status support
|
// Implementors: replace the above line with the next one to disable room status support
|
||||||
// roomStatuses = new MutableLiveData<>();
|
// roomStatuses = new MutableLiveData<>();
|
||||||
}
|
}
|
||||||
return roomStatuses;
|
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;
|
package be.digitalia.fosdem.fragments;
|
||||||
|
|
||||||
import android.database.Cursor;
|
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.fragment.app.Fragment;
|
import androidx.fragment.app.Fragment;
|
||||||
import androidx.loader.app.LoaderManager;
|
import androidx.lifecycle.LiveData;
|
||||||
import androidx.loader.app.LoaderManager.LoaderCallbacks;
|
import androidx.lifecycle.Observer;
|
||||||
import androidx.loader.content.Loader;
|
import androidx.lifecycle.ViewModelProviders;
|
||||||
|
import androidx.paging.PagedList;
|
||||||
import androidx.recyclerview.widget.DividerItemDecoration;
|
import androidx.recyclerview.widget.DividerItemDecoration;
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||||
import androidx.recyclerview.widget.RecyclerView;
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
import be.digitalia.fosdem.adapters.EventsAdapter;
|
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> {
|
public abstract class BaseLiveListFragment extends RecyclerViewFragment implements Observer<PagedList<StatusEvent>> {
|
||||||
|
|
||||||
private static final int EVENTS_LOADER_ID = 1;
|
|
||||||
|
|
||||||
private EventsAdapter adapter;
|
private EventsAdapter adapter;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onCreate(Bundle savedInstanceState) {
|
public void onCreate(Bundle savedInstanceState) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
adapter = new EventsAdapter(getActivity(), this, false);
|
adapter = new EventsAdapter(getContext(), this, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -44,22 +43,29 @@ public abstract class BaseLiveListFragment extends RecyclerViewFragment implemen
|
||||||
setEmptyText(getEmptyText());
|
setEmptyText(getEmptyText());
|
||||||
setProgressBarVisible(true);
|
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();
|
protected abstract String getEmptyText();
|
||||||
|
|
||||||
@Override
|
@NonNull
|
||||||
public void onLoadFinished(@NonNull Loader<Cursor> loader, Cursor data) {
|
protected abstract LiveData<PagedList<StatusEvent>> getDataSource(@NonNull LiveViewModel viewModel);
|
||||||
if (data != null) {
|
|
||||||
adapter.swapCursor(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
setProgressBarVisible(false);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onLoaderReset(@NonNull Loader<Cursor> loader) {
|
|
||||||
adapter.swapCursor(null);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,41 +2,39 @@ package be.digitalia.fosdem.fragments;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.database.Cursor;
|
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.os.Handler;
|
|
||||||
import android.text.format.DateUtils;
|
|
||||||
import android.view.Menu;
|
import android.view.Menu;
|
||||||
import android.view.MenuInflater;
|
import android.view.MenuInflater;
|
||||||
import android.view.MenuItem;
|
import android.view.MenuItem;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.appcompat.app.AppCompatActivity;
|
import androidx.appcompat.app.AppCompatActivity;
|
||||||
import androidx.loader.app.LoaderManager;
|
import androidx.appcompat.view.ActionMode;
|
||||||
import androidx.loader.app.LoaderManager.LoaderCallbacks;
|
import androidx.lifecycle.Observer;
|
||||||
import androidx.loader.content.Loader;
|
import androidx.lifecycle.ViewModelProviders;
|
||||||
import androidx.recyclerview.widget.DividerItemDecoration;
|
import androidx.recyclerview.widget.DividerItemDecoration;
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||||
import androidx.recyclerview.widget.RecyclerView;
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
import be.digitalia.fosdem.R;
|
import be.digitalia.fosdem.R;
|
||||||
import be.digitalia.fosdem.adapters.BookmarksAdapter;
|
import be.digitalia.fosdem.adapters.BookmarksAdapter;
|
||||||
import be.digitalia.fosdem.db.DatabaseManager;
|
import be.digitalia.fosdem.model.Event;
|
||||||
import be.digitalia.fosdem.loaders.SimpleCursorLoader;
|
|
||||||
import be.digitalia.fosdem.providers.BookmarksExportProvider;
|
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.
|
* Bookmarks list, optionally filterable.
|
||||||
*
|
*
|
||||||
* @author Christophe Beyls
|
* @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 PREF_UPCOMING_ONLY = "bookmarks_upcoming_only";
|
||||||
private static final String STATE_ADAPTER = "adapter";
|
private static final String STATE_ADAPTER = "adapter";
|
||||||
|
|
||||||
|
private BookmarksViewModel viewModel;
|
||||||
private BookmarksAdapter adapter;
|
private BookmarksAdapter adapter;
|
||||||
private boolean upcomingOnly;
|
|
||||||
|
|
||||||
private MenuItem filterMenuItem;
|
private MenuItem filterMenuItem;
|
||||||
private MenuItem upcomingOnlyMenuItem;
|
private MenuItem upcomingOnlyMenuItem;
|
||||||
|
@ -45,11 +43,53 @@ public class BookmarksListFragment extends RecyclerViewFragment implements Loade
|
||||||
public void onCreate(Bundle savedInstanceState) {
|
public void onCreate(Bundle savedInstanceState) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
|
|
||||||
adapter = new BookmarksAdapter((AppCompatActivity) getActivity(), this);
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
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) {
|
if (savedInstanceState != null) {
|
||||||
adapter.onRestoreInstanceState(savedInstanceState.getParcelable(STATE_ADAPTER));
|
adapter.getMultiChoiceHelper().onRestoreInstanceState(savedInstanceState.getParcelable(STATE_ADAPTER));
|
||||||
}
|
}
|
||||||
upcomingOnly = getActivity().getPreferences(Context.MODE_PRIVATE).getBoolean(PREF_UPCOMING_ONLY, false);
|
boolean upcomingOnly = getActivity().getPreferences(Context.MODE_PRIVATE).getBoolean(PREF_UPCOMING_ONLY, false);
|
||||||
|
viewModel.setUpcomingOnly(upcomingOnly);
|
||||||
|
|
||||||
setHasOptionsMenu(true);
|
setHasOptionsMenu(true);
|
||||||
}
|
}
|
||||||
|
@ -68,18 +108,18 @@ public class BookmarksListFragment extends RecyclerViewFragment implements Loade
|
||||||
setEmptyText(getString(R.string.no_bookmark));
|
setEmptyText(getString(R.string.no_bookmark));
|
||||||
setProgressBarVisible(true);
|
setProgressBarVisible(true);
|
||||||
|
|
||||||
LoaderManager.getInstance(this).initLoader(BOOKMARKS_LOADER_ID, null, this);
|
viewModel.getBookmarks().observe(getViewLifecycleOwner(), this);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onSaveInstanceState(@NonNull Bundle outState) {
|
public void onSaveInstanceState(@NonNull Bundle outState) {
|
||||||
super.onSaveInstanceState(outState);
|
super.onSaveInstanceState(outState);
|
||||||
outState.putParcelable(STATE_ADAPTER, adapter.onSaveInstanceState());
|
outState.putParcelable(STATE_ADAPTER, adapter.getMultiChoiceHelper().onSaveInstanceState());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onDestroyView() {
|
public void onDestroyView() {
|
||||||
adapter.onDestroyView();
|
adapter.getMultiChoiceHelper().clearChoices();
|
||||||
super.onDestroyView();
|
super.onDestroyView();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -93,6 +133,7 @@ public class BookmarksListFragment extends RecyclerViewFragment implements Loade
|
||||||
|
|
||||||
private void updateFilterMenuItem() {
|
private void updateFilterMenuItem() {
|
||||||
if (filterMenuItem != null) {
|
if (filterMenuItem != null) {
|
||||||
|
final boolean upcomingOnly = viewModel.getUpcomingOnly();
|
||||||
filterMenuItem.setIcon(upcomingOnly ?
|
filterMenuItem.setIcon(upcomingOnly ?
|
||||||
R.drawable.ic_filter_list_selected_white_24dp
|
R.drawable.ic_filter_list_selected_white_24dp
|
||||||
: R.drawable.ic_filter_list_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) {
|
public boolean onOptionsItemSelected(MenuItem item) {
|
||||||
switch (item.getItemId()) {
|
switch (item.getItemId()) {
|
||||||
case R.id.upcoming_only:
|
case R.id.upcoming_only:
|
||||||
upcomingOnly = !upcomingOnly;
|
final boolean upcomingOnly = !viewModel.getUpcomingOnly();
|
||||||
|
viewModel.setUpcomingOnly(upcomingOnly);
|
||||||
updateFilterMenuItem();
|
updateFilterMenuItem();
|
||||||
getActivity().getPreferences(Context.MODE_PRIVATE).edit()
|
getActivity().getPreferences(Context.MODE_PRIVATE).edit()
|
||||||
.putBoolean(PREF_UPCOMING_ONLY, upcomingOnly)
|
.putBoolean(PREF_UPCOMING_ONLY, upcomingOnly)
|
||||||
.apply();
|
.apply();
|
||||||
LoaderManager.getInstance(this).restartLoader(BOOKMARKS_LOADER_ID, null, this);
|
|
||||||
return true;
|
return true;
|
||||||
case R.id.export_bookmarks:
|
case R.id.export_bookmarks:
|
||||||
Intent exportIntent = BookmarksExportProvider.getIntent(getActivity());
|
Intent exportIntent = BookmarksExportProvider.getIntent(getActivity());
|
||||||
|
@ -126,78 +167,9 @@ public class BookmarksListFragment extends RecyclerViewFragment implements Loade
|
||||||
return false;
|
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
|
@Override
|
||||||
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
|
public void onChanged(List<Event> bookmarks) {
|
||||||
return new BookmarksLoader(getActivity(), upcomingOnly);
|
adapter.submitList(bookmarks);
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onLoadFinished(@NonNull Loader<Cursor> loader, Cursor data) {
|
|
||||||
if (data != null) {
|
|
||||||
adapter.swapCursor(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
setProgressBarVisible(false);
|
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.net.Uri;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.provider.CalendarContract;
|
import android.provider.CalendarContract;
|
||||||
import android.text.Spannable;
|
import android.text.*;
|
||||||
import android.text.SpannableString;
|
|
||||||
import android.text.SpannableStringBuilder;
|
|
||||||
import android.text.TextPaint;
|
|
||||||
import android.text.TextUtils;
|
|
||||||
import android.text.method.LinkMovementMethod;
|
import android.text.method.LinkMovementMethod;
|
||||||
import android.text.style.ClickableSpan;
|
import android.text.style.ClickableSpan;
|
||||||
import android.view.LayoutInflater;
|
import android.view.*;
|
||||||
import android.view.Menu;
|
|
||||||
import android.view.MenuInflater;
|
|
||||||
import android.view.MenuItem;
|
|
||||||
import android.view.View;
|
|
||||||
import android.view.ViewGroup;
|
|
||||||
import android.widget.ImageView;
|
import android.widget.ImageView;
|
||||||
import android.widget.TextView;
|
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.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.browser.customtabs.CustomTabsIntent;
|
import androidx.browser.customtabs.CustomTabsIntent;
|
||||||
|
@ -42,15 +26,17 @@ import androidx.lifecycle.ViewModelProviders;
|
||||||
import be.digitalia.fosdem.R;
|
import be.digitalia.fosdem.R;
|
||||||
import be.digitalia.fosdem.activities.PersonInfoActivity;
|
import be.digitalia.fosdem.activities.PersonInfoActivity;
|
||||||
import be.digitalia.fosdem.api.FosdemApi;
|
import be.digitalia.fosdem.api.FosdemApi;
|
||||||
import be.digitalia.fosdem.model.Building;
|
import be.digitalia.fosdem.model.*;
|
||||||
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.utils.ClickableArrowKeyMovementMethod;
|
import be.digitalia.fosdem.utils.ClickableArrowKeyMovementMethod;
|
||||||
import be.digitalia.fosdem.utils.DateUtils;
|
import be.digitalia.fosdem.utils.DateUtils;
|
||||||
import be.digitalia.fosdem.utils.StringUtils;
|
import be.digitalia.fosdem.utils.StringUtils;
|
||||||
import be.digitalia.fosdem.viewmodels.EventDetailsViewModel;
|
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 {
|
public class EventDetailsFragment extends Fragment {
|
||||||
|
|
||||||
|
@ -73,7 +59,6 @@ public class EventDetailsFragment extends Fragment {
|
||||||
private static final String ARG_EVENT = "event";
|
private static final String ARG_EVENT = "event";
|
||||||
|
|
||||||
Event event;
|
Event event;
|
||||||
int personsCount = 1;
|
|
||||||
ViewHolder holder;
|
ViewHolder holder;
|
||||||
EventDetailsViewModel viewModel;
|
EventDetailsViewModel viewModel;
|
||||||
|
|
||||||
|
@ -152,12 +137,12 @@ public class EventDetailsFragment extends Fragment {
|
||||||
if (roomImageResId != 0) {
|
if (roomImageResId != 0) {
|
||||||
roomText.setSpan(new ClickableSpan() {
|
roomText.setSpan(new ClickableSpan() {
|
||||||
@Override
|
@Override
|
||||||
public void onClick(View view) {
|
public void onClick(@NonNull View view) {
|
||||||
RoomImageDialogFragment.newInstance(roomName, roomImageResId).show(getFragmentManager());
|
RoomImageDialogFragment.newInstance(roomName, roomImageResId).show(getFragmentManager());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void updateDrawState(TextPaint ds) {
|
public void updateDrawState(@NonNull TextPaint ds) {
|
||||||
super.updateDrawState(ds);
|
super.updateDrawState(ds);
|
||||||
ds.setUnderlineText(false);
|
ds.setUnderlineText(false);
|
||||||
}
|
}
|
||||||
|
@ -213,15 +198,17 @@ public class EventDetailsFragment extends Fragment {
|
||||||
updateBookmarkMenuItem(isBookmarked, true);
|
updateBookmarkMenuItem(isBookmarked, true);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
viewModel.getEventDetails().observe(getViewLifecycleOwner(), new Observer<EventDetailsViewModel.EventDetails>() {
|
viewModel.getEventDetails().observe(getViewLifecycleOwner(), new Observer<EventDetails>() {
|
||||||
@Override
|
@Override
|
||||||
public void onChanged(@Nullable EventDetailsViewModel.EventDetails eventDetails) {
|
public void onChanged(EventDetails eventDetails) {
|
||||||
setEventDetails(eventDetails);
|
if (eventDetails != null) {
|
||||||
|
setEventDetails(eventDetails);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Live room status
|
// Live room status
|
||||||
FosdemApi.getRoomStatuses().observe(getViewLifecycleOwner(), new Observer<Map<String, RoomStatus>>() {
|
FosdemApi.getRoomStatuses(getContext()).observe(getViewLifecycleOwner(), new Observer<Map<String, RoomStatus>>() {
|
||||||
@Override
|
@Override
|
||||||
public void onChanged(Map<String, RoomStatus> roomStatuses) {
|
public void onChanged(Map<String, RoomStatus> roomStatuses) {
|
||||||
RoomStatus roomStatus = roomStatuses.get(event.getRoomName());
|
RoomStatus roomStatus = roomStatuses.get(event.getRoomName());
|
||||||
|
@ -354,6 +341,8 @@ public class EventDetailsFragment extends Fragment {
|
||||||
}
|
}
|
||||||
description = StringUtils.stripHtml(description);
|
description = StringUtils.stripHtml(description);
|
||||||
// Add speaker info if available
|
// Add speaker info if available
|
||||||
|
EventDetails details = viewModel.getEventDetails().getValue();
|
||||||
|
final int personsCount = (details == null) ? 0 : details.getPersons().size();
|
||||||
if (personsCount > 0) {
|
if (personsCount > 0) {
|
||||||
description = String.format("%1$s: %2$s\n\n%3$s", getResources().getQuantityString(R.plurals.speakers, personsCount), event.getPersonsSummary(),
|
description = String.format("%1$s: %2$s\n\n%3$s", getResources().getQuantityString(R.plurals.speakers, personsCount), event.getPersonsSummary(),
|
||||||
description);
|
description);
|
||||||
|
@ -374,34 +363,33 @@ public class EventDetailsFragment extends Fragment {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void setEventDetails(@NonNull EventDetailsViewModel.EventDetails data) {
|
void setEventDetails(@NonNull EventDetails eventDetails) {
|
||||||
// 1. Persons
|
// 1. Persons
|
||||||
if (data.persons != null) {
|
final List<Person> persons = eventDetails.getPersons();
|
||||||
personsCount = data.persons.size();
|
if (persons.size() > 0) {
|
||||||
if (personsCount > 0) {
|
// Build a list of clickable persons
|
||||||
// Build a list of clickable persons
|
SpannableStringBuilder sb = new SpannableStringBuilder();
|
||||||
SpannableStringBuilder sb = new SpannableStringBuilder();
|
int length = 0;
|
||||||
int length = 0;
|
for (Person person : persons) {
|
||||||
for (Person person : data.persons) {
|
if (length != 0) {
|
||||||
if (length != 0) {
|
sb.append(", ");
|
||||||
sb.append(", ");
|
|
||||||
}
|
|
||||||
String name = person.getName();
|
|
||||||
sb.append(name);
|
|
||||||
length = sb.length();
|
|
||||||
sb.setSpan(new PersonClickableSpan(person), length - name.length(), length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
|
|
||||||
}
|
}
|
||||||
holder.personsTextView.setText(sb);
|
String name = person.getName();
|
||||||
holder.personsTextView.setVisibility(View.VISIBLE);
|
sb.append(name);
|
||||||
|
length = sb.length();
|
||||||
|
sb.setSpan(new PersonClickableSpan(person), length - name.length(), length, Spannable.SPAN_EXCLUSIVE_EXCLUSIVE);
|
||||||
}
|
}
|
||||||
|
holder.personsTextView.setText(sb);
|
||||||
|
holder.personsTextView.setVisibility(View.VISIBLE);
|
||||||
}
|
}
|
||||||
|
|
||||||
// 2. Links
|
// 2. Links
|
||||||
|
final List<Link> links = eventDetails.getLinks();
|
||||||
holder.linksContainer.removeAllViews();
|
holder.linksContainer.removeAllViews();
|
||||||
if ((data.links != null) && (data.links.size() > 0)) {
|
if (links.size() > 0) {
|
||||||
holder.linksHeader.setVisibility(View.VISIBLE);
|
holder.linksHeader.setVisibility(View.VISIBLE);
|
||||||
holder.linksContainer.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);
|
View view = holder.inflater.inflate(R.layout.item_link, holder.linksContainer, false);
|
||||||
TextView tv = view.findViewById(R.id.description);
|
TextView tv = view.findViewById(R.id.description);
|
||||||
tv.setText(link.getDescription());
|
tv.setText(link.getDescription());
|
||||||
|
@ -423,14 +411,14 @@ public class EventDetailsFragment extends Fragment {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onClick(View v) {
|
public void onClick(@NonNull View v) {
|
||||||
Context context = v.getContext();
|
Context context = v.getContext();
|
||||||
Intent intent = new Intent(context, PersonInfoActivity.class).putExtra(PersonInfoActivity.EXTRA_PERSON, person);
|
Intent intent = new Intent(context, PersonInfoActivity.class).putExtra(PersonInfoActivity.EXTRA_PERSON, person);
|
||||||
context.startActivity(intent);
|
context.startActivity(intent);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void updateDrawState(TextPaint ds) {
|
public void updateDrawState(@NonNull TextPaint ds) {
|
||||||
super.updateDrawState(ds);
|
super.updateDrawState(ds);
|
||||||
ds.setUnderlineText(false);
|
ds.setUnderlineText(false);
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,14 +1,11 @@
|
||||||
package be.digitalia.fosdem.fragments;
|
package be.digitalia.fosdem.fragments;
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.database.Cursor;
|
|
||||||
import android.os.Bundle;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
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.R;
|
||||||
import be.digitalia.fosdem.db.DatabaseManager;
|
import be.digitalia.fosdem.model.StatusEvent;
|
||||||
import be.digitalia.fosdem.loaders.BaseLiveLoader;
|
import be.digitalia.fosdem.viewmodels.LiveViewModel;
|
||||||
|
|
||||||
public class NextLiveListFragment extends BaseLiveListFragment {
|
public class NextLiveListFragment extends BaseLiveListFragment {
|
||||||
|
|
||||||
|
@ -19,22 +16,7 @@ public class NextLiveListFragment extends BaseLiveListFragment {
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
@Override
|
@Override
|
||||||
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
|
protected LiveData<PagedList<StatusEvent>> getDataSource(@NonNull LiveViewModel viewModel) {
|
||||||
return new NextLiveLoader(getActivity());
|
return viewModel.getNextEvents();
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,14 +1,11 @@
|
||||||
package be.digitalia.fosdem.fragments;
|
package be.digitalia.fosdem.fragments;
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.database.Cursor;
|
|
||||||
import android.os.Bundle;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
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.R;
|
||||||
import be.digitalia.fosdem.db.DatabaseManager;
|
import be.digitalia.fosdem.model.StatusEvent;
|
||||||
import be.digitalia.fosdem.loaders.BaseLiveLoader;
|
import be.digitalia.fosdem.viewmodels.LiveViewModel;
|
||||||
|
|
||||||
public class NowLiveListFragment extends BaseLiveListFragment {
|
public class NowLiveListFragment extends BaseLiveListFragment {
|
||||||
|
|
||||||
|
@ -19,20 +16,7 @@ public class NowLiveListFragment extends BaseLiveListFragment {
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
@Override
|
@Override
|
||||||
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
|
protected LiveData<PagedList<StatusEvent>> getDataSource(@NonNull LiveViewModel viewModel) {
|
||||||
return new NowLiveLoader(getActivity());
|
return viewModel.getEventsInProgress();
|
||||||
}
|
|
||||||
|
|
||||||
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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,36 +2,27 @@ package be.digitalia.fosdem.fragments;
|
||||||
|
|
||||||
import android.app.Activity;
|
import android.app.Activity;
|
||||||
import android.content.ActivityNotFoundException;
|
import android.content.ActivityNotFoundException;
|
||||||
import android.content.Context;
|
|
||||||
import android.database.Cursor;
|
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.view.LayoutInflater;
|
import android.view.*;
|
||||||
import android.view.Menu;
|
|
||||||
import android.view.MenuInflater;
|
|
||||||
import android.view.MenuItem;
|
|
||||||
import android.view.View;
|
|
||||||
import android.view.ViewGroup;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.browser.customtabs.CustomTabsIntent;
|
import androidx.browser.customtabs.CustomTabsIntent;
|
||||||
import androidx.core.content.ContextCompat;
|
import androidx.core.content.ContextCompat;
|
||||||
import androidx.loader.app.LoaderManager;
|
import androidx.lifecycle.Observer;
|
||||||
import androidx.loader.app.LoaderManager.LoaderCallbacks;
|
import androidx.lifecycle.ViewModelProviders;
|
||||||
import androidx.loader.content.Loader;
|
import androidx.paging.PagedList;
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||||
import androidx.recyclerview.widget.RecyclerView;
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
import be.digitalia.fosdem.R;
|
import be.digitalia.fosdem.R;
|
||||||
import be.digitalia.fosdem.adapters.ConcatAdapter;
|
import be.digitalia.fosdem.adapters.ConcatAdapter;
|
||||||
import be.digitalia.fosdem.adapters.EventsAdapter;
|
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.Person;
|
||||||
|
import be.digitalia.fosdem.model.StatusEvent;
|
||||||
import be.digitalia.fosdem.utils.DateUtils;
|
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 static final String ARG_PERSON = "person";
|
||||||
|
|
||||||
private Person person;
|
private Person person;
|
||||||
|
@ -49,7 +40,7 @@ public class PersonInfoListFragment extends RecyclerViewFragment implements Load
|
||||||
public void onCreate(Bundle savedInstanceState) {
|
public void onCreate(Bundle savedInstanceState) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
|
|
||||||
adapter = new EventsAdapter(getActivity(), this);
|
adapter = new EventsAdapter(getContext(), this);
|
||||||
person = getArguments().getParcelable(ARG_PERSON);
|
person = getArguments().getParcelable(ARG_PERSON);
|
||||||
setHasOptionsMenu(true);
|
setHasOptionsMenu(true);
|
||||||
}
|
}
|
||||||
|
@ -63,8 +54,18 @@ public class PersonInfoListFragment extends RecyclerViewFragment implements Load
|
||||||
public boolean onOptionsItemSelected(MenuItem item) {
|
public boolean onOptionsItemSelected(MenuItem item) {
|
||||||
switch (item.getItemId()) {
|
switch (item.getItemId()) {
|
||||||
case R.id.more_info:
|
case R.id.more_info:
|
||||||
if (adapter.getItemCount() > 0) {
|
// Look for the first non-placeholder event in the paged list
|
||||||
final int year = DateUtils.getYear(adapter.getItem(0).getDay().getDate().getTime());
|
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);
|
String url = person.getUrl(year);
|
||||||
if (url != null) {
|
if (url != null) {
|
||||||
try {
|
try {
|
||||||
|
@ -102,44 +103,17 @@ public class PersonInfoListFragment extends RecyclerViewFragment implements Load
|
||||||
setEmptyText(getString(R.string.no_data));
|
setEmptyText(getString(R.string.no_data));
|
||||||
setProgressBarVisible(true);
|
setProgressBarVisible(true);
|
||||||
|
|
||||||
LoaderManager.getInstance(this).initLoader(PERSON_EVENTS_LOADER_ID, null, this);
|
final PersonInfoViewModel viewModel = ViewModelProviders.of(this).get(PersonInfoViewModel.class);
|
||||||
}
|
viewModel.setPerson(person);
|
||||||
|
viewModel.getEvents().observe(getViewLifecycleOwner(), this);
|
||||||
private static class PersonEventsLoader extends SimpleCursorLoader {
|
|
||||||
|
|
||||||
private final Person person;
|
|
||||||
|
|
||||||
public PersonEventsLoader(Context context, Person person) {
|
|
||||||
super(context);
|
|
||||||
this.person = person;
|
|
||||||
}
|
|
||||||
|
|
||||||
@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
|
@Override
|
||||||
public void onLoadFinished(@NonNull Loader<Cursor> loader, Cursor data) {
|
public void onChanged(PagedList<StatusEvent> events) {
|
||||||
if (data != null) {
|
adapter.submitList(events);
|
||||||
adapter.swapCursor(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
setProgressBarVisible(false);
|
setProgressBarVisible(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onLoaderReset(@NonNull Loader<Cursor> loader) {
|
|
||||||
adapter.swapCursor(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
static class HeaderAdapter extends RecyclerView.Adapter<HeaderAdapter.ViewHolder> {
|
static class HeaderAdapter extends RecyclerView.Adapter<HeaderAdapter.ViewHolder> {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
|
|
@ -2,38 +2,36 @@ package be.digitalia.fosdem.fragments;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.database.Cursor;
|
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.loader.app.LoaderManager;
|
import androidx.core.util.ObjectsCompat;
|
||||||
import androidx.loader.app.LoaderManager.LoaderCallbacks;
|
import androidx.lifecycle.Observer;
|
||||||
import androidx.loader.content.Loader;
|
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.DividerItemDecoration;
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||||
import androidx.recyclerview.widget.RecyclerView;
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
import be.digitalia.fosdem.R;
|
import be.digitalia.fosdem.R;
|
||||||
import be.digitalia.fosdem.activities.PersonInfoActivity;
|
import be.digitalia.fosdem.activities.PersonInfoActivity;
|
||||||
import be.digitalia.fosdem.adapters.RecyclerViewCursorAdapter;
|
import be.digitalia.fosdem.adapters.SimpleItemCallback;
|
||||||
import be.digitalia.fosdem.db.DatabaseManager;
|
|
||||||
import be.digitalia.fosdem.loaders.SimpleCursorLoader;
|
|
||||||
import be.digitalia.fosdem.model.Person;
|
import be.digitalia.fosdem.model.Person;
|
||||||
|
import be.digitalia.fosdem.viewmodels.PersonsViewModel;
|
||||||
|
|
||||||
public class PersonsListFragment extends RecyclerViewFragment implements LoaderCallbacks<Cursor> {
|
public class PersonsListFragment extends RecyclerViewFragment implements Observer<PagedList<Person>> {
|
||||||
|
|
||||||
private static final int PERSONS_LOADER_ID = 1;
|
|
||||||
|
|
||||||
private PersonsAdapter adapter;
|
private PersonsAdapter adapter;
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onCreate(Bundle savedInstanceState) {
|
public void onCreate(Bundle savedInstanceState) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
adapter = new PersonsAdapter(getActivity());
|
adapter = new PersonsAdapter();
|
||||||
}
|
}
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
|
@ -56,70 +54,49 @@ public class PersonsListFragment extends RecyclerViewFragment implements LoaderC
|
||||||
setEmptyText(getString(R.string.no_data));
|
setEmptyText(getString(R.string.no_data));
|
||||||
setProgressBarVisible(true);
|
setProgressBarVisible(true);
|
||||||
|
|
||||||
LoaderManager.getInstance(this).initLoader(PERSONS_LOADER_ID, null, this);
|
final PersonsViewModel viewModel = ViewModelProviders.of(this).get(PersonsViewModel.class);
|
||||||
}
|
viewModel.getPersons().observe(getViewLifecycleOwner(), this);
|
||||||
|
|
||||||
private static class PersonsLoader extends SimpleCursorLoader {
|
|
||||||
|
|
||||||
PersonsLoader(Context context) {
|
|
||||||
super(context);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected Cursor getCursor() {
|
|
||||||
return DatabaseManager.getInstance().getPersons();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@NonNull
|
|
||||||
@Override
|
|
||||||
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
|
|
||||||
return new PersonsLoader(getActivity());
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onLoadFinished(@NonNull Loader<Cursor> loader, Cursor data) {
|
public void onChanged(PagedList<Person> persons) {
|
||||||
if (data != null) {
|
adapter.submitList(persons);
|
||||||
adapter.swapCursor(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
setProgressBarVisible(false);
|
setProgressBarVisible(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
private static class PersonsAdapter extends PagedListAdapter<Person, PersonViewHolder> {
|
||||||
public void onLoaderReset(@NonNull Loader<Cursor> loader) {
|
|
||||||
adapter.swapCursor(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static class PersonsAdapter extends RecyclerViewCursorAdapter<PersonViewHolder> {
|
private static final DiffUtil.ItemCallback<Person> DIFF_CALLBACK = new SimpleItemCallback<Person>() {
|
||||||
|
@Override
|
||||||
|
public boolean areContentsTheSame(@NonNull Person oldItem, @NonNull Person newItem) {
|
||||||
|
return ObjectsCompat.equals(oldItem.getName(), newItem.getName());
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
private final LayoutInflater inflater;
|
PersonsAdapter() {
|
||||||
|
super(DIFF_CALLBACK);
|
||||||
PersonsAdapter(Context context) {
|
|
||||||
inflater = LayoutInflater.from(context);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public Person getItem(int position) {
|
|
||||||
return DatabaseManager.toPerson((Cursor) super.getItem(position));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
@Override
|
@Override
|
||||||
public PersonViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
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);
|
return new PersonViewHolder(view);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onBindViewHolder(PersonViewHolder holder, Cursor cursor) {
|
public void onBindViewHolder(@NonNull PersonViewHolder holder, int position) {
|
||||||
holder.person = DatabaseManager.toPerson(cursor, holder.person);
|
final Person person = getItem(position);
|
||||||
holder.textView.setText(holder.person.getName());
|
if (person == null) {
|
||||||
|
holder.clear();
|
||||||
|
} else {
|
||||||
|
holder.bind(person);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static class PersonViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener {
|
static class PersonViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener {
|
||||||
TextView textView;
|
final TextView textView;
|
||||||
|
|
||||||
Person person;
|
Person person;
|
||||||
|
|
||||||
|
@ -129,12 +106,24 @@ public class PersonsListFragment extends RecyclerViewFragment implements LoaderC
|
||||||
itemView.setOnClickListener(this);
|
itemView.setOnClickListener(this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void clear() {
|
||||||
|
this.person = null;
|
||||||
|
textView.setText(null);
|
||||||
|
}
|
||||||
|
|
||||||
|
void bind(@NonNull Person person) {
|
||||||
|
this.person = person;
|
||||||
|
textView.setText(person.getName());
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onClick(View view) {
|
public void onClick(View view) {
|
||||||
final Context context = view.getContext();
|
if (person != null) {
|
||||||
Intent intent = new Intent(context, PersonInfoActivity.class)
|
final Context context = view.getContext();
|
||||||
.putExtra(PersonInfoActivity.EXTRA_PERSON, person);
|
Intent intent = new Intent(context, PersonInfoActivity.class)
|
||||||
context.startActivity(intent);
|
.putExtra(PersonInfoActivity.EXTRA_PERSON, person);
|
||||||
|
context.startActivity(intent);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,40 +1,29 @@
|
||||||
package be.digitalia.fosdem.fragments;
|
package be.digitalia.fosdem.fragments;
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.database.Cursor;
|
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
|
import androidx.lifecycle.Observer;
|
||||||
import androidx.annotation.NonNull;
|
import androidx.lifecycle.ViewModelProviders;
|
||||||
import androidx.loader.app.LoaderManager;
|
import androidx.paging.PagedList;
|
||||||
import androidx.loader.app.LoaderManager.LoaderCallbacks;
|
|
||||||
import androidx.loader.content.Loader;
|
|
||||||
import androidx.recyclerview.widget.DividerItemDecoration;
|
import androidx.recyclerview.widget.DividerItemDecoration;
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||||
import androidx.recyclerview.widget.RecyclerView;
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
import be.digitalia.fosdem.R;
|
import be.digitalia.fosdem.R;
|
||||||
import be.digitalia.fosdem.adapters.EventsAdapter;
|
import be.digitalia.fosdem.adapters.EventsAdapter;
|
||||||
import be.digitalia.fosdem.db.DatabaseManager;
|
import be.digitalia.fosdem.model.StatusEvent;
|
||||||
import be.digitalia.fosdem.loaders.SimpleCursorLoader;
|
import be.digitalia.fosdem.viewmodels.SearchViewModel;
|
||||||
|
|
||||||
public class SearchResultListFragment extends RecyclerViewFragment implements LoaderCallbacks<Cursor> {
|
public class SearchResultListFragment extends RecyclerViewFragment implements Observer<PagedList<StatusEvent>> {
|
||||||
|
|
||||||
private static final int EVENTS_LOADER_ID = 1;
|
|
||||||
private static final String ARG_QUERY = "query";
|
|
||||||
|
|
||||||
private EventsAdapter adapter;
|
private EventsAdapter adapter;
|
||||||
|
|
||||||
public static SearchResultListFragment newInstance(String query) {
|
public static SearchResultListFragment newInstance() {
|
||||||
SearchResultListFragment f = new SearchResultListFragment();
|
return new SearchResultListFragment();
|
||||||
Bundle args = new Bundle();
|
|
||||||
args.putString(ARG_QUERY, query);
|
|
||||||
f.setArguments(args);
|
|
||||||
return f;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onCreate(Bundle savedInstanceState) {
|
public void onCreate(Bundle savedInstanceState) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
adapter = new EventsAdapter(getActivity(), this);
|
adapter = new EventsAdapter(getContext(), this);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -51,42 +40,13 @@ public class SearchResultListFragment extends RecyclerViewFragment implements Lo
|
||||||
setEmptyText(getString(R.string.no_search_result));
|
setEmptyText(getString(R.string.no_search_result));
|
||||||
setProgressBarVisible(true);
|
setProgressBarVisible(true);
|
||||||
|
|
||||||
LoaderManager.getInstance(this).initLoader(EVENTS_LOADER_ID, null, this);
|
final SearchViewModel viewModel = ViewModelProviders.of(getActivity()).get(SearchViewModel.class);
|
||||||
}
|
viewModel.getResults().observe(getViewLifecycleOwner(), this);
|
||||||
|
|
||||||
private static class TextSearchLoader extends SimpleCursorLoader {
|
|
||||||
|
|
||||||
private final String query;
|
|
||||||
|
|
||||||
public TextSearchLoader(Context context, String query) {
|
|
||||||
super(context);
|
|
||||||
this.query = query;
|
|
||||||
}
|
|
||||||
|
|
||||||
@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
|
@Override
|
||||||
public void onLoadFinished(@NonNull Loader<Cursor> loader, Cursor data) {
|
public void onChanged(PagedList<StatusEvent> results) {
|
||||||
if (data != null) {
|
adapter.submitList(results);
|
||||||
adapter.swapCursor(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
setProgressBarVisible(false);
|
setProgressBarVisible(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onLoaderReset(@NonNull Loader<Cursor> loader) {
|
|
||||||
adapter.swapCursor(null);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,29 +1,29 @@
|
||||||
package be.digitalia.fosdem.fragments;
|
package be.digitalia.fosdem.fragments;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.database.Cursor;
|
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.os.Handler;
|
import android.os.Handler;
|
||||||
import android.os.Message;
|
import android.os.Message;
|
||||||
import android.text.format.DateUtils;
|
import android.text.format.DateUtils;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.loader.app.LoaderManager;
|
import androidx.lifecycle.Observer;
|
||||||
import androidx.loader.app.LoaderManager.LoaderCallbacks;
|
import androidx.lifecycle.ViewModelProviders;
|
||||||
import androidx.loader.content.Loader;
|
|
||||||
import androidx.recyclerview.widget.DividerItemDecoration;
|
import androidx.recyclerview.widget.DividerItemDecoration;
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
import androidx.recyclerview.widget.LinearLayoutManager;
|
||||||
import androidx.recyclerview.widget.RecyclerView;
|
import androidx.recyclerview.widget.RecyclerView;
|
||||||
import be.digitalia.fosdem.R;
|
import be.digitalia.fosdem.R;
|
||||||
import be.digitalia.fosdem.adapters.TrackScheduleAdapter;
|
import be.digitalia.fosdem.adapters.TrackScheduleAdapter;
|
||||||
import be.digitalia.fosdem.loaders.TrackScheduleLoader;
|
|
||||||
import be.digitalia.fosdem.model.Day;
|
import be.digitalia.fosdem.model.Day;
|
||||||
import be.digitalia.fosdem.model.Event;
|
import be.digitalia.fosdem.model.Event;
|
||||||
|
import be.digitalia.fosdem.model.StatusEvent;
|
||||||
import be.digitalia.fosdem.model.Track;
|
import be.digitalia.fosdem.model.Track;
|
||||||
|
import be.digitalia.fosdem.viewmodels.TrackScheduleViewModel;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
public class TrackScheduleListFragment extends RecyclerViewFragment
|
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
|
* Interface implemented by container activities
|
||||||
|
@ -32,7 +32,6 @@ public class TrackScheduleListFragment extends RecyclerViewFragment
|
||||||
void onEventSelected(int position, Event event);
|
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 int REFRESH_TIME_WHAT = 1;
|
||||||
private static final long REFRESH_TIME_INTERVAL = DateUtils.MINUTE_IN_MILLIS;
|
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));
|
setEmptyText(getString(R.string.no_data));
|
||||||
setProgressBarVisible(true);
|
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
|
@Override
|
||||||
|
@ -184,47 +186,33 @@ public class TrackScheduleListFragment extends RecyclerViewFragment
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@NonNull
|
|
||||||
@Override
|
@Override
|
||||||
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
|
public void onChanged(List<StatusEvent> schedule) {
|
||||||
Track track = getArguments().getParcelable(ARG_TRACK);
|
adapter.submitList(schedule);
|
||||||
return new TrackScheduleLoader(getActivity(), day, track);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
if (selectionEnabled) {
|
||||||
public void onLoadFinished(@NonNull Loader<Cursor> loader, Cursor data) {
|
int selectedPosition = adapter.getPositionForId(selectedId);
|
||||||
if (data != null) {
|
if (selectedPosition == RecyclerView.NO_POSITION && adapter.getItemCount() > 0) {
|
||||||
adapter.swapCursor(data);
|
// There is no current valid selection, reset to use the first item
|
||||||
|
setSelectedId(adapter.getItemId(0));
|
||||||
if (selectionEnabled) {
|
selectedPosition = 0;
|
||||||
int selectedPosition = adapter.getPositionForId(selectedId);
|
}
|
||||||
if (selectedPosition == RecyclerView.NO_POSITION && adapter.getItemCount() > 0) {
|
|
||||||
// There is no current valid selection, reset to use the first item
|
// Ensure the current selection is visible
|
||||||
setSelectedId(adapter.getItemId(0));
|
if (selectedPosition != RecyclerView.NO_POSITION) {
|
||||||
selectedPosition = 0;
|
getRecyclerView().scrollToPosition(selectedPosition);
|
||||||
}
|
}
|
||||||
|
// Notify the parent of the current selection to synchronize its state
|
||||||
// Ensure the current selection is visible
|
notifyEventSelected(selectedPosition, (selectedPosition == RecyclerView.NO_POSITION) ? null : schedule.get(selectedPosition).getEvent());
|
||||||
if (selectedPosition != RecyclerView.NO_POSITION) {
|
|
||||||
getRecyclerView().scrollToPosition(selectedPosition);
|
} else if (!isListAlreadyShown) {
|
||||||
}
|
final int position = adapter.getPositionForId(selectedId);
|
||||||
// Notify the parent of the current selection to synchronize its state
|
if (position != RecyclerView.NO_POSITION) {
|
||||||
notifyEventSelected(selectedPosition, (selectedPosition == RecyclerView.NO_POSITION) ? null : adapter.getItem(selectedPosition));
|
getRecyclerView().scrollToPosition(position);
|
||||||
|
|
||||||
} else if (!isListAlreadyShown) {
|
|
||||||
final int position = adapter.getPositionForId(selectedId);
|
|
||||||
if (position != RecyclerView.NO_POSITION) {
|
|
||||||
getRecyclerView().scrollToPosition(position);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
isListAlreadyShown = true;
|
|
||||||
}
|
}
|
||||||
|
isListAlreadyShown = true;
|
||||||
|
|
||||||
setProgressBarVisible(false);
|
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.recyclerview.widget.RecyclerView;
|
||||||
import androidx.viewpager.widget.ViewPager;
|
import androidx.viewpager.widget.ViewPager;
|
||||||
import be.digitalia.fosdem.R;
|
import be.digitalia.fosdem.R;
|
||||||
import be.digitalia.fosdem.db.DatabaseManager;
|
import be.digitalia.fosdem.db.AppDatabase;
|
||||||
import be.digitalia.fosdem.model.Day;
|
import be.digitalia.fosdem.model.Day;
|
||||||
|
|
||||||
public class TracksFragment extends Fragment implements RecycledViewPoolProvider, Observer<List<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) {
|
public void onActivityCreated(Bundle savedInstanceState) {
|
||||||
super.onActivityCreated(savedInstanceState);
|
super.onActivityCreated(savedInstanceState);
|
||||||
|
|
||||||
DatabaseManager.getInstance().getDays()
|
AppDatabase.getInstance(getContext()).getScheduleDao().getDays()
|
||||||
.observe(getViewLifecycleOwner(), this);
|
.observe(getViewLifecycleOwner(), this);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,36 +2,31 @@ package be.digitalia.fosdem.fragments;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.database.Cursor;
|
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.view.ViewGroup;
|
import android.view.ViewGroup;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.core.content.ContextCompat;
|
import androidx.core.content.ContextCompat;
|
||||||
import androidx.fragment.app.Fragment;
|
import androidx.fragment.app.Fragment;
|
||||||
import androidx.loader.app.LoaderManager;
|
import androidx.lifecycle.Observer;
|
||||||
import androidx.loader.app.LoaderManager.LoaderCallbacks;
|
import androidx.lifecycle.ViewModelProviders;
|
||||||
import androidx.loader.content.Loader;
|
import androidx.recyclerview.widget.*;
|
||||||
import androidx.recyclerview.widget.DividerItemDecoration;
|
|
||||||
import androidx.recyclerview.widget.LinearLayoutManager;
|
|
||||||
import androidx.recyclerview.widget.RecyclerView;
|
|
||||||
import be.digitalia.fosdem.R;
|
import be.digitalia.fosdem.R;
|
||||||
import be.digitalia.fosdem.activities.TrackScheduleActivity;
|
import be.digitalia.fosdem.activities.TrackScheduleActivity;
|
||||||
import be.digitalia.fosdem.adapters.RecyclerViewCursorAdapter;
|
import be.digitalia.fosdem.adapters.SimpleItemCallback;
|
||||||
import be.digitalia.fosdem.db.DatabaseManager;
|
|
||||||
import be.digitalia.fosdem.loaders.SimpleCursorLoader;
|
|
||||||
import be.digitalia.fosdem.model.Day;
|
import be.digitalia.fosdem.model.Day;
|
||||||
import be.digitalia.fosdem.model.Track;
|
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";
|
private static final String ARG_DAY = "day";
|
||||||
|
|
||||||
Day day;
|
private Day day;
|
||||||
private TracksAdapter adapter;
|
private TracksAdapter adapter;
|
||||||
|
|
||||||
public static TracksListFragment newInstance(Day day) {
|
public static TracksListFragment newInstance(Day day) {
|
||||||
|
@ -45,8 +40,8 @@ public class TracksListFragment extends RecyclerViewFragment implements LoaderCa
|
||||||
@Override
|
@Override
|
||||||
public void onCreate(Bundle savedInstanceState) {
|
public void onCreate(Bundle savedInstanceState) {
|
||||||
super.onCreate(savedInstanceState);
|
super.onCreate(savedInstanceState);
|
||||||
adapter = new TracksAdapter();
|
|
||||||
day = getArguments().getParcelable(ARG_DAY);
|
day = getArguments().getParcelable(ARG_DAY);
|
||||||
|
adapter = new TracksAdapter(day);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
|
@ -68,77 +63,50 @@ public class TracksListFragment extends RecyclerViewFragment implements LoaderCa
|
||||||
setEmptyText(getString(R.string.no_data));
|
setEmptyText(getString(R.string.no_data));
|
||||||
setProgressBarVisible(true);
|
setProgressBarVisible(true);
|
||||||
|
|
||||||
LoaderManager.getInstance(this).initLoader(TRACKS_LOADER_ID, null, this);
|
final TracksViewModel viewModel = ViewModelProviders.of(this).get(TracksViewModel.class);
|
||||||
}
|
viewModel.setDay(day);
|
||||||
|
viewModel.getTracks().observe(getViewLifecycleOwner(), this);
|
||||||
private static class TracksLoader extends SimpleCursorLoader {
|
|
||||||
|
|
||||||
private final Day day;
|
|
||||||
|
|
||||||
public TracksLoader(Context context, Day day) {
|
|
||||||
super(context);
|
|
||||||
this.day = day;
|
|
||||||
}
|
|
||||||
|
|
||||||
@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
|
@Override
|
||||||
public void onLoadFinished(@NonNull Loader<Cursor> loader, Cursor data) {
|
public void onChanged(List<Track> tracks) {
|
||||||
if (data != null) {
|
adapter.submitList(tracks);
|
||||||
adapter.swapCursor(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
setProgressBarVisible(false);
|
setProgressBarVisible(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
private static class TracksAdapter extends ListAdapter<Track, TrackViewHolder> {
|
||||||
public void onLoaderReset(@NonNull Loader<Cursor> loader) {
|
|
||||||
adapter.swapCursor(null);
|
|
||||||
}
|
|
||||||
|
|
||||||
private class TracksAdapter extends RecyclerViewCursorAdapter<TrackViewHolder> {
|
private static final DiffUtil.ItemCallback<Track> DIFF_CALLBACK = new SimpleItemCallback<Track>() {
|
||||||
|
@Override
|
||||||
|
public boolean areContentsTheSame(@NonNull Track oldItem, @NonNull Track newItem) {
|
||||||
|
return oldItem.getName().equals(newItem.getName())
|
||||||
|
&& oldItem.getType() == newItem.getType();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
private final LayoutInflater inflater;
|
private final Day day;
|
||||||
|
|
||||||
public TracksAdapter() {
|
TracksAdapter(Day day) {
|
||||||
inflater = LayoutInflater.from(getContext());
|
super(DIFF_CALLBACK);
|
||||||
}
|
this.day = day;
|
||||||
|
|
||||||
@Override
|
|
||||||
public Track getItem(int position) {
|
|
||||||
return DatabaseManager.toTrack((Cursor) super.getItem(position));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@NonNull
|
@NonNull
|
||||||
@Override
|
@Override
|
||||||
public TrackViewHolder onCreateViewHolder(@NonNull ViewGroup parent, int viewType) {
|
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);
|
return new TrackViewHolder(view);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onBindViewHolder(TrackViewHolder holder, Cursor cursor) {
|
public void onBindViewHolder(@NonNull TrackViewHolder holder, int position) {
|
||||||
holder.day = day;
|
holder.bind(day, getItem(position));
|
||||||
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()));
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
static class TrackViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener {
|
static class TrackViewHolder extends RecyclerView.ViewHolder implements View.OnClickListener {
|
||||||
TextView name;
|
final TextView name;
|
||||||
TextView type;
|
final TextView type;
|
||||||
|
|
||||||
Day day;
|
Day day;
|
||||||
Track track;
|
Track track;
|
||||||
|
@ -150,6 +118,14 @@ public class TracksListFragment extends RecyclerViewFragment implements LoaderCa
|
||||||
itemView.setOnClickListener(this);
|
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
|
@Override
|
||||||
public void onClick(View view) {
|
public void onClick(View view) {
|
||||||
Context context = view.getContext();
|
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.Parcel;
|
||||||
import android.os.Parcelable;
|
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.DateFormat;
|
||||||
import java.text.SimpleDateFormat;
|
import java.text.SimpleDateFormat;
|
||||||
import java.util.Date;
|
import java.util.Date;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
@Entity(tableName = Day.TABLE_NAME)
|
||||||
import be.digitalia.fosdem.utils.DateUtils;
|
|
||||||
|
|
||||||
public class Day implements Comparable<Day>, Parcelable {
|
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));
|
private static final DateFormat DAY_DATE_FORMAT = DateUtils.withBelgiumTimeZone(new SimpleDateFormat("EEEE", Locale.US));
|
||||||
|
|
||||||
|
@PrimaryKey
|
||||||
private int index;
|
private int index;
|
||||||
|
@TypeConverters({NonNullDateTypeConverters.class})
|
||||||
|
@NonNull
|
||||||
private Date date;
|
private Date date;
|
||||||
|
|
||||||
public Day() {
|
public Day() {
|
||||||
|
@ -29,11 +38,12 @@ public class Day implements Comparable<Day>, Parcelable {
|
||||||
this.index = index;
|
this.index = index;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
public Date getDate() {
|
public Date getDate() {
|
||||||
return date;
|
return date;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setDate(Date date) {
|
public void setDate(@NonNull Date date) {
|
||||||
this.date = date;
|
this.date = date;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -45,6 +55,7 @@ public class Day implements Comparable<Day>, Parcelable {
|
||||||
return DAY_DATE_FORMAT.format(date);
|
return DAY_DATE_FORMAT.format(date);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
@Override
|
@Override
|
||||||
public String toString() {
|
public String toString() {
|
||||||
return getName();
|
return getName();
|
||||||
|
@ -78,7 +89,7 @@ public class Day implements Comparable<Day>, Parcelable {
|
||||||
@Override
|
@Override
|
||||||
public void writeToParcel(Parcel out, int flags) {
|
public void writeToParcel(Parcel out, int flags) {
|
||||||
out.writeInt(index);
|
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>() {
|
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) {
|
Day(Parcel in) {
|
||||||
index = in.readInt();
|
index = in.readInt();
|
||||||
long time = in.readLong();
|
date = new Date(in.readLong());
|
||||||
if (time != 0L) {
|
|
||||||
date = new Date(time);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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.Parcel;
|
||||||
import android.os.Parcelable;
|
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.Date;
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import be.digitalia.fosdem.api.FosdemUrls;
|
|
||||||
import be.digitalia.fosdem.utils.DateUtils;
|
|
||||||
|
|
||||||
public class Event implements Parcelable {
|
public class Event implements Parcelable {
|
||||||
|
|
||||||
private long id;
|
private long id;
|
||||||
|
@Embedded(prefix = "day_")
|
||||||
|
@NonNull
|
||||||
private Day day;
|
private Day day;
|
||||||
|
@ColumnInfo(name = "start_time")
|
||||||
|
@TypeConverters({NullableDateTypeConverters.class})
|
||||||
private Date startTime;
|
private Date startTime;
|
||||||
|
@ColumnInfo(name = "end_time")
|
||||||
|
@TypeConverters({NullableDateTypeConverters.class})
|
||||||
private Date endTime;
|
private Date endTime;
|
||||||
|
@ColumnInfo(name = "room_name")
|
||||||
private String roomName;
|
private String roomName;
|
||||||
private String slug;
|
private String slug;
|
||||||
private String title;
|
private String title;
|
||||||
|
@ColumnInfo(name = "subtitle")
|
||||||
private String subTitle;
|
private String subTitle;
|
||||||
|
@Embedded(prefix = "track_")
|
||||||
|
@NonNull
|
||||||
private Track track;
|
private Track track;
|
||||||
|
@ColumnInfo(name = "abstract")
|
||||||
private String abstractText;
|
private String abstractText;
|
||||||
private String description;
|
private String description;
|
||||||
|
@ColumnInfo(name = "persons")
|
||||||
private String personsSummary;
|
private String personsSummary;
|
||||||
private List<Person> persons; // Optional
|
|
||||||
private List<Link> links; // Optional
|
|
||||||
|
|
||||||
public Event() {
|
public Event() {
|
||||||
}
|
}
|
||||||
|
@ -38,11 +50,12 @@ public class Event implements Parcelable {
|
||||||
this.id = id;
|
this.id = id;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
public Day getDay() {
|
public Day getDay() {
|
||||||
return day;
|
return day;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setDay(Day day) {
|
public void setDay(@NonNull Day day) {
|
||||||
this.day = day;
|
this.day = day;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -112,11 +125,12 @@ public class Event implements Parcelable {
|
||||||
this.subTitle = subTitle;
|
this.subTitle = subTitle;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
public Track getTrack() {
|
public Track getTrack() {
|
||||||
return track;
|
return track;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setTrack(Track track) {
|
public void setTrack(@NonNull Track track) {
|
||||||
this.track = track;
|
this.track = track;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -136,13 +150,11 @@ public class Event implements Parcelable {
|
||||||
this.description = description;
|
this.description = description;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
public String getPersonsSummary() {
|
public String getPersonsSummary() {
|
||||||
if (personsSummary != null) {
|
if (personsSummary != null) {
|
||||||
return personsSummary;
|
return personsSummary;
|
||||||
}
|
}
|
||||||
if (persons != null) {
|
|
||||||
return TextUtils.join(", ", persons);
|
|
||||||
}
|
|
||||||
return "";
|
return "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -150,22 +162,7 @@ public class Event implements Parcelable {
|
||||||
this.personsSummary = personsSummary;
|
this.personsSummary = personsSummary;
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<Person> getPersons() {
|
@NonNull
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String toString() {
|
public String toString() {
|
||||||
return title;
|
return title;
|
||||||
|
@ -205,8 +202,6 @@ public class Event implements Parcelable {
|
||||||
out.writeString(abstractText);
|
out.writeString(abstractText);
|
||||||
out.writeString(description);
|
out.writeString(description);
|
||||||
out.writeString(personsSummary);
|
out.writeString(personsSummary);
|
||||||
out.writeTypedList(persons);
|
|
||||||
out.writeTypedList(links);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public static final Parcelable.Creator<Event> CREATOR = new Parcelable.Creator<Event>() {
|
public static final Parcelable.Creator<Event> CREATOR = new Parcelable.Creator<Event>() {
|
||||||
|
@ -238,7 +233,5 @@ public class Event implements Parcelable {
|
||||||
abstractText = in.readString();
|
abstractText = in.readString();
|
||||||
description = in.readString();
|
description = in.readString();
|
||||||
personsSummary = 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.Parcel;
|
||||||
import android.os.Parcelable;
|
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 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 url;
|
||||||
private String description;
|
private String description;
|
||||||
|
|
||||||
public Link() {
|
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() {
|
public String getUrl() {
|
||||||
return url;
|
return url;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setUrl(String url) {
|
public void setUrl(@NonNull String url) {
|
||||||
this.url = url;
|
this.url = url;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -27,6 +57,7 @@ public class Link implements Parcelable {
|
||||||
this.description = description;
|
this.description = description;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
@Override
|
@Override
|
||||||
public String toString() {
|
public String toString() {
|
||||||
return description;
|
return description;
|
||||||
|
@ -54,6 +85,8 @@ public class Link implements Parcelable {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void writeToParcel(Parcel out, int flags) {
|
public void writeToParcel(Parcel out, int flags) {
|
||||||
|
out.writeLong(id);
|
||||||
|
out.writeLong(eventId);
|
||||||
out.writeString(url);
|
out.writeString(url);
|
||||||
out.writeString(description);
|
out.writeString(description);
|
||||||
}
|
}
|
||||||
|
@ -69,6 +102,8 @@ public class Link implements Parcelable {
|
||||||
};
|
};
|
||||||
|
|
||||||
Link(Parcel in) {
|
Link(Parcel in) {
|
||||||
|
id = in.readLong();
|
||||||
|
eventId = in.readLong();
|
||||||
url = in.readString();
|
url = in.readString();
|
||||||
description = in.readString();
|
description = in.readString();
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,12 +2,22 @@ package be.digitalia.fosdem.model;
|
||||||
|
|
||||||
import android.os.Parcel;
|
import android.os.Parcel;
|
||||||
import android.os.Parcelable;
|
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.api.FosdemUrls;
|
||||||
import be.digitalia.fosdem.utils.StringUtils;
|
import be.digitalia.fosdem.utils.StringUtils;
|
||||||
|
|
||||||
|
@Fts3
|
||||||
|
@Entity(tableName = Person.TABLE_NAME)
|
||||||
public class Person implements Parcelable {
|
public class Person implements Parcelable {
|
||||||
|
|
||||||
|
public static final String TABLE_NAME = "persons";
|
||||||
|
|
||||||
|
@PrimaryKey
|
||||||
|
@ColumnInfo(name = "rowid")
|
||||||
private long id;
|
private long id;
|
||||||
private String name;
|
private String name;
|
||||||
|
|
||||||
|
@ -34,6 +44,7 @@ public class Person implements Parcelable {
|
||||||
return FosdemUrls.getPerson(StringUtils.toSlug(name), year);
|
return FosdemUrls.getPerson(StringUtils.toSlug(name), year);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
@Override
|
@Override
|
||||||
public String toString() {
|
public String toString() {
|
||||||
return name;
|
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.Parcel;
|
||||||
import android.os.Parcelable;
|
import android.os.Parcelable;
|
||||||
|
|
||||||
import androidx.annotation.ColorRes;
|
import androidx.annotation.ColorRes;
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.StringRes;
|
import androidx.annotation.StringRes;
|
||||||
|
import androidx.room.Entity;
|
||||||
|
import androidx.room.Index;
|
||||||
|
import androidx.room.PrimaryKey;
|
||||||
import be.digitalia.fosdem.R;
|
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 class Track implements Parcelable {
|
||||||
|
|
||||||
|
public static final String TABLE_NAME = "tracks";
|
||||||
|
|
||||||
public enum Type {
|
public enum Type {
|
||||||
other(R.string.other, R.color.track_type_other, R.color.track_type_other_dark),
|
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),
|
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;
|
private String name;
|
||||||
|
@NonNull
|
||||||
private Type type;
|
private Type type;
|
||||||
|
|
||||||
public Track() {
|
public Track(@NonNull String name, @NonNull Type type) {
|
||||||
}
|
|
||||||
|
|
||||||
public Track(String name, Type type) {
|
|
||||||
this.name = name;
|
this.name = name;
|
||||||
this.type = type;
|
this.type = type;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public long getId() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setId(long id) {
|
||||||
|
this.id = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
@NonNull
|
||||||
public String getName() {
|
public String getName() {
|
||||||
return name;
|
return name;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setName(String name) {
|
@NonNull
|
||||||
this.name = name;
|
|
||||||
}
|
|
||||||
|
|
||||||
public Type getType() {
|
public Type getType() {
|
||||||
return type;
|
return type;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setType(Type type) {
|
@NonNull
|
||||||
this.type = type;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public String toString() {
|
public String toString() {
|
||||||
return name;
|
return name;
|
||||||
|
@ -101,6 +111,7 @@ public class Track implements Parcelable {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void writeToParcel(Parcel out, int flags) {
|
public void writeToParcel(Parcel out, int flags) {
|
||||||
|
out.writeLong(id);
|
||||||
out.writeString(name);
|
out.writeString(name);
|
||||||
out.writeInt(type.ordinal());
|
out.writeInt(type.ordinal());
|
||||||
}
|
}
|
||||||
|
@ -116,6 +127,7 @@ public class Track implements Parcelable {
|
||||||
};
|
};
|
||||||
|
|
||||||
Track(Parcel in) {
|
Track(Parcel in) {
|
||||||
|
id = in.readLong();
|
||||||
name = in.readString();
|
name = in.readString();
|
||||||
type = Type.values()[in.readInt()];
|
type = Type.values()[in.readInt()];
|
||||||
}
|
}
|
||||||
|
|
|
@ -12,7 +12,7 @@ import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
|
|
||||||
import be.digitalia.fosdem.model.Day;
|
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.Link;
|
||||||
import be.digitalia.fosdem.model.Person;
|
import be.digitalia.fosdem.model.Person;
|
||||||
import be.digitalia.fosdem.model.Track;
|
import be.digitalia.fosdem.model.Track;
|
||||||
|
@ -23,7 +23,7 @@ import be.digitalia.fosdem.utils.DateUtils;
|
||||||
*
|
*
|
||||||
* @author Christophe Beyls
|
* @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));
|
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
|
@Override
|
||||||
protected Event parseNext(XmlPullParser parser) throws Exception {
|
protected DetailedEvent parseNext(XmlPullParser parser) throws Exception {
|
||||||
while (!isNextEndTag("schedule")) {
|
while (!isNextEndTag("schedule")) {
|
||||||
if (isStartTag()) {
|
if (isStartTag()) {
|
||||||
|
|
||||||
|
@ -81,7 +81,7 @@ public class EventsParser extends IterableAbstractPullParser<Event> {
|
||||||
currentRoom = parser.getAttributeValue(null, "name");
|
currentRoom = parser.getAttributeValue(null, "name");
|
||||||
break;
|
break;
|
||||||
case "event":
|
case "event":
|
||||||
Event event = new Event();
|
DetailedEvent event = new DetailedEvent();
|
||||||
event.setId(Long.parseLong(parser.getAttributeValue(null, "id")));
|
event.setId(Long.parseLong(parser.getAttributeValue(null, "id")));
|
||||||
event.setDay(currentDay);
|
event.setDay(currentDay);
|
||||||
event.setRoomName(currentRoom);
|
event.setRoomName(currentRoom);
|
||||||
|
@ -151,6 +151,7 @@ public class EventsParser extends IterableAbstractPullParser<Event> {
|
||||||
while (!isNextEndTag("links")) {
|
while (!isNextEndTag("links")) {
|
||||||
if (isStartTag("link")) {
|
if (isStartTag("link")) {
|
||||||
Link link = new Link();
|
Link link = new Link();
|
||||||
|
link.setEventId(event.getId());
|
||||||
link.setUrl(parser.getAttributeValue(null, "href"));
|
link.setUrl(parser.getAttributeValue(null, "href"));
|
||||||
link.setDescription(parser.nextText());
|
link.setDescription(parser.nextText());
|
||||||
|
|
||||||
|
|
|
@ -11,30 +11,25 @@ import android.os.Build;
|
||||||
import android.os.ParcelFileDescriptor;
|
import android.os.ParcelFileDescriptor;
|
||||||
import android.provider.OpenableColumns;
|
import android.provider.OpenableColumns;
|
||||||
import android.text.TextUtils;
|
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.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.core.app.ShareCompat;
|
import androidx.core.app.ShareCompat;
|
||||||
import be.digitalia.fosdem.BuildConfig;
|
import be.digitalia.fosdem.BuildConfig;
|
||||||
import be.digitalia.fosdem.R;
|
import be.digitalia.fosdem.R;
|
||||||
import be.digitalia.fosdem.api.FosdemUrls;
|
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.model.Event;
|
||||||
import be.digitalia.fosdem.utils.DateUtils;
|
import be.digitalia.fosdem.utils.DateUtils;
|
||||||
import be.digitalia.fosdem.utils.ICalendarWriter;
|
import be.digitalia.fosdem.utils.ICalendarWriter;
|
||||||
import be.digitalia.fosdem.utils.StringUtils;
|
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.
|
* Content Provider generating the current bookmarks list in iCalendar format.
|
||||||
*/
|
*/
|
||||||
|
@ -105,7 +100,7 @@ public class BookmarksExportProvider extends ContentProvider {
|
||||||
for (String col : projection) {
|
for (String col : projection) {
|
||||||
if (OpenableColumns.DISPLAY_NAME.equals(col)) {
|
if (OpenableColumns.DISPLAY_NAME.equals(col)) {
|
||||||
cols[i] = OpenableColumns.DISPLAY_NAME;
|
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)) {
|
} else if (OpenableColumns.SIZE.equals(col)) {
|
||||||
cols[i] = OpenableColumns.SIZE;
|
cols[i] = OpenableColumns.SIZE;
|
||||||
// Unknown size, content will be generated on-the-fly
|
// 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 {
|
public ParcelFileDescriptor openFile(@NonNull Uri uri, @NonNull String mode) throws FileNotFoundException {
|
||||||
try {
|
try {
|
||||||
ParcelFileDescriptor[] pipe = ParcelFileDescriptor.createPipe();
|
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];
|
return pipe[0];
|
||||||
} catch (IOException e) {
|
} catch (IOException e) {
|
||||||
throw new FileNotFoundException("Could not open pipe");
|
throw new FileNotFoundException("Could not open pipe");
|
||||||
|
@ -149,15 +147,17 @@ public class BookmarksExportProvider extends ContentProvider {
|
||||||
static class DownloadThread extends Thread {
|
static class DownloadThread extends Thread {
|
||||||
private final ICalendarWriter writer;
|
private final ICalendarWriter writer;
|
||||||
|
|
||||||
|
private final AppDatabase appDatabase;
|
||||||
private final Calendar calendar = Calendar.getInstance(DateUtils.getBelgiumTimeZone(), Locale.US);
|
private final Calendar calendar = Calendar.getInstance(DateUtils.getBelgiumTimeZone(), Locale.US);
|
||||||
private final DateFormat dateFormat;
|
private final DateFormat dateFormat;
|
||||||
private final String dtStamp;
|
private final String dtStamp;
|
||||||
private final TextUtils.StringSplitter personsSplitter = new StringUtils.SimpleStringSplitter(", ");
|
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)));
|
this.writer = new ICalendarWriter(new BufferedWriter(new OutputStreamWriter(out)));
|
||||||
|
|
||||||
// Format all times in GMT
|
// Format all times in GMT
|
||||||
|
this.appDatabase = appDatabase;
|
||||||
this.dateFormat = new SimpleDateFormat("yyyyMMdd'T'HHmmss'Z'", Locale.US);
|
this.dateFormat = new SimpleDateFormat("yyyyMMdd'T'HHmmss'Z'", Locale.US);
|
||||||
this.dateFormat.setTimeZone(TimeZone.getTimeZone("GMT+0"));
|
this.dateFormat.setTimeZone(TimeZone.getTimeZone("GMT+0"));
|
||||||
this.dtStamp = dateFormat.format(System.currentTimeMillis());
|
this.dtStamp = dateFormat.format(System.currentTimeMillis());
|
||||||
|
@ -166,23 +166,16 @@ public class BookmarksExportProvider extends ContentProvider {
|
||||||
@Override
|
@Override
|
||||||
public void run() {
|
public void run() {
|
||||||
try {
|
try {
|
||||||
final Cursor cursor = DatabaseManager.getInstance().getBookmarks(0L);
|
final Event[] bookmarks = appDatabase.getBookmarksDao().getBookmarks();
|
||||||
try {
|
writer.write("BEGIN", "VCALENDAR");
|
||||||
writer.write("BEGIN", "VCALENDAR");
|
writer.write("VERSION", "2.0");
|
||||||
writer.write("VERSION", "2.0");
|
writer.write("PRODID", "-//" + BuildConfig.APPLICATION_ID + "//NONSGML " + BuildConfig.VERSION_NAME + "//EN");
|
||||||
writer.write("PRODID", "-//" + BuildConfig.APPLICATION_ID + "//NONSGML " + BuildConfig.VERSION_NAME + "//EN");
|
|
||||||
|
|
||||||
Event event = null;
|
for (Event event : bookmarks) {
|
||||||
while (cursor.moveToNext()) {
|
writeEvent(event);
|
||||||
event = DatabaseManager.toEvent(cursor, event);
|
|
||||||
writeEvent(event);
|
|
||||||
}
|
|
||||||
|
|
||||||
writer.write("END", "VCALENDAR");
|
|
||||||
|
|
||||||
} finally {
|
|
||||||
cursor.close();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
writer.write("END", "VCALENDAR");
|
||||||
} catch (Exception ignore) {
|
} catch (Exception ignore) {
|
||||||
} finally {
|
} finally {
|
||||||
try {
|
try {
|
||||||
|
|
|
@ -6,13 +6,12 @@ import android.content.ContentValues;
|
||||||
import android.database.Cursor;
|
import android.database.Cursor;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import be.digitalia.fosdem.db.DatabaseManager;
|
import be.digitalia.fosdem.db.AppDatabase;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Simple content provider responsible for search suggestions.
|
* Simple content provider responsible for search suggestions.
|
||||||
*
|
*
|
||||||
* @author Christophe Beyls
|
* @author Christophe Beyls
|
||||||
*/
|
*/
|
||||||
public class SearchSuggestionProvider extends ContentProvider {
|
public class SearchSuggestionProvider extends ContentProvider {
|
||||||
|
@ -60,6 +59,6 @@ public class SearchSuggestionProvider extends ContentProvider {
|
||||||
String limitParam = uri.getQueryParameter("limit");
|
String limitParam = uri.getQueryParameter("limit");
|
||||||
int limit = TextUtils.isEmpty(limitParam) ? DEFAULT_MAX_RESULTS : Integer.parseInt(limitParam);
|
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;
|
package be.digitalia.fosdem.services;
|
||||||
|
|
||||||
import android.app.AlarmManager;
|
import android.app.*;
|
||||||
import android.app.Notification;
|
|
||||||
import android.app.NotificationChannel;
|
|
||||||
import android.app.NotificationManager;
|
|
||||||
import android.app.PendingIntent;
|
|
||||||
import android.content.ComponentName;
|
import android.content.ComponentName;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.content.Intent;
|
||||||
import android.content.SharedPreferences;
|
import android.content.SharedPreferences;
|
||||||
import android.content.pm.PackageManager;
|
import android.content.pm.PackageManager;
|
||||||
import android.database.Cursor;
|
|
||||||
import android.graphics.Typeface;
|
import android.graphics.Typeface;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
|
@ -20,13 +15,9 @@ import android.text.SpannableString;
|
||||||
import android.text.TextUtils;
|
import android.text.TextUtils;
|
||||||
import android.text.format.DateUtils;
|
import android.text.format.DateUtils;
|
||||||
import android.text.style.StyleSpan;
|
import android.text.style.StyleSpan;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.RequiresApi;
|
import androidx.annotation.RequiresApi;
|
||||||
import androidx.core.app.AlarmManagerCompat;
|
import androidx.core.app.*;
|
||||||
import androidx.core.app.JobIntentService;
|
|
||||||
import androidx.core.app.NotificationCompat;
|
|
||||||
import androidx.core.app.NotificationManagerCompat;
|
|
||||||
import androidx.core.app.TaskStackBuilder;
|
import androidx.core.app.TaskStackBuilder;
|
||||||
import androidx.core.content.ContextCompat;
|
import androidx.core.content.ContextCompat;
|
||||||
import androidx.preference.PreferenceManager;
|
import androidx.preference.PreferenceManager;
|
||||||
|
@ -35,8 +26,9 @@ import be.digitalia.fosdem.R;
|
||||||
import be.digitalia.fosdem.activities.EventDetailsActivity;
|
import be.digitalia.fosdem.activities.EventDetailsActivity;
|
||||||
import be.digitalia.fosdem.activities.MainActivity;
|
import be.digitalia.fosdem.activities.MainActivity;
|
||||||
import be.digitalia.fosdem.activities.RoomImageDialogActivity;
|
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.fragments.SettingsFragment;
|
||||||
|
import be.digitalia.fosdem.model.AlarmInfo;
|
||||||
import be.digitalia.fosdem.model.Event;
|
import be.digitalia.fosdem.model.Event;
|
||||||
import be.digitalia.fosdem.receivers.AlarmReceiver;
|
import be.digitalia.fosdem.receivers.AlarmReceiver;
|
||||||
import be.digitalia.fosdem.utils.StringUtils;
|
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_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_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;
|
private AlarmManager alarmManager;
|
||||||
|
|
||||||
|
@ -89,23 +87,16 @@ public class AlarmIntentService extends JobIntentService {
|
||||||
final long delay = getDelay();
|
final long delay = getDelay();
|
||||||
final long now = System.currentTimeMillis();
|
final long now = System.currentTimeMillis();
|
||||||
boolean hasAlarms = false;
|
boolean hasAlarms = false;
|
||||||
Cursor cursor = DatabaseManager.getInstance().getBookmarks(0L);
|
for (AlarmInfo info : AppDatabase.getInstance(this).getBookmarksDao().getBookmarksAlarmInfo(0L)) {
|
||||||
try {
|
final long notificationTime = info.getStartTime() == null ? -1L : info.getStartTime().getTime() - delay;
|
||||||
while (cursor.moveToNext()) {
|
PendingIntent pi = getAlarmPendingIntent(info.getEventId());
|
||||||
long eventId = DatabaseManager.toEventId(cursor);
|
if (notificationTime < now) {
|
||||||
long notificationTime = DatabaseManager.toEventStartTimeMillis(cursor) - delay;
|
// Cancel pending alarms that are now scheduled in the past, if any
|
||||||
PendingIntent pi = getAlarmPendingIntent(eventId);
|
alarmManager.cancel(pi);
|
||||||
if (notificationTime < now) {
|
} else {
|
||||||
// Cancel pending alarms that are now scheduled in the past, if any
|
AlarmManagerCompat.setExact(alarmManager, AlarmManager.RTC_WAKEUP, notificationTime, pi);
|
||||||
alarmManager.cancel(pi);
|
hasAlarms = true;
|
||||||
} else {
|
|
||||||
AlarmManagerCompat.setExact(alarmManager, AlarmManager.RTC_WAKEUP, notificationTime, pi);
|
|
||||||
hasAlarms = true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
} finally {
|
|
||||||
cursor.close();
|
|
||||||
}
|
}
|
||||||
setAlarmReceiverEnabled(hasAlarms);
|
setAlarmReceiverEnabled(hasAlarms);
|
||||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && hasAlarms) {
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O && hasAlarms) {
|
||||||
|
@ -117,24 +108,18 @@ public class AlarmIntentService extends JobIntentService {
|
||||||
case ACTION_DISABLE_ALARMS: {
|
case ACTION_DISABLE_ALARMS: {
|
||||||
|
|
||||||
// Cancel alarms of every bookmark in the future
|
// Cancel alarms of every bookmark in the future
|
||||||
Cursor cursor = DatabaseManager.getInstance().getBookmarks(System.currentTimeMillis());
|
for (AlarmInfo info : AppDatabase.getInstance(this).getBookmarksDao().getBookmarksAlarmInfo(System.currentTimeMillis())) {
|
||||||
try {
|
alarmManager.cancel(getAlarmPendingIntent(info.getEventId()));
|
||||||
while (cursor.moveToNext()) {
|
|
||||||
long eventId = DatabaseManager.toEventId(cursor);
|
|
||||||
alarmManager.cancel(getAlarmPendingIntent(eventId));
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
cursor.close();
|
|
||||||
}
|
}
|
||||||
setAlarmReceiverEnabled(false);
|
setAlarmReceiverEnabled(false);
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case DatabaseManager.ACTION_ADD_BOOKMARK: {
|
case ACTION_ADD_BOOKMARK: {
|
||||||
|
|
||||||
long delay = getDelay();
|
long delay = getDelay();
|
||||||
long eventId = intent.getLongExtra(DatabaseManager.EXTRA_EVENT_ID, -1L);
|
long eventId = intent.getLongExtra(EXTRA_EVENT_ID, -1L);
|
||||||
long startTime = intent.getLongExtra(DatabaseManager.EXTRA_EVENT_START_TIME, -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
|
// Only schedule future events. If they start before the delay, the alarm will go off immediately
|
||||||
if ((startTime == -1L) || (startTime < System.currentTimeMillis())) {
|
if ((startTime == -1L) || (startTime < System.currentTimeMillis())) {
|
||||||
break;
|
break;
|
||||||
|
@ -147,10 +132,10 @@ public class AlarmIntentService extends JobIntentService {
|
||||||
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case DatabaseManager.ACTION_REMOVE_BOOKMARKS: {
|
case ACTION_REMOVE_BOOKMARKS: {
|
||||||
|
|
||||||
// Cancel matching alarms, might they exist or not
|
// 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) {
|
for (long eventId : eventIds) {
|
||||||
alarmManager.cancel(getAlarmPendingIntent(eventId));
|
alarmManager.cancel(getAlarmPendingIntent(eventId));
|
||||||
}
|
}
|
||||||
|
@ -160,7 +145,7 @@ public class AlarmIntentService extends JobIntentService {
|
||||||
case AlarmReceiver.ACTION_NOTIFY_EVENT: {
|
case AlarmReceiver.ACTION_NOTIFY_EVENT: {
|
||||||
|
|
||||||
long eventId = Long.parseLong(intent.getDataString());
|
long eventId = Long.parseLong(intent.getDataString());
|
||||||
Event event = DatabaseManager.getInstance().getEvent(eventId);
|
Event event = AppDatabase.getInstance(this).getScheduleDao().getEvent(eventId);
|
||||||
if (event != null) {
|
if (event != null) {
|
||||||
NotificationManagerCompat.from(this).notify((int) eventId, buildNotification(event));
|
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;
|
package be.digitalia.fosdem.viewmodels;
|
||||||
|
|
||||||
import android.app.Application;
|
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 android.os.AsyncTask;
|
||||||
|
|
||||||
import java.util.List;
|
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.arch.core.util.Function;
|
||||||
import androidx.lifecycle.AndroidViewModel;
|
import androidx.lifecycle.AndroidViewModel;
|
||||||
import androidx.lifecycle.LiveData;
|
import androidx.lifecycle.LiveData;
|
||||||
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
|
import androidx.lifecycle.MutableLiveData;
|
||||||
import be.digitalia.fosdem.db.DatabaseManager;
|
import androidx.lifecycle.Transformations;
|
||||||
import be.digitalia.fosdem.livedata.AsyncTaskLiveData;
|
import be.digitalia.fosdem.db.AppDatabase;
|
||||||
|
import be.digitalia.fosdem.livedata.ExtraTransformations;
|
||||||
import be.digitalia.fosdem.model.Event;
|
import be.digitalia.fosdem.model.Event;
|
||||||
import be.digitalia.fosdem.model.Link;
|
import be.digitalia.fosdem.model.EventDetails;
|
||||||
import be.digitalia.fosdem.model.Person;
|
|
||||||
import be.digitalia.fosdem.utils.ArrayUtils;
|
|
||||||
|
|
||||||
public class EventDetailsViewModel extends AndroidViewModel {
|
public class EventDetailsViewModel extends AndroidViewModel {
|
||||||
|
|
||||||
public static class EventDetails {
|
private final AppDatabase appDatabase = AppDatabase.getInstance(getApplication());
|
||||||
public List<Person> persons;
|
private final MutableLiveData<Event> event = new MutableLiveData<>();
|
||||||
public List<Link> links;
|
private final LiveData<Boolean> bookmarkStatus = Transformations.switchMap(event,
|
||||||
}
|
new Function<Event, LiveData<Boolean>>() {
|
||||||
|
@Override
|
||||||
private Event event = null;
|
public LiveData<Boolean> apply(Event event) {
|
||||||
|
// Prevent animating the UI when a bookmark is added back or removed back
|
||||||
private final AsyncTaskLiveData<Boolean> bookmarkStatus = new AsyncTaskLiveData<Boolean>() {
|
return ExtraTransformations.distinctUntilChanged(
|
||||||
|
appDatabase.getBookmarksDao().getBookmarkStatus(event)
|
||||||
@Override
|
);
|
||||||
protected Boolean loadInBackground() throws Exception {
|
}
|
||||||
return DatabaseManager.getInstance().isBookmarked(event);
|
});
|
||||||
}
|
private final LiveData<EventDetails> eventDetails = Transformations.switchMap(event,
|
||||||
};
|
new Function<Event, LiveData<EventDetails>>() {
|
||||||
private final AsyncTaskLiveData<EventDetails> eventDetails = new AsyncTaskLiveData<EventDetails>() {
|
@Override
|
||||||
|
public LiveData<EventDetails> apply(Event event) {
|
||||||
@Override
|
return appDatabase.getScheduleDao().getEventDetails(event);
|
||||||
protected EventDetails loadInBackground() throws Exception {
|
}
|
||||||
EventDetails result = new EventDetails();
|
});
|
||||||
DatabaseManager dbm = DatabaseManager.getInstance();
|
|
||||||
result.persons = dbm.getPersons(event);
|
|
||||||
result.links = dbm.getLinks(event);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
private final BroadcastReceiver addBookmarkReceiver = new BroadcastReceiver() {
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onReceive(Context context, Intent intent) {
|
|
||||||
if (event.getId() == intent.getLongExtra(DatabaseManager.EXTRA_EVENT_ID, -1L)) {
|
|
||||||
bookmarkStatus.setValue(true);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
private final BroadcastReceiver removeBookmarksReceiver = new BroadcastReceiver() {
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public void onReceive(Context context, Intent intent) {
|
|
||||||
long[] eventIds = intent.getLongArrayExtra(DatabaseManager.EXTRA_EVENT_IDS);
|
|
||||||
if (ArrayUtils.indexOf(eventIds, event.getId()) != -1) {
|
|
||||||
bookmarkStatus.setValue(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
public EventDetailsViewModel(@NonNull Application application) {
|
public EventDetailsViewModel(@NonNull Application application) {
|
||||||
super(application);
|
super(application);
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setEvent(@NonNull Event event) {
|
public void setEvent(@NonNull Event event) {
|
||||||
if (this.event == null) {
|
if (!event.equals(this.event.getValue())) {
|
||||||
this.event = event;
|
this.event.setValue(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();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -90,39 +50,23 @@ public class EventDetailsViewModel extends AndroidViewModel {
|
||||||
}
|
}
|
||||||
|
|
||||||
public void toggleBookmarkStatus() {
|
public void toggleBookmarkStatus() {
|
||||||
Boolean isBookmarked = bookmarkStatus.getValue();
|
final Event event = this.event.getValue();
|
||||||
if (isBookmarked != null) {
|
final Boolean isBookmarked = bookmarkStatus.getValue();
|
||||||
new ToggleBookmarkAsyncTask(event).execute(isBookmarked);
|
if (event != null && isBookmarked != null) {
|
||||||
}
|
AsyncTask.SERIAL_EXECUTOR.execute(new Runnable() {
|
||||||
}
|
@Override
|
||||||
|
public void run() {
|
||||||
private static class ToggleBookmarkAsyncTask extends AsyncTask<Boolean, Void, Void> {
|
if (isBookmarked) {
|
||||||
|
appDatabase.getBookmarksDao().removeBookmark(event);
|
||||||
private final Event event;
|
} else {
|
||||||
|
appDatabase.getBookmarksDao().addBookmark(event);
|
||||||
public ToggleBookmarkAsyncTask(Event event) {
|
}
|
||||||
this.event = event;
|
}
|
||||||
}
|
});
|
||||||
|
|
||||||
@Override
|
|
||||||
protected Void doInBackground(Boolean... remove) {
|
|
||||||
if (remove[0]) {
|
|
||||||
DatabaseManager.getInstance().removeBookmark(event);
|
|
||||||
} else {
|
|
||||||
DatabaseManager.getInstance().addBookmark(event);
|
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public LiveData<EventDetails> getEventDetails() {
|
public LiveData<EventDetails> getEventDetails() {
|
||||||
return eventDetails;
|
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;
|
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.LiveData;
|
||||||
import androidx.lifecycle.ViewModel;
|
import androidx.lifecycle.MutableLiveData;
|
||||||
import be.digitalia.fosdem.db.DatabaseManager;
|
import androidx.lifecycle.Transformations;
|
||||||
import be.digitalia.fosdem.livedata.AsyncTaskLiveData;
|
import be.digitalia.fosdem.db.AppDatabase;
|
||||||
import be.digitalia.fosdem.model.Event;
|
import be.digitalia.fosdem.model.Event;
|
||||||
|
|
||||||
public class EventViewModel extends ViewModel {
|
public class EventViewModel extends AndroidViewModel {
|
||||||
|
|
||||||
private long eventId = -1L;
|
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
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
private final AsyncTaskLiveData<Event> event = new AsyncTaskLiveData<Event>() {
|
public EventViewModel(@NonNull Application application) {
|
||||||
@Override
|
super(application);
|
||||||
protected Event loadInBackground() throws Exception {
|
}
|
||||||
return DatabaseManager.getInstance().getEvent(eventId);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
public boolean hasEventId() {
|
public boolean hasEventId() {
|
||||||
return this.eventId != -1L;
|
return this.eventId.getValue() != null;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setEventId(long eventId) {
|
public void setEventId(long eventId) {
|
||||||
if (this.eventId != eventId) {
|
Long newEventId = eventId;
|
||||||
this.eventId = eventId;
|
if (!newEventId.equals(this.eventId.getValue())) {
|
||||||
event.forceLoad();
|
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;
|
package be.digitalia.fosdem.widgets;
|
||||||
|
|
||||||
import android.content.Context;
|
|
||||||
import android.os.Parcel;
|
import android.os.Parcel;
|
||||||
import android.os.Parcelable;
|
import android.os.Parcelable;
|
||||||
import android.util.SparseBooleanArray;
|
import android.util.SparseBooleanArray;
|
||||||
|
@ -8,7 +7,6 @@ import android.view.Menu;
|
||||||
import android.view.MenuItem;
|
import android.view.MenuItem;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.widget.Checkable;
|
import android.widget.Checkable;
|
||||||
|
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.appcompat.app.AppCompatActivity;
|
import androidx.appcompat.app.AppCompatActivity;
|
||||||
import androidx.appcompat.view.ActionMode;
|
import androidx.appcompat.view.ActionMode;
|
||||||
|
@ -31,18 +29,18 @@ public class MultiChoiceHelper {
|
||||||
public static abstract class ViewHolder extends RecyclerView.ViewHolder {
|
public static abstract class ViewHolder extends RecyclerView.ViewHolder {
|
||||||
|
|
||||||
View.OnClickListener clickListener;
|
View.OnClickListener clickListener;
|
||||||
MultiChoiceHelper multiChoiceHelper;
|
final MultiChoiceHelper multiChoiceHelper;
|
||||||
|
|
||||||
public ViewHolder(View itemView) {
|
public ViewHolder(@NonNull View itemView, @NonNull MultiChoiceHelper helper) {
|
||||||
super(itemView);
|
super(itemView);
|
||||||
|
multiChoiceHelper = helper;
|
||||||
itemView.setOnClickListener(new View.OnClickListener() {
|
itemView.setOnClickListener(new View.OnClickListener() {
|
||||||
@Override
|
@Override
|
||||||
public void onClick(View view) {
|
public void onClick(View view) {
|
||||||
if (isMultiChoiceActive()) {
|
if (isMultiChoiceActive()) {
|
||||||
int position = getAdapterPosition();
|
int position = getAdapterPosition();
|
||||||
if (position != RecyclerView.NO_POSITION) {
|
if (position != RecyclerView.NO_POSITION) {
|
||||||
multiChoiceHelper.toggleItemChecked(position, false);
|
multiChoiceHelper.toggleItemChecked(position);
|
||||||
updateCheckedState(position);
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (clickListener != null) {
|
if (clickListener != null) {
|
||||||
|
@ -54,41 +52,36 @@ public class MultiChoiceHelper {
|
||||||
itemView.setOnLongClickListener(new View.OnLongClickListener() {
|
itemView.setOnLongClickListener(new View.OnLongClickListener() {
|
||||||
@Override
|
@Override
|
||||||
public boolean onLongClick(View view) {
|
public boolean onLongClick(View view) {
|
||||||
if ((multiChoiceHelper == null) || isMultiChoiceActive()) {
|
if (isMultiChoiceActive()) {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
int position = getAdapterPosition();
|
int position = getAdapterPosition();
|
||||||
if (position != RecyclerView.NO_POSITION) {
|
if (position != RecyclerView.NO_POSITION) {
|
||||||
multiChoiceHelper.setItemChecked(position, true, false);
|
multiChoiceHelper.setItemChecked(position, true);
|
||||||
updateCheckedState(position);
|
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void updateCheckedState(int position) {
|
|
||||||
final boolean isChecked = multiChoiceHelper.isItemChecked(position);
|
|
||||||
if (itemView instanceof Checkable) {
|
|
||||||
((Checkable) itemView).setChecked(isChecked);
|
|
||||||
} else {
|
|
||||||
itemView.setActivated(isChecked);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setOnClickListener(View.OnClickListener clickListener) {
|
public void setOnClickListener(View.OnClickListener clickListener) {
|
||||||
this.clickListener = clickListener;
|
this.clickListener = clickListener;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void bind(MultiChoiceHelper multiChoiceHelper, int position) {
|
public void bindSelection() {
|
||||||
this.multiChoiceHelper = multiChoiceHelper;
|
int position = getAdapterPosition();
|
||||||
if (multiChoiceHelper != null) {
|
if (position != RecyclerView.NO_POSITION) {
|
||||||
updateCheckedState(position);
|
final boolean isChecked = multiChoiceHelper.isItemChecked(position);
|
||||||
|
if (itemView instanceof Checkable) {
|
||||||
|
((Checkable) itemView).setChecked(isChecked);
|
||||||
|
} else {
|
||||||
|
itemView.setActivated(isChecked);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public boolean isMultiChoiceActive() {
|
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);
|
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 static final int CHECK_POSITION_SEARCH_DISTANCE = 20;
|
||||||
|
|
||||||
private final AppCompatActivity activity;
|
private final AppCompatActivity activity;
|
||||||
|
@ -129,10 +124,6 @@ public class MultiChoiceHelper {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public Context getContext() {
|
|
||||||
return activity;
|
|
||||||
}
|
|
||||||
|
|
||||||
public void setMultiChoiceModeListener(MultiChoiceModeListener listener) {
|
public void setMultiChoiceModeListener(MultiChoiceModeListener listener) {
|
||||||
if (listener == null) {
|
if (listener == null) {
|
||||||
multiChoiceModeCallback = null;
|
multiChoiceModeCallback = null;
|
||||||
|
@ -182,7 +173,7 @@ public class MultiChoiceHelper {
|
||||||
}
|
}
|
||||||
checkedItemCount = 0;
|
checkedItemCount = 0;
|
||||||
|
|
||||||
adapter.notifyItemRangeChanged(start, end - start + 1);
|
adapter.notifyItemRangeChanged(start, end - start + 1, SELECTION_PAYLOAD);
|
||||||
|
|
||||||
if (choiceActionMode != null) {
|
if (choiceActionMode != null) {
|
||||||
choiceActionMode.finish();
|
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.
|
// Start selection mode if needed. We don't need to if we're unchecking something.
|
||||||
if (value) {
|
if (value) {
|
||||||
startSupportActionModeIfNeeded();
|
startSupportActionModeIfNeeded();
|
||||||
|
@ -216,9 +207,7 @@ public class MultiChoiceHelper {
|
||||||
checkedItemCount--;
|
checkedItemCount--;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (notifyChanged) {
|
adapter.notifyItemChanged(position, SELECTION_PAYLOAD);
|
||||||
adapter.notifyItemChanged(position);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (choiceActionMode != null) {
|
if (choiceActionMode != null) {
|
||||||
multiChoiceModeCallback.onItemCheckedStateChanged(choiceActionMode, position, id, value);
|
multiChoiceModeCallback.onItemCheckedStateChanged(choiceActionMode, position, id, value);
|
||||||
|
@ -229,8 +218,8 @@ public class MultiChoiceHelper {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public void toggleItemChecked(int position, boolean notifyChanged) {
|
public void toggleItemChecked(int position) {
|
||||||
setItemChecked(position, !isItemChecked(position), notifyChanged);
|
setItemChecked(position, !isItemChecked(position));
|
||||||
}
|
}
|
||||||
|
|
||||||
public Parcelable onSaveInstanceState() {
|
public Parcelable onSaveInstanceState() {
|
||||||
|
|
Loading…
Reference in a new issue