1
0
Fork 0
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:
Christophe Beyls 2019-01-24 20:20:50 +01:00
parent 2a7b1439cc
commit 7e15f7de55
4 changed files with 131 additions and 87 deletions

View file

@ -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);

View file

@ -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) {

View file

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

View file

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