mirror of
https://github.com/MatomoCamp/matomocamp-companion-android.git
synced 2024-09-19 16:13:46 +02:00
Use a consumable LiveData event to report the schedule download result to UI in place of LocalBroadcastManager
Use a simple AtomicBoolean instead of a lock to prevent concurrent downloads
This commit is contained in:
parent
2a7b1439cc
commit
7e15f7de55
4 changed files with 131 additions and 87 deletions
|
@ -4,17 +4,10 @@ import android.animation.Animator;
|
||||||
import android.animation.AnimatorListenerAdapter;
|
import android.animation.AnimatorListenerAdapter;
|
||||||
import android.app.Dialog;
|
import android.app.Dialog;
|
||||||
import android.app.SearchManager;
|
import android.app.SearchManager;
|
||||||
import android.content.ActivityNotFoundException;
|
import android.content.*;
|
||||||
import android.content.BroadcastReceiver;
|
|
||||||
import android.content.Context;
|
|
||||||
import android.content.DialogInterface;
|
|
||||||
import android.content.Intent;
|
|
||||||
import android.content.IntentFilter;
|
|
||||||
import android.content.SharedPreferences;
|
|
||||||
import android.graphics.drawable.Animatable;
|
import android.graphics.drawable.Animatable;
|
||||||
import android.graphics.drawable.Drawable;
|
import android.graphics.drawable.Drawable;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.AsyncTask;
|
|
||||||
import android.os.Build;
|
import android.os.Build;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
import android.text.format.DateUtils;
|
import android.text.format.DateUtils;
|
||||||
|
@ -23,10 +16,6 @@ import android.view.MenuItem;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
import android.widget.ProgressBar;
|
import android.widget.ProgressBar;
|
||||||
import android.widget.TextView;
|
import android.widget.TextView;
|
||||||
|
|
||||||
import com.google.android.material.navigation.NavigationView;
|
|
||||||
import com.google.android.material.snackbar.Snackbar;
|
|
||||||
|
|
||||||
import androidx.annotation.IdRes;
|
import androidx.annotation.IdRes;
|
||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
|
@ -53,11 +42,11 @@ 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.DatabaseManager;
|
||||||
import be.digitalia.fosdem.fragments.BookmarksListFragment;
|
import be.digitalia.fosdem.fragments.*;
|
||||||
import be.digitalia.fosdem.fragments.LiveFragment;
|
import be.digitalia.fosdem.livedata.SingleEvent;
|
||||||
import be.digitalia.fosdem.fragments.MapFragment;
|
import be.digitalia.fosdem.model.DownloadScheduleResult;
|
||||||
import be.digitalia.fosdem.fragments.PersonsListFragment;
|
import com.google.android.material.navigation.NavigationView;
|
||||||
import be.digitalia.fosdem.fragments.TracksFragment;
|
import com.google.android.material.snackbar.Snackbar;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Main entry point of the application. Allows to switch between section fragments and update the database.
|
* Main entry point of the application. Allows to switch between section fragments and update the database.
|
||||||
|
@ -136,24 +125,26 @@ public class MainActivity extends AppCompatActivity {
|
||||||
|
|
||||||
private MenuItem searchMenuItem;
|
private MenuItem searchMenuItem;
|
||||||
|
|
||||||
private final BroadcastReceiver scheduleDownloadResultReceiver = new BroadcastReceiver() {
|
private final Observer<SingleEvent<DownloadScheduleResult>> scheduleDownloadResultObserver = new Observer<SingleEvent<DownloadScheduleResult>>() {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onReceive(Context context, Intent intent) {
|
public void onChanged(SingleEvent<DownloadScheduleResult> singleEvent) {
|
||||||
int result = intent.getIntExtra(FosdemApi.EXTRA_RESULT, FosdemApi.RESULT_ERROR);
|
final DownloadScheduleResult result = singleEvent.consume();
|
||||||
|
if (result == null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
String message;
|
String message;
|
||||||
switch (result) {
|
if (result.isError()) {
|
||||||
case FosdemApi.RESULT_ERROR:
|
message = getString(R.string.schedule_loading_error);
|
||||||
message = getString(R.string.schedule_loading_error);
|
} else if (result.isUpToDate()) {
|
||||||
break;
|
message = getString(R.string.events_download_up_to_date);
|
||||||
case FosdemApi.RESULT_UP_TO_DATE:
|
} else {
|
||||||
message = getString(R.string.events_download_up_to_date);
|
int eventsCount = result.getEventsCount();
|
||||||
break;
|
if (eventsCount == 0) {
|
||||||
case 0:
|
|
||||||
message = getString(R.string.events_download_empty);
|
message = getString(R.string.events_download_empty);
|
||||||
break;
|
} else {
|
||||||
default:
|
message = getResources().getQuantityString(R.plurals.events_download_completed, eventsCount, eventsCount);
|
||||||
message = getResources().getQuantityString(R.plurals.events_download_completed, result, result);
|
}
|
||||||
}
|
}
|
||||||
Snackbar.make(findViewById(R.id.content), message, Snackbar.LENGTH_LONG).show();
|
Snackbar.make(findViewById(R.id.content), message, Snackbar.LENGTH_LONG).show();
|
||||||
}
|
}
|
||||||
|
@ -172,14 +163,14 @@ public class MainActivity extends AppCompatActivity {
|
||||||
@NonNull
|
@NonNull
|
||||||
@Override
|
@Override
|
||||||
public Dialog onCreateDialog(Bundle savedInstanceState) {
|
public Dialog onCreateDialog(Bundle savedInstanceState) {
|
||||||
return new AlertDialog.Builder(getActivity())
|
return new AlertDialog.Builder(getContext())
|
||||||
.setTitle(R.string.download_reminder_title)
|
.setTitle(R.string.download_reminder_title)
|
||||||
.setMessage(R.string.download_reminder_message)
|
.setMessage(R.string.download_reminder_message)
|
||||||
.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
|
.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onClick(DialogInterface dialog, int which) {
|
public void onClick(DialogInterface dialog, int which) {
|
||||||
((MainActivity) getActivity()).startDownloadSchedule();
|
FosdemApi.downloadSchedule(getContext());
|
||||||
}
|
}
|
||||||
|
|
||||||
}).setNegativeButton(android.R.string.cancel, null)
|
}).setNegativeButton(android.R.string.cancel, null)
|
||||||
|
@ -234,6 +225,9 @@ public class MainActivity extends AppCompatActivity {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Monitor the schedule download result
|
||||||
|
FosdemApi.getDownloadScheduleResult().observe(this, scheduleDownloadResultObserver);
|
||||||
|
|
||||||
// Setup drawer layout
|
// Setup drawer layout
|
||||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||||
drawerLayout = findViewById(R.id.drawer_layout);
|
drawerLayout = findViewById(R.id.drawer_layout);
|
||||||
|
@ -296,7 +290,7 @@ public class MainActivity extends AppCompatActivity {
|
||||||
updateLastUpdateTime();
|
updateLastUpdateTime();
|
||||||
|
|
||||||
if (savedInstanceState == null) {
|
if (savedInstanceState == null) {
|
||||||
// Select initial section
|
// Select initial section
|
||||||
currentSection = Section.TRACKS;
|
currentSection = Section.TRACKS;
|
||||||
String action = getIntent().getAction();
|
String action = getIntent().getAction();
|
||||||
if (action != null) {
|
if (action != null) {
|
||||||
|
@ -375,10 +369,6 @@ public class MainActivity extends AppCompatActivity {
|
||||||
protected void onStart() {
|
protected void onStart() {
|
||||||
super.onStart();
|
super.onStart();
|
||||||
|
|
||||||
// Monitor the schedule download result
|
|
||||||
LocalBroadcastManager.getInstance(this).registerReceiver(scheduleDownloadResultReceiver,
|
|
||||||
new IntentFilter(FosdemApi.ACTION_DOWNLOAD_SCHEDULE_RESULT));
|
|
||||||
|
|
||||||
// Download reminder
|
// Download reminder
|
||||||
long now = System.currentTimeMillis();
|
long now = System.currentTimeMillis();
|
||||||
long time = DatabaseManager.getInstance().getLastUpdateTime();
|
long time = DatabaseManager.getInstance().getLastUpdateTime();
|
||||||
|
@ -404,8 +394,6 @@ public class MainActivity extends AppCompatActivity {
|
||||||
searchMenuItem.collapseActionView();
|
searchMenuItem.collapseActionView();
|
||||||
}
|
}
|
||||||
|
|
||||||
LocalBroadcastManager.getInstance(this).unregisterReceiver(scheduleDownloadResultReceiver);
|
|
||||||
|
|
||||||
super.onStop();
|
super.onStop();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -444,35 +432,16 @@ public class MainActivity extends AppCompatActivity {
|
||||||
item.setIcon(icon);
|
item.setIcon(icon);
|
||||||
((Animatable) icon).start();
|
((Animatable) icon).start();
|
||||||
}
|
}
|
||||||
startDownloadSchedule();
|
FosdemApi.downloadSchedule(this);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void startDownloadSchedule() {
|
|
||||||
new DownloadScheduleAsyncTask(this).executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static class DownloadScheduleAsyncTask extends AsyncTask<Void, Void, Void> {
|
|
||||||
|
|
||||||
private final Context appContext;
|
|
||||||
|
|
||||||
public DownloadScheduleAsyncTask(Context context) {
|
|
||||||
appContext = context.getApplicationContext();
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
protected Void doInBackground(Void... args) {
|
|
||||||
FosdemApi.downloadSchedule(appContext);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// MAIN MENU
|
// MAIN MENU
|
||||||
|
|
||||||
void handleNavigationMenuItem(@NonNull MenuItem menuItem) {
|
void handleNavigationMenuItem(@NonNull MenuItem menuItem) {
|
||||||
final int menuItemId = menuItem.getItemId();
|
final int menuItemId = menuItem.getItemId();
|
||||||
final Section section = Section.fromMenuItemId(menuItemId);
|
final Section section = Section.fromMenuItemId(menuItemId);
|
||||||
if (section != null) {
|
if (section != null) {
|
||||||
selectMenuSection(section, menuItem);
|
selectMenuSection(section, menuItem);
|
||||||
|
|
|
@ -1,24 +1,22 @@
|
||||||
package be.digitalia.fosdem.api;
|
package be.digitalia.fosdem.api;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.Intent;
|
import android.os.AsyncTask;
|
||||||
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.concurrent.locks.Lock;
|
|
||||||
import java.util.concurrent.locks.ReentrantLock;
|
|
||||||
|
|
||||||
import androidx.annotation.MainThread;
|
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 androidx.localbroadcastmanager.content.LocalBroadcastManager;
|
|
||||||
import be.digitalia.fosdem.BuildConfig;
|
|
||||||
import be.digitalia.fosdem.db.DatabaseManager;
|
import be.digitalia.fosdem.db.DatabaseManager;
|
||||||
|
import be.digitalia.fosdem.livedata.SingleEvent;
|
||||||
|
import be.digitalia.fosdem.model.DownloadScheduleResult;
|
||||||
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.parsers.EventsParser;
|
import be.digitalia.fosdem.parsers.EventsParser;
|
||||||
import be.digitalia.fosdem.utils.HttpUtils;
|
import be.digitalia.fosdem.utils.HttpUtils;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.concurrent.atomic.AtomicBoolean;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Main API entry point.
|
* Main API entry point.
|
||||||
*
|
*
|
||||||
|
@ -26,31 +24,36 @@ import be.digitalia.fosdem.utils.HttpUtils;
|
||||||
*/
|
*/
|
||||||
public class FosdemApi {
|
public class FosdemApi {
|
||||||
|
|
||||||
// Local broadcasts parameters
|
private static final AtomicBoolean isLoading = new AtomicBoolean();
|
||||||
public static final String ACTION_DOWNLOAD_SCHEDULE_RESULT = BuildConfig.APPLICATION_ID + ".action.DOWNLOAD_SCHEDULE_RESULT";
|
|
||||||
public static final String EXTRA_RESULT = "RESULT";
|
|
||||||
|
|
||||||
public static final int RESULT_ERROR = -1;
|
|
||||||
public static final int RESULT_UP_TO_DATE = -2;
|
|
||||||
|
|
||||||
private static final Lock scheduleLock = new ReentrantLock();
|
|
||||||
private static final MutableLiveData<Integer> progress = new MutableLiveData<>();
|
private static final MutableLiveData<Integer> progress = new MutableLiveData<>();
|
||||||
|
private static final MutableLiveData<SingleEvent<DownloadScheduleResult>> result = new MutableLiveData<>();
|
||||||
private static LiveData<Map<String, RoomStatus>> roomStatuses;
|
private static LiveData<Map<String, RoomStatus>> roomStatuses;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Download & store the schedule to the database.
|
* Download & store the schedule to the database.
|
||||||
* Only one thread at a time will perform the actual action, the other ones will return immediately.
|
* Only one thread at a time will perform the actual action, the other ones will return immediately.
|
||||||
* The result will be sent back in the form of a local broadcast with an ACTION_DOWNLOAD_SCHEDULE_RESULT action.
|
* The result will be sent back in the consumable Result LiveData.
|
||||||
*/
|
*/
|
||||||
@WorkerThread
|
@MainThread
|
||||||
public static void downloadSchedule(Context context) {
|
public static void downloadSchedule(Context context) {
|
||||||
if (!scheduleLock.tryLock()) {
|
if (!isLoading.compareAndSet(false, true)) {
|
||||||
// If a download is already in progress, return immediately
|
// If a download is already in progress, return immediately
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
final Context appContext = context.getApplicationContext();
|
||||||
|
AsyncTask.THREAD_POOL_EXECUTOR.execute(new Runnable() {
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
downloadScheduleInternal(appContext);
|
||||||
|
isLoading.set(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@WorkerThread
|
||||||
|
private static void downloadScheduleInternal(Context context) {
|
||||||
progress.postValue(-1);
|
progress.postValue(-1);
|
||||||
int result = RESULT_ERROR;
|
DownloadScheduleResult res = DownloadScheduleResult.error();
|
||||||
try {
|
try {
|
||||||
DatabaseManager dbManager = DatabaseManager.getInstance();
|
DatabaseManager dbManager = DatabaseManager.getInstance();
|
||||||
HttpUtils.HttpResult httpResult = HttpUtils.get(
|
HttpUtils.HttpResult httpResult = HttpUtils.get(
|
||||||
|
@ -64,13 +67,14 @@ public class FosdemApi {
|
||||||
});
|
});
|
||||||
if (httpResult.inputStream == null) {
|
if (httpResult.inputStream == null) {
|
||||||
// Nothing to parse, the result is up-to-date.
|
// Nothing to parse, the result is up-to-date.
|
||||||
result = RESULT_UP_TO_DATE;
|
res = DownloadScheduleResult.upToDate();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
Iterable<Event> events = new EventsParser().parse(httpResult.inputStream);
|
Iterable<Event> events = new EventsParser().parse(httpResult.inputStream);
|
||||||
result = dbManager.storeSchedule(events, httpResult.lastModified);
|
int count = dbManager.storeSchedule(events, httpResult.lastModified);
|
||||||
|
res = DownloadScheduleResult.success(count);
|
||||||
} finally {
|
} finally {
|
||||||
try {
|
try {
|
||||||
httpResult.inputStream.close();
|
httpResult.inputStream.close();
|
||||||
|
@ -80,12 +84,10 @@ public class FosdemApi {
|
||||||
|
|
||||||
} catch (Exception e) {
|
} catch (Exception e) {
|
||||||
e.printStackTrace();
|
e.printStackTrace();
|
||||||
|
res = DownloadScheduleResult.error();
|
||||||
} finally {
|
} finally {
|
||||||
progress.postValue(100);
|
progress.postValue(100);
|
||||||
Intent resultIntent = new Intent(ACTION_DOWNLOAD_SCHEDULE_RESULT)
|
result.postValue(new SingleEvent<>(res));
|
||||||
.putExtra(EXTRA_RESULT, result);
|
|
||||||
LocalBroadcastManager.getInstance(context).sendBroadcast(resultIntent);
|
|
||||||
scheduleLock.unlock();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -99,6 +101,10 @@ public class FosdemApi {
|
||||||
return progress;
|
return progress;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static LiveData<SingleEvent<DownloadScheduleResult>> getDownloadScheduleResult() {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
@MainThread
|
@MainThread
|
||||||
public static LiveData<Map<String, RoomStatus>> getRoomStatuses() {
|
public static LiveData<Map<String, RoomStatus>> getRoomStatuses() {
|
||||||
if (roomStatuses == null) {
|
if (roomStatuses == null) {
|
||||||
|
|
|
@ -0,0 +1,28 @@
|
||||||
|
package be.digitalia.fosdem.livedata;
|
||||||
|
|
||||||
|
import androidx.annotation.NonNull;
|
||||||
|
import androidx.annotation.Nullable;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Encapsulates data that can only be consumed once.
|
||||||
|
*/
|
||||||
|
public class SingleEvent<T> {
|
||||||
|
|
||||||
|
private T content;
|
||||||
|
|
||||||
|
public SingleEvent(@NonNull T content) {
|
||||||
|
this.content = content;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return The content, or null if it has already been consumed.
|
||||||
|
*/
|
||||||
|
@Nullable
|
||||||
|
public T consume() {
|
||||||
|
final T previousContent = content;
|
||||||
|
if (previousContent != null) {
|
||||||
|
content = null;
|
||||||
|
}
|
||||||
|
return previousContent;
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,41 @@
|
||||||
|
package be.digitalia.fosdem.model;
|
||||||
|
|
||||||
|
public class DownloadScheduleResult {
|
||||||
|
|
||||||
|
private static final DownloadScheduleResult RESULT_ERROR = new DownloadScheduleResult(0);
|
||||||
|
private static final DownloadScheduleResult RESULT_UP_TO_DATE = new DownloadScheduleResult(0);
|
||||||
|
|
||||||
|
private final int eventsCount;
|
||||||
|
|
||||||
|
private DownloadScheduleResult(int eventsCount) {
|
||||||
|
this.eventsCount = eventsCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static DownloadScheduleResult success(int eventsCount) {
|
||||||
|
return new DownloadScheduleResult(eventsCount);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static DownloadScheduleResult error() {
|
||||||
|
return RESULT_ERROR;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static DownloadScheduleResult upToDate() {
|
||||||
|
return RESULT_UP_TO_DATE;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isSuccess() {
|
||||||
|
return this != RESULT_ERROR && this != RESULT_UP_TO_DATE;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isError() {
|
||||||
|
return this == RESULT_ERROR;
|
||||||
|
}
|
||||||
|
|
||||||
|
public boolean isUpToDate() {
|
||||||
|
return this == RESULT_UP_TO_DATE;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getEventsCount() {
|
||||||
|
return eventsCount;
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue