diff options
16 files changed, 304 insertions, 218 deletions
diff --git a/.circleci/config.yml b/.circleci/config.yml index fa0e1cef8..8063f259c 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -66,7 +66,7 @@ workflows: build-steps: - run: name: Build free (for F-Droid) - command: ./gradlew assembleFreeRelease -PdisablePreDex -PfreeBuild + command: ./gradlew assembleFreeRelease -PdisablePreDex static-analysis: jobs: diff --git a/app/build.gradle b/app/build.gradle index dea0e9992..9493a074b 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -22,8 +22,8 @@ android { // "1.2.3-SNAPSHOT" -> 1020300 // "1.2.3-RC4" -> 1020304 // "1.2.3" -> 1020395 - versionCode 2010395 - versionName "2.1.3" + versionCode 2010495 + versionName "2.1.4" multiDexEnabled false vectorDrawables.useSupportLibrary true @@ -156,14 +156,7 @@ android { } dependencies { - freeImplementation project(":core") - // free build hack: skip some dependencies - if (!doFreeBuild()) { - playImplementation project(":core") - implementation 'com.google.android.play:core:1.8.0' - } else { - System.out.println("app: free build hack, skipping some dependencies") - } + implementation project(":core") implementation project(':ui:app-start-intent') implementation project(':ui:common') @@ -200,6 +193,8 @@ dependencies { implementation 'com.github.ByteHamster:SearchPreference:v2.0.0' implementation 'com.github.skydoves:balloon:1.1.5' + // Non-free dependencies: + playImplementation 'com.google.android.play:core:1.8.0' compileOnly "com.google.android.wearable:wearable:$wearableSupportVersion" androidTestImplementation "org.awaitility:awaitility:$awaitilityVersion" diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/gpodnet/PodcastListFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/gpodnet/PodcastListFragment.java index be9699348..7ee0936d0 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/gpodnet/PodcastListFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/gpodnet/PodcastListFragment.java @@ -1,10 +1,7 @@ package de.danoeh.antennapod.fragment.gpodnet; -import android.content.Context; import android.content.Intent; -import android.os.AsyncTask; import android.os.Bundle; -import androidx.fragment.app.Fragment; import android.util.Log; import android.view.LayoutInflater; import android.view.View; @@ -13,9 +10,7 @@ import android.widget.Button; import android.widget.GridView; import android.widget.ProgressBar; import android.widget.TextView; - -import java.util.List; - +import androidx.fragment.app.Fragment; import de.danoeh.antennapod.R; import de.danoeh.antennapod.activity.MainActivity; import de.danoeh.antennapod.activity.OnlineFeedViewActivity; @@ -25,6 +20,12 @@ import de.danoeh.antennapod.core.service.download.AntennapodHttpClient; import de.danoeh.antennapod.core.sync.gpoddernet.GpodnetService; import de.danoeh.antennapod.core.sync.gpoddernet.GpodnetServiceException; import de.danoeh.antennapod.core.sync.gpoddernet.model.GpodnetPodcast; +import io.reactivex.Observable; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.disposables.Disposable; +import io.reactivex.schedulers.Schedulers; + +import java.util.List; /** * Displays a list of GPodnetPodcast-Objects in a GridView @@ -36,6 +37,7 @@ public abstract class PodcastListFragment extends Fragment { private ProgressBar progressBar; private TextView txtvError; private Button butRetry; + private Disposable disposable; @Override public View onCreateView(LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) { @@ -64,60 +66,44 @@ public abstract class PodcastListFragment extends Fragment { protected abstract List<GpodnetPodcast> loadPodcastData(GpodnetService service) throws GpodnetServiceException; final void loadData() { - AsyncTask<Void, Void, List<GpodnetPodcast>> loaderTask = new AsyncTask<Void, Void, List<GpodnetPodcast>>() { - volatile Exception exception = null; - - @Override - protected List<GpodnetPodcast> doInBackground(Void... params) { - try { + if (disposable != null) { + disposable.dispose(); + } + gridView.setVisibility(View.GONE); + progressBar.setVisibility(View.VISIBLE); + txtvError.setVisibility(View.GONE); + butRetry.setVisibility(View.GONE); + disposable = Observable.fromCallable( + () -> { GpodnetService service = new GpodnetService(AntennapodHttpClient.getHttpClient(), GpodnetPreferences.getHosturl()); return loadPodcastData(service); - } catch (GpodnetServiceException e) { - exception = e; - e.printStackTrace(); - return null; - } - } - - @Override - protected void onPostExecute(List<GpodnetPodcast> gpodnetPodcasts) { - super.onPostExecute(gpodnetPodcasts); - final Context context = getActivity(); - if (context != null && gpodnetPodcasts != null && gpodnetPodcasts.size() > 0) { - PodcastListAdapter listAdapter = new PodcastListAdapter(context, 0, gpodnetPodcasts); - gridView.setAdapter(listAdapter); - listAdapter.notifyDataSetChanged(); - - progressBar.setVisibility(View.GONE); - gridView.setVisibility(View.VISIBLE); - txtvError.setVisibility(View.GONE); - butRetry.setVisibility(View.GONE); - } else if (context != null && gpodnetPodcasts != null) { - gridView.setVisibility(View.GONE); - progressBar.setVisibility(View.GONE); - txtvError.setText(getString(R.string.search_status_no_results)); - txtvError.setVisibility(View.VISIBLE); - butRetry.setVisibility(View.GONE); - } else if (context != null) { - gridView.setVisibility(View.GONE); - progressBar.setVisibility(View.GONE); - txtvError.setText(getString(R.string.error_msg_prefix) + exception.getMessage()); - txtvError.setVisibility(View.VISIBLE); - butRetry.setVisibility(View.VISIBLE); - } - } - - @Override - protected void onPreExecute() { - super.onPreExecute(); - gridView.setVisibility(View.GONE); - progressBar.setVisibility(View.VISIBLE); - txtvError.setVisibility(View.GONE); - butRetry.setVisibility(View.GONE); - } - }; - - loaderTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); + }) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + podcasts -> { + progressBar.setVisibility(View.GONE); + butRetry.setVisibility(View.GONE); + + if (podcasts.size() > 0) { + PodcastListAdapter listAdapter = new PodcastListAdapter(getContext(), 0, podcasts); + gridView.setAdapter(listAdapter); + listAdapter.notifyDataSetChanged(); + gridView.setVisibility(View.VISIBLE); + txtvError.setVisibility(View.GONE); + } else { + gridView.setVisibility(View.GONE); + txtvError.setText(getString(R.string.search_status_no_results)); + txtvError.setVisibility(View.VISIBLE); + } + }, error -> { + gridView.setVisibility(View.GONE); + progressBar.setVisibility(View.GONE); + txtvError.setText(getString(R.string.error_msg_prefix) + error.getMessage()); + txtvError.setVisibility(View.VISIBLE); + butRetry.setVisibility(View.VISIBLE); + Log.e(TAG, Log.getStackTraceString(error)); + }); } } diff --git a/app/src/main/java/de/danoeh/antennapod/fragment/gpodnet/TagListFragment.java b/app/src/main/java/de/danoeh/antennapod/fragment/gpodnet/TagListFragment.java index a26ec9e84..9d0f99aa9 100644 --- a/app/src/main/java/de/danoeh/antennapod/fragment/gpodnet/TagListFragment.java +++ b/app/src/main/java/de/danoeh/antennapod/fragment/gpodnet/TagListFragment.java @@ -1,32 +1,34 @@ package de.danoeh.antennapod.fragment.gpodnet; -import android.content.Context; -import android.os.AsyncTask; import android.os.Bundle; +import android.util.Log; import android.view.View; import android.widget.TextView; +import androidx.annotation.NonNull; import androidx.fragment.app.ListFragment; import de.danoeh.antennapod.activity.MainActivity; import de.danoeh.antennapod.adapter.gpodnet.TagListAdapter; import de.danoeh.antennapod.core.preferences.GpodnetPreferences; import de.danoeh.antennapod.core.service.download.AntennapodHttpClient; import de.danoeh.antennapod.core.sync.gpoddernet.GpodnetService; -import de.danoeh.antennapod.core.sync.gpoddernet.GpodnetServiceException; import de.danoeh.antennapod.core.sync.gpoddernet.model.GpodnetTag; - -import java.util.List; +import io.reactivex.Observable; +import io.reactivex.android.schedulers.AndroidSchedulers; +import io.reactivex.disposables.Disposable; +import io.reactivex.schedulers.Schedulers; public class TagListFragment extends ListFragment { private static final int COUNT = 50; + private static final String TAG = "TagListFragment"; + private Disposable disposable; @Override - public void onViewCreated(View view, Bundle savedInstanceState) { + public void onViewCreated(@NonNull View view, Bundle savedInstanceState) { super.onViewCreated(view, savedInstanceState); getListView().setOnItemClickListener((parent, view1, position, id) -> { GpodnetTag tag = (GpodnetTag) getListAdapter().getItem(position); - MainActivity activity = (MainActivity) getActivity(); - activity.loadChildFragment(TagFragment.newInstance(tag)); + ((MainActivity) getActivity()).loadChildFragment(TagFragment.newInstance(tag)); }); startLoadTask(); @@ -35,59 +37,36 @@ public class TagListFragment extends ListFragment { @Override public void onDestroyView() { super.onDestroyView(); - cancelLoadTask(); - } - private AsyncTask<Void, Void, List<GpodnetTag>> loadTask; - - private void cancelLoadTask() { - if (loadTask != null && !loadTask.isCancelled()) { - loadTask.cancel(true); + if (disposable != null) { + disposable.dispose(); } } private void startLoadTask() { - cancelLoadTask(); - loadTask = new AsyncTask<Void, Void, List<GpodnetTag>>() { - private Exception exception; - - @Override - protected List<GpodnetTag> doInBackground(Void... params) { + if (disposable != null) { + disposable.dispose(); + } + setListShown(false); + disposable = Observable.fromCallable( + () -> { GpodnetService service = new GpodnetService(AntennapodHttpClient.getHttpClient(), GpodnetPreferences.getHosturl()); - try { - return service.getTopTags(COUNT); - } catch (GpodnetServiceException e) { - e.printStackTrace(); - exception = e; - return null; - } - } - - @Override - protected void onPreExecute() { - super.onPreExecute(); - setListShown(false); - } - - @Override - protected void onPostExecute(List<GpodnetTag> gpodnetTags) { - super.onPostExecute(gpodnetTags); - final Context context = getActivity(); - if (context != null) { - if (gpodnetTags != null) { - setListAdapter(new TagListAdapter(context, android.R.layout.simple_list_item_1, gpodnetTags)); - } else if (exception != null) { + return service.getTopTags(COUNT); + }) + .subscribeOn(Schedulers.io()) + .observeOn(AndroidSchedulers.mainThread()) + .subscribe( + tags -> { + setListAdapter(new TagListAdapter(getContext(), android.R.layout.simple_list_item_1, tags)); + setListShown(true); + }, error -> { TextView txtvError = new TextView(getActivity()); - txtvError.setText(exception.getMessage()); + txtvError.setText(error.getMessage()); getListView().setEmptyView(txtvError); - } - setListShown(true); - - } - } - }; - loadTask.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); + setListShown(true); + Log.e(TAG, Log.getStackTraceString(error)); + }); } } diff --git a/app/src/main/res/layout/feeditemlist_item.xml b/app/src/main/res/layout/feeditemlist_item.xml index a8ae5743e..e1f382e46 100644 --- a/app/src/main/res/layout/feeditemlist_item.xml +++ b/app/src/main/res/layout/feeditemlist_item.xml @@ -145,6 +145,7 @@ android:layout_marginRight="4dp" android:layout_marginEnd="4dp" android:text="·" + android:importantForAccessibility="no" tools:background="@android:color/holo_blue_light"/> <TextView @@ -163,6 +164,7 @@ android:layout_marginRight="4dp" android:layout_marginEnd="4dp" android:text="·" + android:importantForAccessibility="no" tools:background="@android:color/holo_blue_light"/> <TextView diff --git a/build.gradle b/build.gradle index e3e66b1b6..aa7f8ebbe 100644 --- a/build.gradle +++ b/build.gradle @@ -86,11 +86,6 @@ wrapper { gradleVersion = "6.3" } -// free build hack: common functions -def doFreeBuild() { - return hasProperty("freeBuild") -} - apply plugin: "checkstyle" checkstyle { toolVersion '8.24' diff --git a/core/build.gradle b/core/build.gradle index 29cef1cef..e68fa9f97 100644 --- a/core/build.gradle +++ b/core/build.gradle @@ -99,17 +99,12 @@ dependencies { implementation 'com.google.android.exoplayer:exoplayer:2.11.8' implementation "com.github.AntennaPod:AntennaPod-AudioPlayer:$audioPlayerVersion" - // Add casting features - // free build hack: skip some dependencies - if (!doFreeBuild()) { - playApi 'com.google.android.libraries.cast.companionlibrary:ccl:2.9.1' - api 'androidx.mediarouter:mediarouter:1.0.0' - playApi 'com.google.android.gms:play-services-cast:8.4.0' - api "com.google.android.support:wearable:$wearableSupportVersion" - compileOnly "com.google.android.wearable:wearable:$wearableSupportVersion" - } else { - System.out.println("core: free build hack, skipping some dependencies") - } + // Non-free dependencies: + playApi 'com.google.android.libraries.cast.companionlibrary:ccl:2.9.1' + playApi 'androidx.mediarouter:mediarouter:1.0.0' + playApi 'com.google.android.gms:play-services-cast:8.4.0' + playApi "com.google.android.support:wearable:$wearableSupportVersion" + compileOnly "com.google.android.wearable:wearable:$wearableSupportVersion" // bundle conscrypt with free builds freeImplementation "org.conscrypt:conscrypt-android:$conscryptVersion" diff --git a/core/src/main/java/de/danoeh/antennapod/core/feed/Feed.java b/core/src/main/java/de/danoeh/antennapod/core/feed/Feed.java index da946cf0b..dd8a466eb 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/feed/Feed.java +++ b/core/src/main/java/de/danoeh/antennapod/core/feed/Feed.java @@ -1,6 +1,5 @@ package de.danoeh.antennapod.core.feed; -import android.database.Cursor; import android.text.TextUtils; import androidx.annotation.Nullable; @@ -9,11 +8,10 @@ import java.util.ArrayList; import java.util.Date; import java.util.List; -import de.danoeh.antennapod.core.storage.PodDBAdapter; import de.danoeh.antennapod.core.util.SortOrder; /** - * Data Object for a whole feed + * Data Object for a whole feed. * * @author daniel */ @@ -24,9 +22,14 @@ public class Feed extends FeedFile { public static final String TYPE_ATOM1 = "atom"; public static final String PREFIX_LOCAL_FOLDER = "antennapod_local:"; - /* title as defined by the feed */ + /** + * title as defined by the feed. + */ private String feedTitle; - /* custom title set by the user */ + + /** + * custom title set by the user. + */ private String customTitle; /** @@ -40,25 +43,25 @@ public class Feed extends FeedFile { private String description; private String language; /** - * Name of the author + * Name of the author. */ private String author; private String imageUrl; private List<FeedItem> items; /** - * String that identifies the last update (adopted from Last-Modified or ETag header) + * String that identifies the last update (adopted from Last-Modified or ETag header). */ private String lastUpdate; private String paymentLink; /** - * Feed type, for example RSS 2 or Atom + * Feed type, for example RSS 2 or Atom. */ private String type; /** - * Feed preferences + * Feed preferences. */ private FeedPreferences preferences; @@ -120,7 +123,7 @@ public class Feed extends FeedFile { this.paged = paged; this.nextPageLink = nextPageLink; this.items = new ArrayList<>(); - if(filter != null) { + if (filter != null) { this.itemfilter = new FeedItemFilter(filter); } else { this.itemfilter = new FeedItemFilter(new String[0]); @@ -130,7 +133,7 @@ public class Feed extends FeedFile { } /** - * This constructor is used for test purposes + * This constructor is used for test purposes. */ public Feed(long id, String lastUpdate, String title, String link, String description, String paymentLink, String author, String language, String type, String feedIdentifier, String imageUrl, String fileUrl, @@ -173,56 +176,6 @@ public class Feed extends FeedFile { preferences = new FeedPreferences(0, true, FeedPreferences.AutoDeleteAction.GLOBAL, VolumeAdaptionSetting.OFF, username, password); } - public static Feed fromCursor(Cursor cursor) { - int indexId = cursor.getColumnIndex(PodDBAdapter.KEY_ID); - int indexLastUpdate = cursor.getColumnIndex(PodDBAdapter.KEY_LASTUPDATE); - int indexTitle = cursor.getColumnIndex(PodDBAdapter.KEY_TITLE); - int indexCustomTitle = cursor.getColumnIndex(PodDBAdapter.KEY_CUSTOM_TITLE); - int indexLink = cursor.getColumnIndex(PodDBAdapter.KEY_LINK); - int indexDescription = cursor.getColumnIndex(PodDBAdapter.KEY_DESCRIPTION); - int indexPaymentLink = cursor.getColumnIndex(PodDBAdapter.KEY_PAYMENT_LINK); - int indexAuthor = cursor.getColumnIndex(PodDBAdapter.KEY_AUTHOR); - int indexLanguage = cursor.getColumnIndex(PodDBAdapter.KEY_LANGUAGE); - int indexType = cursor.getColumnIndex(PodDBAdapter.KEY_TYPE); - int indexFeedIdentifier = cursor.getColumnIndex(PodDBAdapter.KEY_FEED_IDENTIFIER); - int indexFileUrl = cursor.getColumnIndex(PodDBAdapter.KEY_FILE_URL); - int indexDownloadUrl = cursor.getColumnIndex(PodDBAdapter.KEY_DOWNLOAD_URL); - int indexDownloaded = cursor.getColumnIndex(PodDBAdapter.KEY_DOWNLOADED); - int indexIsPaged = cursor.getColumnIndex(PodDBAdapter.KEY_IS_PAGED); - int indexNextPageLink = cursor.getColumnIndex(PodDBAdapter.KEY_NEXT_PAGE_LINK); - int indexHide = cursor.getColumnIndex(PodDBAdapter.KEY_HIDE); - int indexSortOrder = cursor.getColumnIndex(PodDBAdapter.KEY_SORT_ORDER); - int indexLastUpdateFailed = cursor.getColumnIndex(PodDBAdapter.KEY_LAST_UPDATE_FAILED); - int indexImageUrl = cursor.getColumnIndex(PodDBAdapter.KEY_IMAGE_URL); - - Feed feed = new Feed( - cursor.getLong(indexId), - cursor.getString(indexLastUpdate), - cursor.getString(indexTitle), - cursor.getString(indexCustomTitle), - cursor.getString(indexLink), - cursor.getString(indexDescription), - cursor.getString(indexPaymentLink), - cursor.getString(indexAuthor), - cursor.getString(indexLanguage), - cursor.getString(indexType), - cursor.getString(indexFeedIdentifier), - cursor.getString(indexImageUrl), - cursor.getString(indexFileUrl), - cursor.getString(indexDownloadUrl), - cursor.getInt(indexDownloaded) > 0, - cursor.getInt(indexIsPaged) > 0, - cursor.getString(indexNextPageLink), - cursor.getString(indexHide), - SortOrder.fromCodeString(cursor.getString(indexSortOrder)), - cursor.getInt(indexLastUpdateFailed) > 0 - ); - - FeedPreferences preferences = FeedPreferences.fromCursor(cursor); - feed.setPreferences(preferences); - return feed; - } - /** * Returns the item at the specified index. * @@ -382,7 +335,7 @@ public class Feed extends FeedFile { } public void setCustomTitle(String customTitle) { - if(customTitle == null || customTitle.equals(feedTitle)) { + if (customTitle == null || customTitle.equals(feedTitle)) { this.customTitle = null; } else { this.customTitle = customTitle; diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/download/DownloadService.java b/core/src/main/java/de/danoeh/antennapod/core/service/download/DownloadService.java index 4a17fbbda..44b673a4d 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/service/download/DownloadService.java +++ b/core/src/main/java/de/danoeh/antennapod/core/service/download/DownloadService.java @@ -172,8 +172,7 @@ public class DownloadService extends Service { setupNotificationUpdaterIfNecessary(); syncExecutor.execute(() -> onDownloadQueued(intent)); } else if (numberOfDownloads.get() == 0) { - stopForeground(true); - stopSelf(); + shutdown(); } else { Log.d(TAG, "onStartCommand: Unknown intent"); } @@ -227,10 +226,6 @@ public class DownloadService extends Service { } unregisterReceiver(cancelDownloadReceiver); - stopForeground(true); - NotificationManager nm = (NotificationManager) getSystemService(NOTIFICATION_SERVICE); - nm.cancel(R.id.notification_downloading); - // if this was the initial gpodder sync, i.e. we just synced the feeds successfully, // it is now time to sync the episode actions SyncService.sync(this); @@ -550,14 +545,7 @@ public class DownloadService extends Service { if (numberOfDownloads.get() <= 0 && DownloadRequester.getInstance().hasNoDownloads()) { Log.d(TAG, "Attempting shutdown"); - stopForeground(true); - stopSelf(); - - // Trick to hide the notification more quickly when the service is stopped - // Without this, the second-last update of the notification stays for 3 seconds after onDestroy returns - notificationUpdater.run(); - NotificationManager nm = (NotificationManager) getSystemService(NOTIFICATION_SERVICE); - nm.cancel(R.id.notification_downloading); + shutdown(); } } @@ -647,4 +635,14 @@ public class DownloadService extends Service { new PostDownloaderTask(downloads), 1, 1, TimeUnit.SECONDS); } } + + private void shutdown() { + // If the service was run for a very short time, the system may delay closing + // the notification. Set the notification text now so that a misleading message + // is not left on the notification. + notificationUpdater.run(); + cancelNotificationUpdater(); + stopForeground(true); + stopSelf(); + } } diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/download/DownloadServiceNotification.java b/core/src/main/java/de/danoeh/antennapod/core/service/download/DownloadServiceNotification.java index 64ed85cf3..7c8fe9452 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/service/download/DownloadServiceNotification.java +++ b/core/src/main/java/de/danoeh/antennapod/core/service/download/DownloadServiceNotification.java @@ -53,7 +53,7 @@ public class DownloadServiceNotification { String contentTitle = context.getString(R.string.download_notification_title); String downloadsLeft = (numDownloads > 0) ? context.getResources().getQuantityString(R.plurals.downloads_left, numDownloads, numDownloads) - : context.getString(R.string.downloads_processing); + : context.getString(R.string.service_shutting_down); String bigText = compileNotificationString(downloads); notificationCompatBuilder.setContentTitle(contentTitle); diff --git a/core/src/main/java/de/danoeh/antennapod/core/storage/DBReader.java b/core/src/main/java/de/danoeh/antennapod/core/storage/DBReader.java index 27f08243f..fcf61b070 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/storage/DBReader.java +++ b/core/src/main/java/de/danoeh/antennapod/core/storage/DBReader.java @@ -23,6 +23,7 @@ import de.danoeh.antennapod.core.feed.FeedPreferences; import de.danoeh.antennapod.core.feed.SubscriptionsFilter; import de.danoeh.antennapod.core.preferences.UserPreferences; import de.danoeh.antennapod.core.service.download.DownloadStatus; +import de.danoeh.antennapod.core.storage.mapper.FeedCursorMapper; import de.danoeh.antennapod.core.util.LongIntMap; import de.danoeh.antennapod.core.util.LongList; import de.danoeh.antennapod.core.util.comparator.DownloadStatusComparator; @@ -204,7 +205,7 @@ public final class DBReader { } private static Feed extractFeedFromCursorRow(Cursor cursor) { - Feed feed = Feed.fromCursor(cursor); + Feed feed = FeedCursorMapper.convert(cursor); FeedPreferences preferences = FeedPreferences.fromCursor(cursor); feed.setPreferences(preferences); return feed; diff --git a/core/src/main/java/de/danoeh/antennapod/core/storage/DBTasks.java b/core/src/main/java/de/danoeh/antennapod/core/storage/DBTasks.java index f5a8fb07a..596ab624e 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/storage/DBTasks.java +++ b/core/src/main/java/de/danoeh/antennapod/core/storage/DBTasks.java @@ -19,6 +19,7 @@ import de.danoeh.antennapod.core.feed.FeedMedia; import de.danoeh.antennapod.core.feed.LocalFeedUpdater; import de.danoeh.antennapod.core.preferences.UserPreferences; import de.danoeh.antennapod.core.service.download.DownloadStatus; +import de.danoeh.antennapod.core.storage.mapper.FeedCursorMapper; import de.danoeh.antennapod.core.sync.SyncService; import de.danoeh.antennapod.core.util.DownloadError; import de.danoeh.antennapod.core.util.LongList; @@ -516,7 +517,7 @@ public final class DBTasks { List<Feed> items = new ArrayList<>(); if (cursor.moveToFirst()) { do { - items.add(Feed.fromCursor(cursor)); + items.add(FeedCursorMapper.convert(cursor)); } while (cursor.moveToNext()); } setResult(items); diff --git a/core/src/main/java/de/danoeh/antennapod/core/storage/PodDBAdapter.java b/core/src/main/java/de/danoeh/antennapod/core/storage/PodDBAdapter.java index 8d1352a10..adb5e6a74 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/storage/PodDBAdapter.java +++ b/core/src/main/java/de/danoeh/antennapod/core/storage/PodDBAdapter.java @@ -15,6 +15,7 @@ import android.util.Log; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; import de.danoeh.antennapod.core.storage.mapper.FeedItemFilterQuery; import org.apache.commons.io.FileUtils; @@ -372,6 +373,7 @@ public class PodDBAdapter { * For more information see * <a href="https://github.com/robolectric/robolectric/issues/1890">robolectric/robolectric#1890</a>.</p> */ + @VisibleForTesting(otherwise = VisibleForTesting.NONE) public static void tearDownTests() { getInstance().dbHelper.close(); instance = null; @@ -1379,7 +1381,16 @@ public class PodDBAdapter { } /** - * Called when a database corruption happens + * Insert raw data to the database. * + * Call method only for unit tests. + */ + @VisibleForTesting(otherwise = VisibleForTesting.NONE) + public void insertTestData(@NonNull String table, @NonNull ContentValues values) { + db.insert(table, null, values); + } + + /** + * Called when a database corruption happens. */ public static class PodDbErrorHandler implements DatabaseErrorHandler { @Override diff --git a/core/src/main/java/de/danoeh/antennapod/core/storage/mapper/FeedCursorMapper.java b/core/src/main/java/de/danoeh/antennapod/core/storage/mapper/FeedCursorMapper.java new file mode 100644 index 000000000..783fba596 --- /dev/null +++ b/core/src/main/java/de/danoeh/antennapod/core/storage/mapper/FeedCursorMapper.java @@ -0,0 +1,70 @@ +package de.danoeh.antennapod.core.storage.mapper; + +import android.database.Cursor; + +import androidx.annotation.NonNull; + +import de.danoeh.antennapod.core.feed.Feed; +import de.danoeh.antennapod.core.feed.FeedPreferences; +import de.danoeh.antennapod.core.storage.PodDBAdapter; +import de.danoeh.antennapod.core.util.SortOrder; + +/** + * Converts a {@link Cursor} to a {@link Feed} object. + */ +public abstract class FeedCursorMapper { + + /** + * Create a {@link Feed} instance from a database row (cursor). + */ + @NonNull + public static Feed convert(@NonNull Cursor cursor) { + int indexId = cursor.getColumnIndex(PodDBAdapter.KEY_ID); + int indexLastUpdate = cursor.getColumnIndex(PodDBAdapter.KEY_LASTUPDATE); + int indexTitle = cursor.getColumnIndex(PodDBAdapter.KEY_TITLE); + int indexCustomTitle = cursor.getColumnIndex(PodDBAdapter.KEY_CUSTOM_TITLE); + int indexLink = cursor.getColumnIndex(PodDBAdapter.KEY_LINK); + int indexDescription = cursor.getColumnIndex(PodDBAdapter.KEY_DESCRIPTION); + int indexPaymentLink = cursor.getColumnIndex(PodDBAdapter.KEY_PAYMENT_LINK); + int indexAuthor = cursor.getColumnIndex(PodDBAdapter.KEY_AUTHOR); + int indexLanguage = cursor.getColumnIndex(PodDBAdapter.KEY_LANGUAGE); + int indexType = cursor.getColumnIndex(PodDBAdapter.KEY_TYPE); + int indexFeedIdentifier = cursor.getColumnIndex(PodDBAdapter.KEY_FEED_IDENTIFIER); + int indexFileUrl = cursor.getColumnIndex(PodDBAdapter.KEY_FILE_URL); + int indexDownloadUrl = cursor.getColumnIndex(PodDBAdapter.KEY_DOWNLOAD_URL); + int indexDownloaded = cursor.getColumnIndex(PodDBAdapter.KEY_DOWNLOADED); + int indexIsPaged = cursor.getColumnIndex(PodDBAdapter.KEY_IS_PAGED); + int indexNextPageLink = cursor.getColumnIndex(PodDBAdapter.KEY_NEXT_PAGE_LINK); + int indexHide = cursor.getColumnIndex(PodDBAdapter.KEY_HIDE); + int indexSortOrder = cursor.getColumnIndex(PodDBAdapter.KEY_SORT_ORDER); + int indexLastUpdateFailed = cursor.getColumnIndex(PodDBAdapter.KEY_LAST_UPDATE_FAILED); + int indexImageUrl = cursor.getColumnIndex(PodDBAdapter.KEY_IMAGE_URL); + + Feed feed = new Feed( + cursor.getLong(indexId), + cursor.getString(indexLastUpdate), + cursor.getString(indexTitle), + cursor.getString(indexCustomTitle), + cursor.getString(indexLink), + cursor.getString(indexDescription), + cursor.getString(indexPaymentLink), + cursor.getString(indexAuthor), + cursor.getString(indexLanguage), + cursor.getString(indexType), + cursor.getString(indexFeedIdentifier), + cursor.getString(indexImageUrl), + cursor.getString(indexFileUrl), + cursor.getString(indexDownloadUrl), + cursor.getInt(indexDownloaded) > 0, + cursor.getInt(indexIsPaged) > 0, + cursor.getString(indexNextPageLink), + cursor.getString(indexHide), + SortOrder.fromCodeString(cursor.getString(indexSortOrder)), + cursor.getInt(indexLastUpdateFailed) > 0 + ); + + FeedPreferences preferences = FeedPreferences.fromCursor(cursor); + feed.setPreferences(preferences); + return feed; + } +} diff --git a/core/src/main/res/values/strings.xml b/core/src/main/res/values/strings.xml index e9c1d8fcd..8e15d60d7 100644 --- a/core/src/main/res/values/strings.xml +++ b/core/src/main/res/values/strings.xml @@ -270,7 +270,7 @@ <item quantity="one">%d download left</item> <item quantity="other">%d downloads left</item> </plurals> - <string name="downloads_processing">Processing downloads</string> + <string name="service_shutting_down">Service shutting down</string> <string name="download_notification_title">Downloading podcast data</string> <plurals name="download_report_content"> <item quantity="one">%d download succeeded, %d failed</item> diff --git a/core/src/test/java/de/danoeh/antennapod/core/storage/mapper/FeedCursorMapperTest.java b/core/src/test/java/de/danoeh/antennapod/core/storage/mapper/FeedCursorMapperTest.java new file mode 100644 index 000000000..c779b6d55 --- /dev/null +++ b/core/src/test/java/de/danoeh/antennapod/core/storage/mapper/FeedCursorMapperTest.java @@ -0,0 +1,100 @@ +package de.danoeh.antennapod.core.storage.mapper; + +import android.content.ContentValues; +import android.content.Context; +import android.database.Cursor; + +import androidx.test.platform.app.InstrumentationRegistry; + +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.robolectric.RobolectricTestRunner; + +import de.danoeh.antennapod.core.feed.Feed; +import de.danoeh.antennapod.core.storage.PodDBAdapter; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; + +@RunWith(RobolectricTestRunner.class) +public class FeedCursorMapperTest { + private PodDBAdapter adapter; + + @Before + public void setUp() { + Context context = InstrumentationRegistry.getInstrumentation().getContext(); + + PodDBAdapter.init(context); + adapter = PodDBAdapter.getInstance(); + + writeFeedToDatabase(); + } + + @After + public void tearDown() { + PodDBAdapter.tearDownTests(); + } + + @SuppressWarnings("ConstantConditions") + @Test + public void testFromCursor() { + try (Cursor cursor = adapter.getAllFeedsCursor()) { + cursor.moveToNext(); + Feed feed = FeedCursorMapper.convert(cursor); + assertTrue(feed.getId() >= 0); + assertEquals("feed custom title", feed.getTitle()); + assertEquals("feed custom title", feed.getCustomTitle()); + assertEquals("feed link", feed.getLink()); + assertEquals("feed description", feed.getDescription()); + assertEquals("feed payment link", feed.getPaymentLink()); + assertEquals("feed author", feed.getAuthor()); + assertEquals("feed language", feed.getLanguage()); + assertEquals("feed image url", feed.getImageUrl()); + assertEquals("feed file url", feed.getFile_url()); + assertEquals("feed download url", feed.getDownload_url()); + assertTrue(feed.isDownloaded()); + assertEquals("feed last update", feed.getLastUpdate()); + assertEquals("feed type", feed.getType()); + assertEquals("feed identifier", feed.getFeedIdentifier()); + assertTrue(feed.isPaged()); + assertEquals("feed next page link", feed.getNextPageLink()); + assertTrue(feed.getItemFilter().showUnplayed); + assertEquals(1, feed.getSortOrder().code); + assertTrue(feed.hasLastUpdateFailed()); + } + } + + /** + * Insert test data to the database. + * Uses raw database insert instead of adapter.setCompleteFeed() to avoid testing the Feed class + * against itself. + */ + private void writeFeedToDatabase() { + ContentValues values = new ContentValues(); + values.put(PodDBAdapter.KEY_TITLE, "feed title"); + values.put(PodDBAdapter.KEY_CUSTOM_TITLE, "feed custom title"); + values.put(PodDBAdapter.KEY_LINK, "feed link"); + values.put(PodDBAdapter.KEY_DESCRIPTION, "feed description"); + values.put(PodDBAdapter.KEY_PAYMENT_LINK, "feed payment link"); + values.put(PodDBAdapter.KEY_AUTHOR, "feed author"); + values.put(PodDBAdapter.KEY_LANGUAGE, "feed language"); + values.put(PodDBAdapter.KEY_IMAGE_URL, "feed image url"); + + values.put(PodDBAdapter.KEY_FILE_URL, "feed file url"); + values.put(PodDBAdapter.KEY_DOWNLOAD_URL, "feed download url"); + values.put(PodDBAdapter.KEY_DOWNLOADED, true); + values.put(PodDBAdapter.KEY_LASTUPDATE, "feed last update"); + values.put(PodDBAdapter.KEY_TYPE, "feed type"); + values.put(PodDBAdapter.KEY_FEED_IDENTIFIER, "feed identifier"); + + values.put(PodDBAdapter.KEY_IS_PAGED, true); + values.put(PodDBAdapter.KEY_NEXT_PAGE_LINK, "feed next page link"); + values.put(PodDBAdapter.KEY_HIDE, "unplayed"); + values.put(PodDBAdapter.KEY_SORT_ORDER, "1"); + values.put(PodDBAdapter.KEY_LAST_UPDATE_FAILED, true); + + adapter.insertTestData(PodDBAdapter.TABLE_NAME_FEEDS, values); + } +} |