diff --git a/app/src/main/java/be/digitalia/fosdem/activities/MainActivity.java b/app/src/main/java/be/digitalia/fosdem/activities/MainActivity.java index 9c30bb5..f469d53 100644 --- a/app/src/main/java/be/digitalia/fosdem/activities/MainActivity.java +++ b/app/src/main/java/be/digitalia/fosdem/activities/MainActivity.java @@ -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> scheduleDownloadResultObserver = new Observer>() { @Override - public void onReceive(Context context, Intent intent) { - int result = intent.getIntExtra(FosdemApi.EXTRA_RESULT, FosdemApi.RESULT_ERROR); + public void onChanged(SingleEvent 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 { - - 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); diff --git a/app/src/main/java/be/digitalia/fosdem/api/FosdemApi.java b/app/src/main/java/be/digitalia/fosdem/api/FosdemApi.java index 368242d..ed80cf4 100644 --- a/app/src/main/java/be/digitalia/fosdem/api/FosdemApi.java +++ b/app/src/main/java/be/digitalia/fosdem/api/FosdemApi.java @@ -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 progress = new MutableLiveData<>(); + private static final MutableLiveData> result = new MutableLiveData<>(); private static LiveData> 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 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> getDownloadScheduleResult() { + return result; + } + @MainThread public static LiveData> getRoomStatuses() { if (roomStatuses == null) { diff --git a/app/src/main/java/be/digitalia/fosdem/livedata/SingleEvent.java b/app/src/main/java/be/digitalia/fosdem/livedata/SingleEvent.java new file mode 100644 index 0000000..53c5884 --- /dev/null +++ b/app/src/main/java/be/digitalia/fosdem/livedata/SingleEvent.java @@ -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 { + + 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; + } +} diff --git a/app/src/main/java/be/digitalia/fosdem/model/DownloadScheduleResult.java b/app/src/main/java/be/digitalia/fosdem/model/DownloadScheduleResult.java new file mode 100644 index 0000000..89f17ec --- /dev/null +++ b/app/src/main/java/be/digitalia/fosdem/model/DownloadScheduleResult.java @@ -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; + } +}