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.app.Dialog;
|
||||
import android.app.SearchManager;
|
||||
import android.content.ActivityNotFoundException;
|
||||
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.content.*;
|
||||
import android.graphics.drawable.Animatable;
|
||||
import android.graphics.drawable.Drawable;
|
||||
import android.net.Uri;
|
||||
import android.os.AsyncTask;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.text.format.DateUtils;
|
||||
|
@ -23,10 +16,6 @@ import android.view.MenuItem;
|
|||
import android.view.View;
|
||||
import android.widget.ProgressBar;
|
||||
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.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
@ -53,11 +42,11 @@ import be.digitalia.fosdem.R;
|
|||
import be.digitalia.fosdem.api.FosdemApi;
|
||||
import be.digitalia.fosdem.api.FosdemUrls;
|
||||
import be.digitalia.fosdem.db.DatabaseManager;
|
||||
import be.digitalia.fosdem.fragments.BookmarksListFragment;
|
||||
import be.digitalia.fosdem.fragments.LiveFragment;
|
||||
import be.digitalia.fosdem.fragments.MapFragment;
|
||||
import be.digitalia.fosdem.fragments.PersonsListFragment;
|
||||
import be.digitalia.fosdem.fragments.TracksFragment;
|
||||
import be.digitalia.fosdem.fragments.*;
|
||||
import be.digitalia.fosdem.livedata.SingleEvent;
|
||||
import be.digitalia.fosdem.model.DownloadScheduleResult;
|
||||
import com.google.android.material.navigation.NavigationView;
|
||||
import com.google.android.material.snackbar.Snackbar;
|
||||
|
||||
/**
|
||||
* 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 final BroadcastReceiver scheduleDownloadResultReceiver = new BroadcastReceiver() {
|
||||
private final Observer<SingleEvent<DownloadScheduleResult>> scheduleDownloadResultObserver = new Observer<SingleEvent<DownloadScheduleResult>>() {
|
||||
|
||||
@Override
|
||||
public void onReceive(Context context, Intent intent) {
|
||||
int result = intent.getIntExtra(FosdemApi.EXTRA_RESULT, FosdemApi.RESULT_ERROR);
|
||||
public void onChanged(SingleEvent<DownloadScheduleResult> singleEvent) {
|
||||
final DownloadScheduleResult result = singleEvent.consume();
|
||||
if (result == null) {
|
||||
return;
|
||||
}
|
||||
String message;
|
||||
switch (result) {
|
||||
case FosdemApi.RESULT_ERROR:
|
||||
message = getString(R.string.schedule_loading_error);
|
||||
break;
|
||||
case FosdemApi.RESULT_UP_TO_DATE:
|
||||
message = getString(R.string.events_download_up_to_date);
|
||||
break;
|
||||
case 0:
|
||||
if (result.isError()) {
|
||||
message = getString(R.string.schedule_loading_error);
|
||||
} else if (result.isUpToDate()) {
|
||||
message = getString(R.string.events_download_up_to_date);
|
||||
} else {
|
||||
int eventsCount = result.getEventsCount();
|
||||
if (eventsCount == 0) {
|
||||
message = getString(R.string.events_download_empty);
|
||||
break;
|
||||
default:
|
||||
message = getResources().getQuantityString(R.plurals.events_download_completed, result, result);
|
||||
} else {
|
||||
message = getResources().getQuantityString(R.plurals.events_download_completed, eventsCount, eventsCount);
|
||||
}
|
||||
}
|
||||
Snackbar.make(findViewById(R.id.content), message, Snackbar.LENGTH_LONG).show();
|
||||
}
|
||||
|
@ -172,14 +163,14 @@ public class MainActivity extends AppCompatActivity {
|
|||
@NonNull
|
||||
@Override
|
||||
public Dialog onCreateDialog(Bundle savedInstanceState) {
|
||||
return new AlertDialog.Builder(getActivity())
|
||||
return new AlertDialog.Builder(getContext())
|
||||
.setTitle(R.string.download_reminder_title)
|
||||
.setMessage(R.string.download_reminder_message)
|
||||
.setPositiveButton(android.R.string.ok, new DialogInterface.OnClickListener() {
|
||||
|
||||
@Override
|
||||
public void onClick(DialogInterface dialog, int which) {
|
||||
((MainActivity) getActivity()).startDownloadSchedule();
|
||||
FosdemApi.downloadSchedule(getContext());
|
||||
}
|
||||
|
||||
}).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
|
||||
getSupportActionBar().setDisplayHomeAsUpEnabled(true);
|
||||
drawerLayout = findViewById(R.id.drawer_layout);
|
||||
|
@ -296,7 +290,7 @@ public class MainActivity extends AppCompatActivity {
|
|||
updateLastUpdateTime();
|
||||
|
||||
if (savedInstanceState == null) {
|
||||
// Select initial section
|
||||
// Select initial section
|
||||
currentSection = Section.TRACKS;
|
||||
String action = getIntent().getAction();
|
||||
if (action != null) {
|
||||
|
@ -375,10 +369,6 @@ public class MainActivity extends AppCompatActivity {
|
|||
protected void onStart() {
|
||||
super.onStart();
|
||||
|
||||
// Monitor the schedule download result
|
||||
LocalBroadcastManager.getInstance(this).registerReceiver(scheduleDownloadResultReceiver,
|
||||
new IntentFilter(FosdemApi.ACTION_DOWNLOAD_SCHEDULE_RESULT));
|
||||
|
||||
// Download reminder
|
||||
long now = System.currentTimeMillis();
|
||||
long time = DatabaseManager.getInstance().getLastUpdateTime();
|
||||
|
@ -404,8 +394,6 @@ public class MainActivity extends AppCompatActivity {
|
|||
searchMenuItem.collapseActionView();
|
||||
}
|
||||
|
||||
LocalBroadcastManager.getInstance(this).unregisterReceiver(scheduleDownloadResultReceiver);
|
||||
|
||||
super.onStop();
|
||||
}
|
||||
|
||||
|
@ -444,35 +432,16 @@ public class MainActivity extends AppCompatActivity {
|
|||
item.setIcon(icon);
|
||||
((Animatable) icon).start();
|
||||
}
|
||||
startDownloadSchedule();
|
||||
FosdemApi.downloadSchedule(this);
|
||||
return true;
|
||||
}
|
||||
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
|
||||
|
||||
void handleNavigationMenuItem(@NonNull MenuItem menuItem) {
|
||||
final int menuItemId = menuItem.getItemId();
|
||||
final int menuItemId = menuItem.getItemId();
|
||||
final Section section = Section.fromMenuItemId(menuItemId);
|
||||
if (section != null) {
|
||||
selectMenuSection(section, menuItem);
|
||||
|
|
|
@ -1,24 +1,22 @@
|
|||
package be.digitalia.fosdem.api;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.Intent;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.locks.Lock;
|
||||
import java.util.concurrent.locks.ReentrantLock;
|
||||
|
||||
import android.os.AsyncTask;
|
||||
import androidx.annotation.MainThread;
|
||||
import androidx.annotation.WorkerThread;
|
||||
import androidx.lifecycle.LiveData;
|
||||
import androidx.lifecycle.MutableLiveData;
|
||||
import androidx.localbroadcastmanager.content.LocalBroadcastManager;
|
||||
import be.digitalia.fosdem.BuildConfig;
|
||||
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.RoomStatus;
|
||||
import be.digitalia.fosdem.parsers.EventsParser;
|
||||
import be.digitalia.fosdem.utils.HttpUtils;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
/**
|
||||
* Main API entry point.
|
||||
*
|
||||
|
@ -26,31 +24,36 @@ import be.digitalia.fosdem.utils.HttpUtils;
|
|||
*/
|
||||
public class FosdemApi {
|
||||
|
||||
// Local broadcasts parameters
|
||||
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 AtomicBoolean isLoading = new AtomicBoolean();
|
||||
private static final MutableLiveData<Integer> progress = new MutableLiveData<>();
|
||||
private static final MutableLiveData<SingleEvent<DownloadScheduleResult>> result = new MutableLiveData<>();
|
||||
private static LiveData<Map<String, RoomStatus>> roomStatuses;
|
||||
|
||||
/**
|
||||
* Download & store the schedule to the database.
|
||||
* 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) {
|
||||
if (!scheduleLock.tryLock()) {
|
||||
if (!isLoading.compareAndSet(false, true)) {
|
||||
// If a download is already in progress, return immediately
|
||||
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);
|
||||
int result = RESULT_ERROR;
|
||||
DownloadScheduleResult res = DownloadScheduleResult.error();
|
||||
try {
|
||||
DatabaseManager dbManager = DatabaseManager.getInstance();
|
||||
HttpUtils.HttpResult httpResult = HttpUtils.get(
|
||||
|
@ -64,13 +67,14 @@ public class FosdemApi {
|
|||
});
|
||||
if (httpResult.inputStream == null) {
|
||||
// Nothing to parse, the result is up-to-date.
|
||||
result = RESULT_UP_TO_DATE;
|
||||
res = DownloadScheduleResult.upToDate();
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
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 {
|
||||
try {
|
||||
httpResult.inputStream.close();
|
||||
|
@ -80,12 +84,10 @@ public class FosdemApi {
|
|||
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
res = DownloadScheduleResult.error();
|
||||
} finally {
|
||||
progress.postValue(100);
|
||||
Intent resultIntent = new Intent(ACTION_DOWNLOAD_SCHEDULE_RESULT)
|
||||
.putExtra(EXTRA_RESULT, result);
|
||||
LocalBroadcastManager.getInstance(context).sendBroadcast(resultIntent);
|
||||
scheduleLock.unlock();
|
||||
result.postValue(new SingleEvent<>(res));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -99,6 +101,10 @@ public class FosdemApi {
|
|||
return progress;
|
||||
}
|
||||
|
||||
public static LiveData<SingleEvent<DownloadScheduleResult>> getDownloadScheduleResult() {
|
||||
return result;
|
||||
}
|
||||
|
||||
@MainThread
|
||||
public static LiveData<Map<String, RoomStatus>> getRoomStatuses() {
|
||||
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