diff options
author | ByteHamster <info@bytehamster.com> | 2021-01-22 11:07:40 +0100 |
---|---|---|
committer | ByteHamster <info@bytehamster.com> | 2021-01-22 11:07:40 +0100 |
commit | cdf59a1c8e99720fd737fcbcc1f5ac1eb5252890 (patch) | |
tree | 119819aaec911b7510dc3f3bb1a5a4a6ddf82c1c /core/src/main/java/de/danoeh | |
parent | 85b897c7d778ae70d837ae0c2a9e7b4252ea8945 (diff) | |
parent | 41d23fa671a3ad7d417f4791c63d0f25634d725f (diff) | |
download | AntennaPod-cdf59a1c8e99720fd737fcbcc1f5ac1eb5252890.zip |
Merge branch 'develop' into fix_episodes_list_item_loading_b
Diffstat (limited to 'core/src/main/java/de/danoeh')
46 files changed, 598 insertions, 500 deletions
diff --git a/core/src/main/java/de/danoeh/antennapod/core/DBTasksCallbacks.java b/core/src/main/java/de/danoeh/antennapod/core/DBTasksCallbacks.java deleted file mode 100644 index 11a6b2c9f..000000000 --- a/core/src/main/java/de/danoeh/antennapod/core/DBTasksCallbacks.java +++ /dev/null @@ -1,20 +0,0 @@ -package de.danoeh.antennapod.core; - -import de.danoeh.antennapod.core.storage.AutomaticDownloadAlgorithm; -import de.danoeh.antennapod.core.storage.EpisodeCleanupAlgorithm; - -/** - * Callbacks for the DBTasks class of the storage module. - */ -public interface DBTasksCallbacks { - - /** - * Returns the client's implementation of the AutomaticDownloadAlgorithm interface. - */ - AutomaticDownloadAlgorithm getAutomaticDownloadAlgorithm(); - - /** - * Returns the client's implementation of the EpisodeCacheCleanupAlgorithm interface. - */ - EpisodeCleanupAlgorithm getEpisodeCacheCleanupAlgorithm(); -} diff --git a/core/src/main/java/de/danoeh/antennapod/core/DownloadServiceCallbacks.java b/core/src/main/java/de/danoeh/antennapod/core/DownloadServiceCallbacks.java index ad3fb8d42..ae9b47629 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/DownloadServiceCallbacks.java +++ b/core/src/main/java/de/danoeh/antennapod/core/DownloadServiceCallbacks.java @@ -37,7 +37,7 @@ public interface DownloadServiceCallbacks { * <p/> * The PendingIntent takes users to an activity where they can look at all successful and failed downloads. * - * @return A non-null PendingIntent for the notification or null if shouldCreateReport()==false + * @return A non-null PendingIntent for the notification */ PendingIntent getReportNotificationContentIntent(Context context); @@ -47,14 +47,8 @@ public interface DownloadServiceCallbacks { * <p/> * The PendingIntent takes users to an activity where they can look at their episode queue. * - * @return A non-null PendingIntent for the notification or null if shouldCreateReport()==false + * @return A non-null PendingIntent for the notification */ PendingIntent getAutoDownloadReportNotificationContentIntent(Context context); - - /** - * Returns true if the DownloadService should create a report that shows the number of failed - * downloads when the service shuts down. - */ - boolean shouldCreateReport(); } diff --git a/core/src/main/java/de/danoeh/antennapod/core/PlaybackServiceCallbacks.java b/core/src/main/java/de/danoeh/antennapod/core/PlaybackServiceCallbacks.java index 194ee65ae..3dcaac4dc 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/PlaybackServiceCallbacks.java +++ b/core/src/main/java/de/danoeh/antennapod/core/PlaybackServiceCallbacks.java @@ -19,11 +19,4 @@ public interface PlaybackServiceCallbacks { * @return A non-null activity intent. */ Intent getPlayerActivityIntent(Context context, MediaType mediaType, boolean remotePlayback); - - /** - * Returns true if the PlaybackService should load new episodes from the queue when playback ends - * and false if the PlaybackService should ignore the queue and load no more episodes when playback - * finishes. - */ - boolean useQueue(); } diff --git a/core/src/main/java/de/danoeh/antennapod/core/asynctask/FeedRemover.java b/core/src/main/java/de/danoeh/antennapod/core/asynctask/FeedRemover.java deleted file mode 100644 index 4504b2e7f..000000000 --- a/core/src/main/java/de/danoeh/antennapod/core/asynctask/FeedRemover.java +++ /dev/null @@ -1,61 +0,0 @@ -package de.danoeh.antennapod.core.asynctask; - -import android.app.ProgressDialog; -import android.content.Context; -import android.os.AsyncTask; - -import java.util.concurrent.ExecutionException; - -import de.danoeh.antennapod.core.R; -import de.danoeh.antennapod.core.feed.Feed; -import de.danoeh.antennapod.core.service.playback.PlaybackService; -import de.danoeh.antennapod.core.storage.DBWriter; -import de.danoeh.antennapod.core.util.IntentUtils; - -/** Removes a feed in the background. */ -public class FeedRemover extends AsyncTask<Void, Void, Void> { - private final Context context; - private ProgressDialog dialog; - private final Feed feed; - public boolean skipOnCompletion = false; - - public FeedRemover(Context context, Feed feed) { - super(); - this.context = context; - this.feed = feed; - } - - @Override - protected Void doInBackground(Void... params) { - try { - DBWriter.deleteFeed(context, feed.getId()).get(); - } catch (InterruptedException | ExecutionException e) { - e.printStackTrace(); - } - return null; - } - - @Override - protected void onPostExecute(Void result) { - if(dialog != null && dialog.isShowing()) { - dialog.dismiss(); - } - if(skipOnCompletion) { - IntentUtils.sendLocalBroadcast(context, PlaybackService.ACTION_SKIP_CURRENT_EPISODE); - } - } - - @Override - protected void onPreExecute() { - dialog = new ProgressDialog(context); - dialog.setMessage(context.getString(R.string.feed_remover_msg)); - dialog.setIndeterminate(true); - dialog.setCancelable(false); - dialog.show(); - } - - public void executeAsync() { - executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); - } - -} diff --git a/core/src/main/java/de/danoeh/antennapod/core/backup/OpmlBackupAgent.java b/core/src/main/java/de/danoeh/antennapod/core/backup/OpmlBackupAgent.java index 4c11d0489..c05e2e9f1 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/backup/OpmlBackupAgent.java +++ b/core/src/main/java/de/danoeh/antennapod/core/backup/OpmlBackupAgent.java @@ -20,6 +20,7 @@ import java.io.OutputStreamWriter; import java.io.Reader; import java.io.Writer; import java.math.BigInteger; +import java.nio.charset.Charset; import java.security.DigestInputStream; import java.security.DigestOutputStream; import java.security.MessageDigest; @@ -34,7 +35,6 @@ import de.danoeh.antennapod.core.feed.Feed; import de.danoeh.antennapod.core.storage.DBReader; import de.danoeh.antennapod.core.storage.DownloadRequestException; import de.danoeh.antennapod.core.storage.DownloadRequester; -import de.danoeh.antennapod.core.util.LangUtils; public class OpmlBackupAgent extends BackupAgentHelper { private static final String OPML_BACKUP_KEY = "opml"; @@ -73,9 +73,9 @@ public class OpmlBackupAgent extends BackupAgentHelper { try { digester = MessageDigest.getInstance("MD5"); writer = new OutputStreamWriter(new DigestOutputStream(byteStream, digester), - LangUtils.UTF_8); + Charset.forName("UTF-8")); } catch (NoSuchAlgorithmException e) { - writer = new OutputStreamWriter(byteStream, LangUtils.UTF_8); + writer = new OutputStreamWriter(byteStream, Charset.forName("UTF-8")); } try { @@ -138,9 +138,9 @@ public class OpmlBackupAgent extends BackupAgentHelper { try { digester = MessageDigest.getInstance("MD5"); reader = new InputStreamReader(new DigestInputStream(data, digester), - LangUtils.UTF_8); + Charset.forName("UTF-8")); } catch (NoSuchAlgorithmException e) { - reader = new InputStreamReader(data, LangUtils.UTF_8); + reader = new InputStreamReader(data, Charset.forName("UTF-8")); } try { diff --git a/core/src/main/java/de/danoeh/antennapod/core/feed/FeedItemFilter.java b/core/src/main/java/de/danoeh/antennapod/core/feed/FeedItemFilter.java index b9d79715a..16ab5a171 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/feed/FeedItemFilter.java +++ b/core/src/main/java/de/danoeh/antennapod/core/feed/FeedItemFilter.java @@ -1,10 +1,8 @@ package de.danoeh.antennapod.core.feed; import android.text.TextUtils; -import android.util.Log; import java.util.ArrayList; -import java.util.Arrays; import java.util.List; import de.danoeh.antennapod.core.storage.DBReader; diff --git a/core/src/main/java/de/danoeh/antennapod/core/feed/FeedMedia.java b/core/src/main/java/de/danoeh/antennapod/core/feed/FeedMedia.java index 88945b930..4857e899d 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/feed/FeedMedia.java +++ b/core/src/main/java/de/danoeh/antennapod/core/feed/FeedMedia.java @@ -5,6 +5,7 @@ import android.content.SharedPreferences; import android.content.SharedPreferences.Editor; import android.database.Cursor; import android.media.MediaMetadataRetriever; +import android.net.Uri; import android.os.Parcel; import android.os.Parcelable; import androidx.annotation.Nullable; @@ -165,13 +166,20 @@ public class FeedMedia extends FeedFile implements Playable { */ public MediaBrowserCompat.MediaItem getMediaItem() { Playable p = this; - MediaDescriptionCompat description = new MediaDescriptionCompat.Builder() + MediaDescriptionCompat.Builder builder = new MediaDescriptionCompat.Builder() .setMediaId(String.valueOf(id)) .setTitle(p.getEpisodeTitle()) .setDescription(p.getFeedTitle()) - .setSubtitle(p.getFeedTitle()) - .build(); - return new MediaBrowserCompat.MediaItem(description, MediaBrowserCompat.MediaItem.FLAG_PLAYABLE); + .setSubtitle(p.getFeedTitle()); + if (item != null) { + // getImageLocation() also loads embedded images, which we can not send to external devices + if (item.getImageUrl() != null) { + builder.setIconUri(Uri.parse(item.getImageUrl())); + } else if (item.getFeed() != null && item.getFeed().getImageLocation() != null) { + builder.setIconUri(Uri.parse(item.getFeed().getImageLocation())); + } + } + return new MediaBrowserCompat.MediaItem(builder.build(), MediaBrowserCompat.MediaItem.FLAG_PLAYABLE); } /** diff --git a/core/src/main/java/de/danoeh/antennapod/core/feed/LocalFeedUpdater.java b/core/src/main/java/de/danoeh/antennapod/core/feed/LocalFeedUpdater.java index 971808eb4..d0e15d591 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/feed/LocalFeedUpdater.java +++ b/core/src/main/java/de/danoeh/antennapod/core/feed/LocalFeedUpdater.java @@ -6,14 +6,13 @@ import android.media.MediaMetadataRetriever; import android.net.Uri; import android.text.TextUtils; +import androidx.annotation.NonNull; import androidx.documentfile.provider.DocumentFile; -import org.apache.commons.lang3.StringUtils; - +import java.io.IOException; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.ArrayList; -import java.util.Arrays; import java.util.Collections; import java.util.Date; import java.util.HashSet; @@ -22,7 +21,6 @@ import java.util.List; import java.util.Locale; import java.util.Set; import java.util.UUID; -import java.util.concurrent.ExecutionException; import de.danoeh.antennapod.core.R; import de.danoeh.antennapod.core.service.download.DownloadStatus; @@ -34,17 +32,31 @@ import de.danoeh.antennapod.core.util.DownloadError; public class LocalFeedUpdater { + static final String[] PREFERRED_FEED_IMAGE_FILENAMES = { "folder.jpg", "Folder.jpg", "folder.png", "Folder.png" }; + public static void updateFeed(Feed feed, Context context) { + try { + tryUpdateFeed(feed, context); + + if (mustReportDownloadSuccessful(feed)) { + reportSuccess(feed); + } + } catch (Exception e) { + e.printStackTrace(); + reportError(feed, e.getMessage()); + } + } + + private static void tryUpdateFeed(Feed feed, Context context) throws IOException { String uriString = feed.getDownload_url().replace(Feed.PREFIX_LOCAL_FOLDER, ""); DocumentFile documentFolder = DocumentFile.fromTreeUri(context, Uri.parse(uriString)); if (documentFolder == null) { - reportError(feed, "Unable to retrieve document tree." + throw new IOException("Unable to retrieve document tree. " + "Try re-connecting the folder on the podcast info page."); - return; } if (!documentFolder.exists() || !documentFolder.canRead()) { - reportError(feed, "Cannot read local directory. Try re-connecting the folder on the podcast info page."); - return; + throw new IOException("Cannot read local directory. " + + "Try re-connecting the folder on the podcast info page."); } if (feed.getItems() == null) { @@ -85,37 +97,43 @@ public class LocalFeedUpdater { } } - List<String> iconLocations = Arrays.asList("folder.jpg", "Folder.jpg", "folder.png", "Folder.png"); - for (String iconLocation : iconLocations) { - DocumentFile image = documentFolder.findFile(iconLocation); - if (image != null) { - feed.setImageUrl(image.getUri().toString()); - break; - } - } - if (StringUtils.isBlank(feed.getImageUrl())) { - // set default feed image - feed.setImageUrl(getDefaultIconUrl(context)); - } - if (feed.getPreferences().getAutoDownload()) { - feed.getPreferences().setAutoDownload(false); - feed.getPreferences().setAutoDeleteAction(FeedPreferences.AutoDeleteAction.NO); - try { - DBWriter.setFeedPreferences(feed.getPreferences()).get(); - } catch (ExecutionException | InterruptedException e) { - e.printStackTrace(); - } - } + feed.setImageUrl(getImageUrl(context, documentFolder)); + + feed.getPreferences().setAutoDownload(false); + feed.getPreferences().setAutoDeleteAction(FeedPreferences.AutoDeleteAction.NO); + feed.setDescription(context.getString(R.string.local_feed_description)); + feed.setAuthor(context.getString(R.string.local_folder)); // update items, delete items without existing file; // only delete items if the folder contains at least one element to avoid accidentally // deleting played state or position in case the folder is temporarily unavailable. boolean removeUnlistedItems = (newItems.size() >= 1); DBTasks.updateFeed(context, feed, removeUnlistedItems); + } - if (mustReportDownloadSuccessful(feed)) { - reportSuccess(feed); + /** + * Returns the image URL for the local feed. + */ + @NonNull + static String getImageUrl(@NonNull Context context, @NonNull DocumentFile documentFolder) { + // look for special file names + for (String iconLocation : PREFERRED_FEED_IMAGE_FILENAMES) { + DocumentFile image = documentFolder.findFile(iconLocation); + if (image != null) { + return image.getUri().toString(); + } } + + // use the first image in the folder if existing + for (DocumentFile file : documentFolder.listFiles()) { + String mime = file.getType(); + if (mime != null && (mime.startsWith("image/jpeg") || mime.startsWith("image/png"))) { + return file.getUri().toString(); + } + } + + // use default icon as fallback + return getDefaultIconUrl(context); } /** @@ -139,46 +157,50 @@ public class LocalFeedUpdater { } private static FeedItem createFeedItem(Feed feed, DocumentFile file, Context context) { - String uuid = UUID.randomUUID().toString(); + FeedItem item = new FeedItem(0, file.getName(), UUID.randomUUID().toString(), + file.getName(), new Date(file.lastModified()), FeedItem.UNPLAYED, feed); + item.setAutoDownload(false); + + long size = file.length(); + FeedMedia media = new FeedMedia(0, item, 0, 0, size, file.getType(), + file.getUri().toString(), file.getUri().toString(), false, null, 0, 0); + item.setMedia(media); + + try { + loadMetadata(item, file, context); + } catch (Exception e) { + item.setDescription(e.getMessage()); + } + + return item; + } + private static void loadMetadata(FeedItem item, DocumentFile file, Context context) { MediaMetadataRetriever mediaMetadataRetriever = new MediaMetadataRetriever(); mediaMetadataRetriever.setDataSource(context, file.getUri()); - String dateStr = mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DATE); - Date date = null; + String dateStr = mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DATE); if (!TextUtils.isEmpty(dateStr)) { try { SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyyMMdd'T'HHmmss", Locale.getDefault()); - date = simpleDateFormat.parse(dateStr); + item.setPubDate(simpleDateFormat.parse(dateStr)); } catch (ParseException parseException) { - date = DateUtils.parse(dateStr); - if (date == null) { - date = new Date(file.lastModified()); + Date date = DateUtils.parse(dateStr); + if (date != null) { + item.setPubDate(date); } } - } else { - date = new Date(file.lastModified()); } - FeedItem item = new FeedItem(0, file.getName(), uuid, file.getName(), date, - FeedItem.UNPLAYED, feed); - item.setAutoDownload(false); - - String durationStr = mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION); String title = mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_TITLE); if (!TextUtils.isEmpty(title)) { item.setTitle(title); } - //add the media to the item - long duration = Long.parseLong(durationStr); - long size = file.length(); - FeedMedia media = new FeedMedia(0, item, (int) duration, 0, size, file.getType(), - file.getUri().toString(), file.getUri().toString(), false, null, 0, 0); - media.setHasEmbeddedPicture(mediaMetadataRetriever.getEmbeddedPicture() != null); - item.setMedia(media); + String durationStr = mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION); + item.getMedia().setDuration((int) Long.parseLong(durationStr)); - return item; + item.getMedia().setHasEmbeddedPicture(mediaMetadataRetriever.getEmbeddedPicture() != null); } private static void reportError(Feed feed, String reasonDetailed) { diff --git a/core/src/main/java/de/danoeh/antennapod/core/feed/util/PlaybackSpeedUtils.java b/core/src/main/java/de/danoeh/antennapod/core/feed/util/PlaybackSpeedUtils.java index 0d5ecbb71..d6740994d 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/feed/util/PlaybackSpeedUtils.java +++ b/core/src/main/java/de/danoeh/antennapod/core/feed/util/PlaybackSpeedUtils.java @@ -1,5 +1,6 @@ package de.danoeh.antennapod.core.feed.util; +import android.util.Log; import de.danoeh.antennapod.core.feed.Feed; import de.danoeh.antennapod.core.feed.FeedItem; import de.danoeh.antennapod.core.feed.FeedMedia; @@ -14,6 +15,7 @@ import static de.danoeh.antennapod.core.feed.FeedPreferences.SPEED_USE_GLOBAL; * Utility class to use the appropriate playback speed based on {@link PlaybackPreferences} */ public final class PlaybackSpeedUtils { + private static final String TAG = "PlaybackSpeedUtils"; private PlaybackSpeedUtils() { } @@ -33,8 +35,10 @@ public final class PlaybackSpeedUtils { FeedItem item = ((FeedMedia) media).getItem(); if (item != null) { Feed feed = item.getFeed(); - if (feed != null) { + if (feed != null && feed.getPreferences() != null) { playbackSpeed = feed.getPreferences().getFeedPlaybackSpeed(); + } else { + Log.d(TAG, "Can not get feed specific playback speed: " + feed); } } } diff --git a/core/src/main/java/de/danoeh/antennapod/core/glide/ApGlideModule.java b/core/src/main/java/de/danoeh/antennapod/core/glide/ApGlideModule.java index ab4247cef..b3adc567e 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/glide/ApGlideModule.java +++ b/core/src/main/java/de/danoeh/antennapod/core/glide/ApGlideModule.java @@ -10,7 +10,6 @@ import com.bumptech.glide.annotation.GlideModule; import com.bumptech.glide.load.DecodeFormat; import com.bumptech.glide.load.engine.cache.InternalCacheDiskCacheFactory; import com.bumptech.glide.load.model.StringLoader; -import com.bumptech.glide.load.model.UriLoader; import com.bumptech.glide.module.AppGlideModule; import de.danoeh.antennapod.core.util.EmbeddedChapterImage; diff --git a/core/src/main/java/de/danoeh/antennapod/core/glide/ChapterImageModelLoader.java b/core/src/main/java/de/danoeh/antennapod/core/glide/ChapterImageModelLoader.java index 35a9d987b..519d625e2 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/glide/ChapterImageModelLoader.java +++ b/core/src/main/java/de/danoeh/antennapod/core/glide/ChapterImageModelLoader.java @@ -10,17 +10,13 @@ import com.bumptech.glide.load.model.ModelLoader; import com.bumptech.glide.load.model.ModelLoaderFactory; import com.bumptech.glide.load.model.MultiModelLoaderFactory; import com.bumptech.glide.signature.ObjectKey; -import de.danoeh.antennapod.core.ClientConfig; -import de.danoeh.antennapod.core.feed.Chapter; import de.danoeh.antennapod.core.service.download.AntennapodHttpClient; import de.danoeh.antennapod.core.util.EmbeddedChapterImage; import java.io.BufferedInputStream; import java.io.File; import java.io.FileInputStream; import java.io.IOException; -import java.net.URL; import java.nio.ByteBuffer; -import okhttp3.OkHttpClient; import okhttp3.Request; import okhttp3.Response; import org.apache.commons.io.IOUtils; diff --git a/core/src/main/java/de/danoeh/antennapod/core/preferences/PlaybackPreferences.java b/core/src/main/java/de/danoeh/antennapod/core/preferences/PlaybackPreferences.java index 08ea27434..95b828e28 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/preferences/PlaybackPreferences.java +++ b/core/src/main/java/de/danoeh/antennapod/core/preferences/PlaybackPreferences.java @@ -100,7 +100,7 @@ public class PlaybackPreferences implements SharedPreferences.OnSharedPreference } public void onSharedPreferenceChanged(SharedPreferences sharedPreferences, String key) { - if (key.equals(PREF_CURRENT_PLAYER_STATUS)) { + if (PREF_CURRENT_PLAYER_STATUS.equals(key)) { EventBus.getDefault().post(new PlayerStatusEvent()); } } diff --git a/core/src/main/java/de/danoeh/antennapod/core/preferences/UserPreferences.java b/core/src/main/java/de/danoeh/antennapod/core/preferences/UserPreferences.java index 56dd95fe6..ed9c519a6 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/preferences/UserPreferences.java +++ b/core/src/main/java/de/danoeh/antennapod/core/preferences/UserPreferences.java @@ -301,10 +301,30 @@ public class UserPreferences { * @return {@code true} if download reports are shown, {@code false} otherwise */ public static boolean showDownloadReport() { + if (Build.VERSION.SDK_INT >= 26) { + return true; // System handles notification preferences + } + return prefs.getBoolean(PREF_SHOW_DOWNLOAD_REPORT, true); + } + + /** + * Used for migration of the preference to system notification channels. + */ + public static boolean getShowDownloadReportRaw() { return prefs.getBoolean(PREF_SHOW_DOWNLOAD_REPORT, true); } public static boolean showAutoDownloadReport() { + if (Build.VERSION.SDK_INT >= 26) { + return true; // System handles notification preferences + } + return prefs.getBoolean(PREF_SHOW_AUTO_DOWNLOAD_REPORT, false); + } + + /** + * Used for migration of the preference to system notification channels. + */ + public static boolean getShowAutoDownloadReportRaw() { return prefs.getBoolean(PREF_SHOW_AUTO_DOWNLOAD_REPORT, false); } @@ -728,6 +748,16 @@ public class UserPreferences { } public static boolean gpodnetNotificationsEnabled() { + if (Build.VERSION.SDK_INT >= 26) { + return true; // System handles notification preferences + } + return prefs.getBoolean(PREF_GPODNET_NOTIFICATIONS, true); + } + + /** + * Used for migration of the preference to system notification channels. + */ + public static boolean getGpodnetNotificationsEnabledRaw() { return prefs.getBoolean(PREF_GPODNET_NOTIFICATIONS, true); } diff --git a/core/src/main/java/de/danoeh/antennapod/core/receiver/PlayerWidget.java b/core/src/main/java/de/danoeh/antennapod/core/receiver/PlayerWidget.java index 2e592bdf5..9e9663205 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/receiver/PlayerWidget.java +++ b/core/src/main/java/de/danoeh/antennapod/core/receiver/PlayerWidget.java @@ -16,6 +16,9 @@ public class PlayerWidget extends AppWidgetProvider { public static final String PREFS_NAME = "PlayerWidgetPrefs"; private static final String KEY_ENABLED = "WidgetEnabled"; public static final String KEY_WIDGET_COLOR = "widget_color"; + public static final String KEY_WIDGET_SKIP = "widget_skip"; + public static final String KEY_WIDGET_FAST_FORWARD = "widget_fast_forward"; + public static final String KEY_WIDGET_REWIND = "widget_rewind"; public static final int DEFAULT_COLOR = 0x00262C31; @Override @@ -52,6 +55,9 @@ public class PlayerWidget extends AppWidgetProvider { for (int appWidgetId : appWidgetIds) { SharedPreferences prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE); prefs.edit().remove(KEY_WIDGET_COLOR + appWidgetId).apply(); + prefs.edit().remove(KEY_WIDGET_REWIND + appWidgetId).apply(); + prefs.edit().remove(KEY_WIDGET_FAST_FORWARD + appWidgetId).apply(); + prefs.edit().remove(KEY_WIDGET_SKIP + appWidgetId).apply(); } super.onDeleted(context, appWidgetIds); } diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/PlayerWidgetJobService.java b/core/src/main/java/de/danoeh/antennapod/core/service/PlayerWidgetJobService.java index 74735a264..5af05b6d2 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/service/PlayerWidgetJobService.java +++ b/core/src/main/java/de/danoeh/antennapod/core/service/PlayerWidgetJobService.java @@ -102,9 +102,10 @@ public class PlayerWidgetJobService extends SafeJobIntentService { ComponentName playerWidget = new ComponentName(this, PlayerWidget.class); AppWidgetManager manager = AppWidgetManager.getInstance(this); int[] widgetIds = manager.getAppWidgetIds(playerWidget); - RemoteViews views = new RemoteViews(getPackageName(), R.layout.player_widget); final PendingIntent startMediaPlayer = PendingIntent.getActivity(this, R.id.pending_intent_player_activity, PlaybackService.getPlayerActivityIntent(this), PendingIntent.FLAG_UPDATE_CURRENT); + RemoteViews views; + views = new RemoteViews(getPackageName(), R.layout.player_widget); boolean nothingPlaying = false; Playable media; @@ -119,6 +120,7 @@ public class PlayerWidgetJobService extends SafeJobIntentService { if (media != null) { views.setOnClickPendingIntent(R.id.layout_left, startMediaPlayer); + views.setOnClickPendingIntent(R.id.imgvCover, startMediaPlayer); try { Bitmap icon; @@ -155,11 +157,18 @@ public class PlayerWidgetJobService extends SafeJobIntentService { if (status == PlayerStatus.PLAYING) { views.setImageViewResource(R.id.butPlay, R.drawable.ic_av_pause_white_48dp); views.setContentDescription(R.id.butPlay, getString(R.string.pause_label)); + views.setImageViewResource(R.id.butPlayExtended, R.drawable.ic_av_pause_white_48dp); + views.setContentDescription(R.id.butPlayExtended, getString(R.string.pause_label)); } else { views.setImageViewResource(R.id.butPlay, R.drawable.ic_av_play_white_48dp); views.setContentDescription(R.id.butPlay, getString(R.string.play_label)); + views.setImageViewResource(R.id.butPlayExtended, R.drawable.ic_av_play_white_48dp); + views.setContentDescription(R.id.butPlayExtended, getString(R.string.play_label)); } - views.setOnClickPendingIntent(R.id.butPlay, createMediaButtonIntent()); + views.setOnClickPendingIntent(R.id.butPlay, + createMediaButtonIntent(KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE)); + views.setOnClickPendingIntent(R.id.butPlayExtended, + createMediaButtonIntent(KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE)); } else { nothingPlaying = true; } @@ -168,16 +177,20 @@ public class PlayerWidgetJobService extends SafeJobIntentService { // start the app if they click anything views.setOnClickPendingIntent(R.id.layout_left, startMediaPlayer); views.setOnClickPendingIntent(R.id.butPlay, startMediaPlayer); + views.setOnClickPendingIntent(R.id.butPlayExtended, + createMediaButtonIntent(KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE)); views.setViewVisibility(R.id.txtvProgress, View.GONE); views.setViewVisibility(R.id.txtvTitle, View.GONE); views.setViewVisibility(R.id.txtNoPlaying, View.VISIBLE); views.setImageViewResource(R.id.imgvCover, R.mipmap.ic_launcher_round); views.setImageViewResource(R.id.butPlay, R.drawable.ic_av_play_white_48dp); + views.setImageViewResource(R.id.butPlayExtended, R.drawable.ic_av_play_white_48dp); } if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.JELLY_BEAN) { for (int id : widgetIds) { Bundle options = manager.getAppWidgetOptions(id); + SharedPreferences prefs = getSharedPreferences(PlayerWidget.PREFS_NAME, Context.MODE_PRIVATE); int minWidth = options.getInt(AppWidgetManager.OPTION_APPWIDGET_MIN_WIDTH); int columns = getCellsForSize(minWidth); if (columns < 3) { @@ -185,8 +198,18 @@ public class PlayerWidgetJobService extends SafeJobIntentService { } else { views.setViewVisibility(R.id.layout_center, View.VISIBLE); } + boolean showRewind = prefs.getBoolean(PlayerWidget.KEY_WIDGET_REWIND + id, false); + boolean showFastForward = prefs.getBoolean(PlayerWidget.KEY_WIDGET_FAST_FORWARD + id, false); + boolean showSkip = prefs.getBoolean(PlayerWidget.KEY_WIDGET_SKIP + id, false); + + if (showRewind || showSkip || showFastForward) { + views.setInt(R.id.extendedButtonsContainer, "setVisibility", View.VISIBLE); + views.setInt(R.id.butPlay, "setVisibility", View.GONE); + views.setInt(R.id.butRew, "setVisibility", showRewind ? View.VISIBLE : View.GONE); + views.setInt(R.id.butFastForward, "setVisibility", showFastForward ? View.VISIBLE : View.GONE); + views.setInt(R.id.butSkip, "setVisibility", showSkip ? View.VISIBLE : View.GONE); + } - SharedPreferences prefs = getSharedPreferences(PlayerWidget.PREFS_NAME, Context.MODE_PRIVATE); int backgroundColor = prefs.getInt(PlayerWidget.KEY_WIDGET_COLOR + id, PlayerWidget.DEFAULT_COLOR); views.setInt(R.id.widgetLayout, "setBackgroundColor", backgroundColor); @@ -200,13 +223,13 @@ public class PlayerWidgetJobService extends SafeJobIntentService { /** * Creates an intent which fakes a mediabutton press */ - private PendingIntent createMediaButtonIntent() { - KeyEvent event = new KeyEvent(KeyEvent.ACTION_DOWN, KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE); + private PendingIntent createMediaButtonIntent(int eventCode) { + KeyEvent event = new KeyEvent(KeyEvent.ACTION_DOWN, eventCode); Intent startingIntent = new Intent(getBaseContext(), MediaButtonReceiver.class); startingIntent.setAction(MediaButtonReceiver.NOTIFY_BUTTON_RECEIVER); startingIntent.putExtra(Intent.EXTRA_KEY_EVENT, event); - return PendingIntent.getBroadcast(this, 0, startingIntent, 0); + return PendingIntent.getBroadcast(this, eventCode, startingIntent, 0); } private String getProgressString(int position, int duration, float speed) { 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 de106a01e..6dbc4a82f 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 @@ -25,7 +25,6 @@ import org.greenrobot.eventbus.EventBus; import java.io.File; import java.io.IOException; -import java.net.HttpURLConnection; import java.util.ArrayList; import java.util.Collections; import java.util.List; @@ -39,7 +38,6 @@ import java.util.concurrent.ScheduledThreadPoolExecutor; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; -import de.danoeh.antennapod.core.ClientConfig; import de.danoeh.antennapod.core.event.DownloadEvent; import de.danoeh.antennapod.core.event.FeedItemEvent; import de.danoeh.antennapod.core.feed.Feed; @@ -168,6 +166,7 @@ public class DownloadService extends Service { startForeground(R.id.notification_downloading, notification); syncExecutor.execute(() -> onDownloadQueued(intent)); } else if (numberOfDownloads.get() == 0) { + stopForeground(true); stopSelf(); } else { Log.d(TAG, "onStartCommand: Unknown intent"); @@ -205,8 +204,7 @@ public class DownloadService extends Service { isRunning = false; boolean showAutoDownloadReport = UserPreferences.showAutoDownloadReport(); - if (ClientConfig.downloadServiceCallbacks.shouldCreateReport() - && (UserPreferences.showDownloadReport() || showAutoDownloadReport)) { + if (UserPreferences.showDownloadReport() || showAutoDownloadReport) { notificationManager.updateReport(reportQueue, showAutoDownloadReport); reportQueue.clear(); } @@ -326,18 +324,11 @@ public class DownloadService extends Service { if (item == null) { return; } - boolean httpNotFound = status.getReason() == DownloadError.ERROR_HTTP_DATA_ERROR - && String.valueOf(HttpURLConnection.HTTP_NOT_FOUND).equals(status.getReasonDetailed()); - boolean forbidden = status.getReason() == DownloadError.ERROR_FORBIDDEN - && String.valueOf(HttpURLConnection.HTTP_FORBIDDEN).equals(status.getReasonDetailed()); - boolean notEnoughSpace = status.getReason() == DownloadError.ERROR_NOT_ENOUGH_SPACE; - boolean wrongFileType = status.getReason() == DownloadError.ERROR_FILE_TYPE; - boolean httpGone = status.getReason() == DownloadError.ERROR_HTTP_DATA_ERROR - && String.valueOf(HttpURLConnection.HTTP_GONE).equals(status.getReasonDetailed()); - boolean httpBadReq = status.getReason() == DownloadError.ERROR_HTTP_DATA_ERROR - && String.valueOf(HttpURLConnection.HTTP_BAD_REQUEST).equals(status.getReasonDetailed()); - - if (httpNotFound || forbidden || notEnoughSpace || wrongFileType || httpGone || httpBadReq ) { + boolean unknownHost = status.getReason() == DownloadError.ERROR_UNKNOWN_HOST; + boolean unsupportedType = status.getReason() == DownloadError.ERROR_UNSUPPORTED_TYPE; + boolean wrongSize = status.getReason() == DownloadError.ERROR_IO_WRONG_SIZE; + + if (! (unknownHost || unsupportedType || wrongSize)) { try { DBWriter.saveFeedItemAutoDownloadFailed(item).get(); } catch (ExecutionException | InterruptedException e) { @@ -429,7 +420,7 @@ public class DownloadService extends Service { + ", cleanupMedia=" + cleanupMedia); if (cleanupMedia) { - ClientConfig.dbTasksCallbacks.getEpisodeCacheCleanupAlgorithm() + UserPreferences.getEpisodeCleanupAlgorithm() .makeRoomForEpisodes(getApplicationContext(), requests.size()); } @@ -553,6 +544,7 @@ public class DownloadService extends Service { if (numberOfDownloads.get() <= 0 && DownloadRequester.getInstance().hasNoDownloads()) { Log.d(TAG, "Number of downloads is " + numberOfDownloads.get() + ", attempting shutdown"); + stopForeground(true); stopSelf(); if (notificationUpdater != null) { notificationUpdater.run(); 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 0715d50dd..fb6009c02 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 @@ -148,7 +148,7 @@ public class DownloadServiceNotification { id = R.id.notification_auto_download_report; content = createAutoDownloadNotificationContent(reportQueue); } else { - channelId = NotificationUtils.CHANNEL_ID_ERROR; + channelId = NotificationUtils.CHANNEL_ID_DOWNLOAD_ERROR; titleId = R.string.download_report_title; iconId = R.drawable.ic_notification_sync_error; intent = ClientConfig.downloadServiceCallbacks.getReportNotificationContentIntent(context); diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/download/HttpDownloader.java b/core/src/main/java/de/danoeh/antennapod/core/service/download/HttpDownloader.java index 65b7ed7d1..b553a9d1f 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/service/download/HttpDownloader.java +++ b/core/src/main/java/de/danoeh/antennapod/core/service/download/HttpDownloader.java @@ -4,7 +4,6 @@ import androidx.annotation.NonNull; import android.text.TextUtils; import android.util.Log; -import de.danoeh.antennapod.core.service.BasicAuthorizationInterceptor; import okhttp3.CacheControl; import org.apache.commons.io.IOUtils; @@ -21,14 +20,12 @@ import java.net.UnknownHostException; import java.util.Collections; import java.util.Date; -import de.danoeh.antennapod.core.ClientConfig; import de.danoeh.antennapod.core.R; import de.danoeh.antennapod.core.feed.FeedMedia; import de.danoeh.antennapod.core.util.DateUtils; import de.danoeh.antennapod.core.util.DownloadError; import de.danoeh.antennapod.core.util.StorageUtils; import de.danoeh.antennapod.core.util.URIUtil; -import okhttp3.Interceptor; import okhttp3.OkHttpClient; import okhttp3.Protocol; import okhttp3.Request; @@ -226,7 +223,7 @@ public class HttpDownloader extends Downloader { // written file. This check cannot be made if compression was used if (!isGzip && request.getSize() != DownloadStatus.SIZE_UNKNOWN && request.getSoFar() != request.getSize()) { - onFail(DownloadError.ERROR_IO_ERROR, "Download completed but size: " + + onFail(DownloadError.ERROR_IO_WRONG_SIZE, "Download completed but size: " + request.getSoFar() + " does not equal expected size " + request.getSize()); return; } else if (request.getSize() > 0 && request.getSoFar() == 0) { diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/download/handler/FailedDownloadHandler.java b/core/src/main/java/de/danoeh/antennapod/core/service/download/handler/FailedDownloadHandler.java index 041d26bd4..386e5e6f7 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/service/download/handler/FailedDownloadHandler.java +++ b/core/src/main/java/de/danoeh/antennapod/core/service/download/handler/FailedDownloadHandler.java @@ -3,7 +3,6 @@ package de.danoeh.antennapod.core.service.download.handler; import android.util.Log; import de.danoeh.antennapod.core.feed.Feed; import de.danoeh.antennapod.core.service.download.DownloadRequest; -import de.danoeh.antennapod.core.service.download.DownloadStatus; import de.danoeh.antennapod.core.storage.DBWriter; /** diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/playback/ExoPlayerWrapper.java b/core/src/main/java/de/danoeh/antennapod/core/service/playback/ExoPlayerWrapper.java index 71bbf2efd..9a8248984 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/service/playback/ExoPlayerWrapper.java +++ b/core/src/main/java/de/danoeh/antennapod/core/service/playback/ExoPlayerWrapper.java @@ -2,6 +2,7 @@ package de.danoeh.antennapod.core.service.playback; import android.content.Context; import android.net.Uri; +import android.text.TextUtils; import android.util.Log; import android.view.SurfaceHolder; import com.google.android.exoplayer2.C; @@ -28,8 +29,10 @@ import com.google.android.exoplayer2.upstream.DataSource; import com.google.android.exoplayer2.upstream.DefaultDataSourceFactory; import com.google.android.exoplayer2.upstream.DefaultHttpDataSource; import com.google.android.exoplayer2.upstream.DefaultHttpDataSourceFactory; + import de.danoeh.antennapod.core.ClientConfig; import de.danoeh.antennapod.core.preferences.UserPreferences; +import de.danoeh.antennapod.core.service.download.HttpDownloader; import de.danoeh.antennapod.core.util.playback.IPlayer; import io.reactivex.Observable; import io.reactivex.android.schedulers.AndroidSchedulers; @@ -184,14 +187,22 @@ public class ExoPlayerWrapper implements IPlayer { exoPlayer.setAudioAttributes(b.build()); } - @Override - public void setDataSource(String s) throws IllegalArgumentException, IllegalStateException { + public void setDataSource(String s, String user, String password) + throws IllegalArgumentException, IllegalStateException { Log.d(TAG, "setDataSource: " + s); DefaultHttpDataSourceFactory httpDataSourceFactory = new DefaultHttpDataSourceFactory( ClientConfig.USER_AGENT, null, DefaultHttpDataSource.DEFAULT_CONNECT_TIMEOUT_MILLIS, DefaultHttpDataSource.DEFAULT_READ_TIMEOUT_MILLIS, true); + + if (!TextUtils.isEmpty(user) && !TextUtils.isEmpty(password)) { + httpDataSourceFactory.getDefaultRequestProperties().set("Authorization", + HttpDownloader.encodeCredentials( + user, + password, + "ISO-8859-1")); + } DataSource.Factory dataSourceFactory = new DefaultDataSourceFactory(context, null, httpDataSourceFactory); DefaultExtractorsFactory extractorsFactory = new DefaultExtractorsFactory(); extractorsFactory.setConstantBitrateSeekingEnabled(true); @@ -200,6 +211,11 @@ public class ExoPlayerWrapper implements IPlayer { } @Override + public void setDataSource(String s) throws IllegalArgumentException, IllegalStateException { + setDataSource(s, null, null); + } + + @Override public void setDisplay(SurfaceHolder sh) { exoPlayer.setVideoSurfaceHolder(sh); } diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/playback/LocalPSMP.java b/core/src/main/java/de/danoeh/antennapod/core/service/playback/LocalPSMP.java index ae5d62872..98280f54d 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/service/playback/LocalPSMP.java +++ b/core/src/main/java/de/danoeh/antennapod/core/service/playback/LocalPSMP.java @@ -1,7 +1,6 @@ package de.danoeh.antennapod.core.service.playback; import android.content.Context; -import android.media.AudioAttributes; import android.media.AudioManager; import android.os.PowerManager; import androidx.annotation.NonNull; @@ -261,7 +260,16 @@ public class LocalPSMP extends PlaybackServiceMediaPlayer { callback.onMediaChanged(false); setPlaybackParams(PlaybackSpeedUtils.getCurrentPlaybackSpeed(media), UserPreferences.isSkipSilence()); if (stream) { - mediaPlayer.setDataSource(media.getStreamUrl()); + if (playable instanceof FeedMedia) { + FeedMedia feedMedia = (FeedMedia) playable; + FeedPreferences preferences = feedMedia.getItem().getFeed().getPreferences(); + mediaPlayer.setDataSource( + media.getStreamUrl(), + preferences.getUsername(), + preferences.getPassword()); + } else { + mediaPlayer.setDataSource(media.getStreamUrl()); + } } else if (media.getLocalMediaUrl() != null && new File(media.getLocalMediaUrl()).canRead()) { mediaPlayer.setDataSource(media.getLocalMediaUrl()); } else { diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackService.java b/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackService.java index 60075dda6..c1500d78b 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackService.java +++ b/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackService.java @@ -7,6 +7,7 @@ import android.app.UiModeManager; import android.bluetooth.BluetoothA2dp; import android.content.BroadcastReceiver; import android.content.ComponentName; +import android.content.ContentResolver; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; @@ -51,7 +52,6 @@ import java.util.concurrent.TimeUnit; import de.danoeh.antennapod.core.ClientConfig; import de.danoeh.antennapod.core.R; -import de.danoeh.antennapod.core.event.FeedItemEvent; import de.danoeh.antennapod.core.event.MessageEvent; import de.danoeh.antennapod.core.event.PlaybackPositionEvent; import de.danoeh.antennapod.core.event.ServiceEvent; @@ -149,7 +149,7 @@ public class PlaybackService extends MediaBrowserServiceCompat { public static final String ACTION_PAUSE_PLAY_CURRENT_EPISODE = "action.de.danoeh.antennapod.core.service.pausePlayCurrentEpisode"; /** - * Custom action used by Android Wear + * Custom action used by Android Wear, Android Auto */ private static final String CUSTOM_ACTION_FAST_FORWARD = "action.de.danoeh.antennapod.core.service.fastForward"; private static final String CUSTOM_ACTION_REWIND = "action.de.danoeh.antennapod.core.service.rewind"; @@ -370,9 +370,26 @@ public class PlaybackService extends MediaBrowserServiceCompat { } private MediaBrowserCompat.MediaItem createBrowsableMediaItemForRoot() { + Uri uri = new Uri.Builder() + .scheme(ContentResolver.SCHEME_ANDROID_RESOURCE) + .authority(getResources().getResourcePackageName(R.drawable.ic_playlist_black)) + .appendPath(getResources().getResourceTypeName(R.drawable.ic_playlist_black)) + .appendPath(getResources().getResourceEntryName(R.drawable.ic_playlist_black)) + .build(); + + String subtitle = ""; + try { + int count = taskManager.getQueue().size(); + subtitle = getResources().getQuantityString(R.plurals.num_episodes, count, count); + } catch (InterruptedException e) { + e.printStackTrace(); + } + MediaDescriptionCompat description = new MediaDescriptionCompat.Builder() + .setIconUri(uri) .setMediaId(getResources().getString(R.string.queue_label)) .setTitle(getResources().getString(R.string.queue_label)) + .setSubtitle(subtitle) .build(); return new MediaBrowserCompat.MediaItem(description, MediaBrowserCompat.MediaItem.FLAG_BROWSABLE); @@ -517,11 +534,12 @@ public class PlaybackService extends MediaBrowserServiceCompat { .observeOn(AndroidSchedulers.mainThread()) .subscribe( playableLoaded -> { - mediaPlayer.playMediaObject(playable, stream, startWhenPrepared, + mediaPlayer.playMediaObject(playableLoaded, stream, startWhenPrepared, prepareImmediately); - addPlayableToQueue(playable); + addPlayableToQueue(playableLoaded); }, error -> { Log.d(TAG, "Playable was not found. Stopping service."); + error.printStackTrace(); stateManager.stopService(); }); return Service.START_NOT_STICKY; @@ -729,6 +747,7 @@ public class PlaybackService extends MediaBrowserServiceCompat { addPlayableToQueue(playable); }, error -> { Log.d(TAG, "Playable was not loaded from preferences. Stopping service."); + error.printStackTrace(); stateManager.stopService(); }); } @@ -971,10 +990,6 @@ public class PlaybackService extends MediaBrowserServiceCompat { Log.d(TAG, "getNextInQueue(), but playable not an instance of FeedMedia, so not proceeding"); return null; } - if (!ClientConfig.playbackServiceCallbacks.useQueue()) { - Log.d(TAG, "getNextInQueue(), but queue not in use by this app"); - return null; - } Log.d(TAG, "getNextInQueue()"); FeedMedia media = (FeedMedia) currentMedia; try { @@ -1022,6 +1037,7 @@ public class PlaybackService extends MediaBrowserServiceCompat { */ private void onPlaybackEnded(MediaType mediaType, boolean stopPlaying) { Log.d(TAG, "Playback ended"); + PlaybackPreferences.clearCurrentlyPlayingTemporaryPlaybackSpeed(); if (stopPlaying) { taskManager.cancelPositionSaver(); cancelPositionObserver(); @@ -1060,7 +1076,6 @@ public class PlaybackService extends MediaBrowserServiceCompat { */ private void onPostPlayback(final Playable playable, boolean ended, boolean skipped, boolean playingNext) { - PlaybackPreferences.clearCurrentlyPlayingTemporaryPlaybackSpeed(); if (playable == null) { Log.e(TAG, "Cannot do post-playback processing: media was null"); return; @@ -1206,6 +1221,7 @@ public class PlaybackService extends MediaBrowserServiceCompat { sessionState.setState(state, getCurrentPosition(), getCurrentPlaybackSpeed()); long capabilities = PlaybackStateCompat.ACTION_PLAY_PAUSE | PlaybackStateCompat.ACTION_REWIND + | PlaybackStateCompat.ACTION_PAUSE | PlaybackStateCompat.ACTION_FAST_FORWARD | PlaybackStateCompat.ACTION_SKIP_TO_NEXT | PlaybackStateCompat.ACTION_SEEK_TO; @@ -1236,17 +1252,24 @@ public class PlaybackService extends MediaBrowserServiceCompat { CUSTOM_ACTION_FAST_FORWARD, getString(R.string.fast_forward_label), R.drawable.ic_notification_fast_forward) .build()); + } else { + // This would give the PIP of videos a play button + capabilities = capabilities | PlaybackStateCompat.ACTION_PLAY; + if (uiModeManager.getCurrentModeType() == Configuration.UI_MODE_TYPE_WATCH) { + flavorHelper.sessionStateAddActionForWear(sessionState, + CUSTOM_ACTION_REWIND, + getString(R.string.rewind_label), + android.R.drawable.ic_media_rew); + flavorHelper.sessionStateAddActionForWear(sessionState, + CUSTOM_ACTION_FAST_FORWARD, + getString(R.string.fast_forward_label), + android.R.drawable.ic_media_ff); + flavorHelper.mediaSessionSetExtraForWear(mediaSession); + } } sessionState.setActions(capabilities); - flavorHelper.sessionStateAddActionForWear(sessionState, - CUSTOM_ACTION_REWIND, getString(R.string.rewind_label), android.R.drawable.ic_media_rew); - flavorHelper.sessionStateAddActionForWear(sessionState, - CUSTOM_ACTION_FAST_FORWARD, getString(R.string.fast_forward_label), android.R.drawable.ic_media_ff); - - flavorHelper.mediaSessionSetExtraForWear(mediaSession); - mediaSession.setPlaybackState(sessionState.build()); } diff --git a/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceNotificationBuilder.java b/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceNotificationBuilder.java index 632ac07ea..9d249620d 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceNotificationBuilder.java +++ b/core/src/main/java/de/danoeh/antennapod/core/service/playback/PlaybackServiceNotificationBuilder.java @@ -69,7 +69,7 @@ public class PlaybackServiceNotificationBuilder { } public void loadIcon() { - int iconSize = context.getResources().getDimensionPixelSize(android.R.dimen.notification_large_icon_width); + int iconSize = (int) (128 * context.getResources().getDisplayMetrics().density); try { icon = Glide.with(context) .asBitmap() diff --git a/core/src/main/java/de/danoeh/antennapod/core/ssl/BackportCaCerts.java b/core/src/main/java/de/danoeh/antennapod/core/ssl/BackportCaCerts.java index 720d6a9d9..78c105e38 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/ssl/BackportCaCerts.java +++ b/core/src/main/java/de/danoeh/antennapod/core/ssl/BackportCaCerts.java @@ -70,4 +70,36 @@ public class BackportCaCerts { + "0MC2Hb46TpSi125sC8KKfPog88Tk5c0NqMuRkrF8hey1FGlmDoLnzc7ILaZRfyHB\n" + "NVOFBkpdn627G190\n" + "-----END CERTIFICATE-----"; + + public static final String LETSENCRYPT_ISRG = "-----BEGIN CERTIFICATE-----\n" + + "MIIFazCCA1OgAwIBAgIRAIIQz7DSQONZRGPgu2OCiwAwDQYJKoZIhvcNAQELBQAw\n" + + "TzELMAkGA1UEBhMCVVMxKTAnBgNVBAoTIEludGVybmV0IFNlY3VyaXR5IFJlc2Vh\n" + + "cmNoIEdyb3VwMRUwEwYDVQQDEwxJU1JHIFJvb3QgWDEwHhcNMTUwNjA0MTEwNDM4\n" + + "WhcNMzUwNjA0MTEwNDM4WjBPMQswCQYDVQQGEwJVUzEpMCcGA1UEChMgSW50ZXJu\n" + + "ZXQgU2VjdXJpdHkgUmVzZWFyY2ggR3JvdXAxFTATBgNVBAMTDElTUkcgUm9vdCBY\n" + + "MTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAK3oJHP0FDfzm54rVygc\n" + + "h77ct984kIxuPOZXoHj3dcKi/vVqbvYATyjb3miGbESTtrFj/RQSa78f0uoxmyF+\n" + + "0TM8ukj13Xnfs7j/EvEhmkvBioZxaUpmZmyPfjxwv60pIgbz5MDmgK7iS4+3mX6U\n" + + "A5/TR5d8mUgjU+g4rk8Kb4Mu0UlXjIB0ttov0DiNewNwIRt18jA8+o+u3dpjq+sW\n" + + "T8KOEUt+zwvo/7V3LvSye0rgTBIlDHCNAymg4VMk7BPZ7hm/ELNKjD+Jo2FR3qyH\n" + + "B5T0Y3HsLuJvW5iB4YlcNHlsdu87kGJ55tukmi8mxdAQ4Q7e2RCOFvu396j3x+UC\n" + + "B5iPNgiV5+I3lg02dZ77DnKxHZu8A/lJBdiB3QW0KtZB6awBdpUKD9jf1b0SHzUv\n" + + "KBds0pjBqAlkd25HN7rOrFleaJ1/ctaJxQZBKT5ZPt0m9STJEadao0xAH0ahmbWn\n" + + "OlFuhjuefXKnEgV4We0+UXgVCwOPjdAvBbI+e0ocS3MFEvzG6uBQE3xDk3SzynTn\n" + + "jh8BCNAw1FtxNrQHusEwMFxIt4I7mKZ9YIqioymCzLq9gwQbooMDQaHWBfEbwrbw\n" + + "qHyGO0aoSCqI3Haadr8faqU9GY/rOPNk3sgrDQoo//fb4hVC1CLQJ13hef4Y53CI\n" + + "rU7m2Ys6xt0nUW7/vGT1M0NPAgMBAAGjQjBAMA4GA1UdDwEB/wQEAwIBBjAPBgNV\n" + + "HRMBAf8EBTADAQH/MB0GA1UdDgQWBBR5tFnme7bl5AFzgAiIyBpY9umbbjANBgkq\n" + + "hkiG9w0BAQsFAAOCAgEAVR9YqbyyqFDQDLHYGmkgJykIrGF1XIpu+ILlaS/V9lZL\n" + + "ubhzEFnTIZd+50xx+7LSYK05qAvqFyFWhfFQDlnrzuBZ6brJFe+GnY+EgPbk6ZGQ\n" + + "3BebYhtF8GaV0nxvwuo77x/Py9auJ/GpsMiu/X1+mvoiBOv/2X/qkSsisRcOj/KK\n" + + "NFtY2PwByVS5uCbMiogziUwthDyC3+6WVwW6LLv3xLfHTjuCvjHIInNzktHCgKQ5\n" + + "ORAzI4JMPJ+GslWYHb4phowim57iaztXOoJwTdwJx4nLCgdNbOhdjsnvzqvHu7Ur\n" + + "TkXWStAmzOVyyghqpZXjFaH3pO3JLF+l+/+sKAIuvtd7u+Nxe5AW0wdeRlN8NwdC\n" + + "jNPElpzVmbUq4JUagEiuTDkHzsxHpFKVK7q4+63SM1N95R1NbdWhscdCb+ZAJzVc\n" + + "oyi3B43njTOQ5yOf+1CceWxG1bQVs5ZufpsMljq4Ui0/1lvh+wjChP4kqKOJ2qxq\n" + + "4RgqsahDYVvTH9w7jXbyLeiNdd8XM2w9U/t7y0Ff/9yi0GE44Za4rF2LN9d11TPA\n" + + "mRGunUHBcnWEvgJBQl9nJEiU0Zsnvgc/ubhPgXRR4Xq37Z0j4r7g1SgEEzwxA57d\n" + + "emyPxgcYxn/eR44/KJ4EBs+lVDR3veyJm+kXQ99b21/+jh5Xos1AnX5iItreGCc=\n" + + "-----END CERTIFICATE-----"; } diff --git a/core/src/main/java/de/danoeh/antennapod/core/ssl/BackportTrustManager.java b/core/src/main/java/de/danoeh/antennapod/core/ssl/BackportTrustManager.java index b8fe950b2..81d2a0709 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/ssl/BackportTrustManager.java +++ b/core/src/main/java/de/danoeh/antennapod/core/ssl/BackportTrustManager.java @@ -45,6 +45,8 @@ public class BackportTrustManager { new ByteArrayInputStream(BackportCaCerts.COMODO.getBytes(Charset.forName("UTF-8"))))); keystore.setCertificateEntry("SECTIGO_USER_TRUST_CA", cf.generateCertificate( new ByteArrayInputStream(BackportCaCerts.SECTIGO_USER_TRUST.getBytes(Charset.forName("UTF-8"))))); + keystore.setCertificateEntry("LETSENCRYPT_ISRG_CA", cf.generateCertificate( + new ByteArrayInputStream(BackportCaCerts.LETSENCRYPT_ISRG.getBytes(Charset.forName("UTF-8"))))); List<X509TrustManager> managers = new ArrayList<>(); managers.add(getSystemTrustManager(keystore)); diff --git a/core/src/main/java/de/danoeh/antennapod/core/storage/APDownloadAlgorithm.java b/core/src/main/java/de/danoeh/antennapod/core/storage/APDownloadAlgorithm.java index 7ec4db5dd..061d6cf3f 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/storage/APDownloadAlgorithm.java +++ b/core/src/main/java/de/danoeh/antennapod/core/storage/APDownloadAlgorithm.java @@ -65,7 +65,7 @@ public class APDownloadAlgorithm implements AutomaticDownloadAlgorithm { Iterator<FeedItem> it = candidates.iterator(); while (it.hasNext()) { FeedItem item = it.next(); - if (!item.isAutoDownloadable()) { + if (!item.isAutoDownloadable() || item.getFeed().isLocalFeed()) { it.remove(); } } 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 2ba817b94..58f838a75 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 @@ -7,10 +7,10 @@ import androidx.collection.ArrayMap; import android.text.TextUtils; import android.util.Log; +import java.io.File; import java.util.ArrayList; import java.util.Collections; import java.util.Comparator; -import java.util.Date; import java.util.List; import java.util.Map; @@ -799,7 +799,7 @@ public final class DBReader { feedTotalTime += media.getDuration() / 1000; if (media.isDownloaded()) { - totalDownloadSize = totalDownloadSize + media.getSize(); + totalDownloadSize += new File(media.getFile_url()).length(); episodesDownloadCount++; } 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 c059e696a..ec39e7144 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 @@ -6,7 +6,6 @@ import android.database.Cursor; import android.os.Looper; import android.text.TextUtils; import android.util.Log; -import androidx.annotation.VisibleForTesting; import de.danoeh.antennapod.core.ClientConfig; import de.danoeh.antennapod.core.R; import de.danoeh.antennapod.core.event.FeedItemEvent; @@ -290,7 +289,7 @@ public final class DBTasks { */ public static Future<?> autodownloadUndownloadedItems(final Context context) { Log.d(TAG, "autodownloadUndownloadedItems"); - return autodownloadExec.submit(ClientConfig.dbTasksCallbacks.getAutomaticDownloadAlgorithm() + return autodownloadExec.submit(ClientConfig.automaticDownloadAlgorithm .autoDownloadUndownloadedItems(context)); } @@ -304,7 +303,7 @@ public final class DBTasks { * @param context Used for accessing the DB. */ public static void performAutoCleanup(final Context context) { - ClientConfig.dbTasksCallbacks.getEpisodeCacheCleanupAlgorithm().performCleanup(context); + UserPreferences.getEpisodeCleanupAlgorithm().performCleanup(context); } /** @@ -445,7 +444,8 @@ public final class DBTasks { // as the most recent item // (if the most recent date is null then we can assume there are no items // and this is the first, hence 'new') - if (priorMostRecentDate == null + // New items that do not have a pubDate set are always marked as new + if (item.getPubDate() == null || priorMostRecentDate == null || priorMostRecentDate.before(item.getPubDate()) || priorMostRecentDate.equals(item.getPubDate())) { Log.d(TAG, "Marking item published on " + item.getPubDate() diff --git a/core/src/main/java/de/danoeh/antennapod/core/storage/DBWriter.java b/core/src/main/java/de/danoeh/antennapod/core/storage/DBWriter.java index 9e6041df3..84cc4b6a8 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/storage/DBWriter.java +++ b/core/src/main/java/de/danoeh/antennapod/core/storage/DBWriter.java @@ -21,8 +21,8 @@ import java.util.Set; import java.util.concurrent.ExecutorService; import java.util.concurrent.Executors; import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; -import de.danoeh.antennapod.core.ClientConfig; import de.danoeh.antennapod.core.R; import de.danoeh.antennapod.core.event.DownloadLogEvent; import de.danoeh.antennapod.core.event.FavoritesEvent; @@ -74,6 +74,18 @@ public class DBWriter { } /** + * Wait until all threads are finished to avoid the "Illegal connection pointer" error of + * Robolectric. Call this method only for unit tests. + */ + public static void tearDownTests() { + try { + dbExec.awaitTermination(1, TimeUnit.SECONDS); + } catch (InterruptedException e) { + // ignore error + } + } + + /** * Deletes a downloaded FeedMedia file from the storage device. * * @param context A context that is used for opening a database connection. diff --git a/core/src/main/java/de/danoeh/antennapod/core/storage/FeedSearcher.java b/core/src/main/java/de/danoeh/antennapod/core/storage/FeedSearcher.java index 2f48cfc07..ea62065fc 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/storage/FeedSearcher.java +++ b/core/src/main/java/de/danoeh/antennapod/core/storage/FeedSearcher.java @@ -3,10 +3,8 @@ package de.danoeh.antennapod.core.storage; import android.content.Context; import androidx.annotation.NonNull; import de.danoeh.antennapod.core.feed.Feed; -import de.danoeh.antennapod.core.feed.FeedComponent; import de.danoeh.antennapod.core.feed.FeedItem; -import java.util.ArrayList; import java.util.Collections; import java.util.List; import java.util.concurrent.ExecutionException; 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 a02cce504..7eae8b9f0 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 @@ -1,6 +1,5 @@ package de.danoeh.antennapod.core.storage; -import android.annotation.SuppressLint; import android.content.ContentValues; import android.content.Context; import android.database.Cursor; @@ -317,46 +316,44 @@ public class PodDBAdapter { + JOIN_FEED_ITEM_AND_MEDIA; private static Context context; + private static PodDBAdapter instance; - private static volatile SQLiteDatabase db; + private final SQLiteDatabase db; + private final PodDBHelper dbHelper; public static void init(Context context) { PodDBAdapter.context = context.getApplicationContext(); } - // Bill Pugh Singleton Implementation - private static class SingletonHolder { - private static final PodDBHelper dbHelper = new PodDBHelper(PodDBAdapter.context, DATABASE_NAME, null); - private static final PodDBAdapter dbAdapter = new PodDBAdapter(); - } - public static PodDBAdapter getInstance() { - return SingletonHolder.dbAdapter; + if (instance == null) { + instance = new PodDBAdapter(); + } + return instance; } private PodDBAdapter() { + dbHelper = new PodDBHelper(PodDBAdapter.context, DATABASE_NAME, null); + db = openDb(); } - public synchronized PodDBAdapter open() { - if (db == null || !db.isOpen() || db.isReadOnly()) { - db = openDb(); - } - return this; - } - - @SuppressLint("NewApi") private SQLiteDatabase openDb() { SQLiteDatabase newDb; try { - newDb = SingletonHolder.dbHelper.getWritableDatabase(); + newDb = dbHelper.getWritableDatabase(); newDb.disableWriteAheadLogging(); } catch (SQLException ex) { Log.e(TAG, Log.getStackTraceString(ex)); - newDb = SingletonHolder.dbHelper.getReadableDatabase(); + newDb = dbHelper.getReadableDatabase(); } return newDb; } + public synchronized PodDBAdapter open() { + // do nothing + return this; + } + public synchronized void close() { // do nothing } @@ -372,8 +369,8 @@ public class PodDBAdapter { * <a href="https://github.com/robolectric/robolectric/issues/1890">robolectric/robolectric#1890</a>.</p> */ public static void tearDownTests() { - db = null; - SingletonHolder.dbHelper.close(); + getInstance().dbHelper.close(); + instance = null; } public static boolean deleteDatabase() { @@ -381,7 +378,7 @@ public class PodDBAdapter { adapter.open(); try { for (String tableName : ALL_TABLES) { - db.delete(tableName, "1", null); + adapter.db.delete(tableName, "1", null); } return true; } finally { diff --git a/core/src/main/java/de/danoeh/antennapod/core/sync/SyncService.java b/core/src/main/java/de/danoeh/antennapod/core/sync/SyncService.java index 1f5d9b75f..7563ab715 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/sync/SyncService.java +++ b/core/src/main/java/de/danoeh/antennapod/core/sync/SyncService.java @@ -80,7 +80,7 @@ public class SyncService extends Worker { if (!GpodnetPreferences.loggedIn()) { return Result.success(); } - syncServiceImpl = new GpodnetService(AntennapodHttpClient.getHttpClient(), GpodnetService.DEFAULT_BASE_HOST); + syncServiceImpl = new GpodnetService(AntennapodHttpClient.getHttpClient(), GpodnetPreferences.getHostname()); SharedPreferences.Editor prefs = getApplicationContext().getSharedPreferences(PREF_NAME, Context.MODE_PRIVATE) .edit(); prefs.putLong(PREF_LAST_SYNC_ATTEMPT_TIMESTAMP, System.currentTimeMillis()).apply(); @@ -485,7 +485,11 @@ public class SyncService extends Worker { } private void updateErrorNotification(SyncServiceException exception) { - Log.d(TAG, "Posting error notification"); + if (!UserPreferences.gpodnetNotificationsEnabled()) { + Log.d(TAG, "Skipping sync error notification because of user setting"); + return; + } + Log.d(TAG, "Posting sync error notification"); final String description = getApplicationContext().getString(R.string.gpodnetsync_error_descr) + exception.getMessage(); diff --git a/core/src/main/java/de/danoeh/antennapod/core/sync/gpoddernet/GpodnetService.java b/core/src/main/java/de/danoeh/antennapod/core/sync/gpoddernet/GpodnetService.java index 62c8ce5f3..75ac42b31 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/sync/gpoddernet/GpodnetService.java +++ b/core/src/main/java/de/danoeh/antennapod/core/sync/gpoddernet/GpodnetService.java @@ -2,6 +2,7 @@ package de.danoeh.antennapod.core.sync.gpoddernet; import android.util.Log; import androidx.annotation.NonNull; +import de.danoeh.antennapod.core.BuildConfig; import de.danoeh.antennapod.core.preferences.GpodnetPreferences; import de.danoeh.antennapod.core.sync.gpoddernet.model.GpodnetDevice; import de.danoeh.antennapod.core.sync.model.EpisodeAction; @@ -36,6 +37,7 @@ import java.net.URL; import java.nio.charset.Charset; import java.util.ArrayList; import java.util.Collection; +import java.util.Collections; import java.util.LinkedList; import java.util.List; import java.util.Locale; @@ -47,6 +49,7 @@ public class GpodnetService implements ISyncService { public static final String TAG = "GpodnetService"; public static final String DEFAULT_BASE_HOST = "gpodder.net"; private static final String BASE_SCHEME = "https"; + private static final int PORT = 443; private static final int UPLOAD_BULK_SIZE = 30; private static final MediaType TEXT = MediaType.parse("plain/text; charset=utf-8"); private static final MediaType JSON = MediaType.parse("application/json; charset=utf-8"); @@ -71,7 +74,8 @@ public class GpodnetService implements ISyncService { public List<GpodnetTag> getTopTags(int count) throws GpodnetServiceException { URL url; try { - url = new URI(BASE_SCHEME, baseHost, String.format(Locale.US, "/api/2/tags/%d.json", count), null).toURL(); + url = new URI(BASE_SCHEME, null, baseHost, PORT, + String.format(Locale.US, "/api/2/tags/%d.json", count), null, null).toURL(); } catch (MalformedURLException | URISyntaxException e) { e.printStackTrace(); throw new GpodnetServiceException(e); @@ -104,8 +108,8 @@ public class GpodnetService implements ISyncService { public List<GpodnetPodcast> getPodcastsForTag(@NonNull GpodnetTag tag, int count) throws GpodnetServiceException { try { - URL url = new URI(BASE_SCHEME, baseHost, String.format(Locale.US, - "/api/2/tag/%s/%d.json", tag.getTag(), count), null).toURL(); + URL url = new URI(BASE_SCHEME, null, baseHost, PORT, + String.format(Locale.US, "/api/2/tag/%s/%d.json", tag.getTag(), count), null, null).toURL(); Request.Builder request = new Request.Builder().url(url); String response = executeRequest(request); @@ -130,7 +134,8 @@ public class GpodnetService implements ISyncService { } try { - URL url = new URI(BASE_SCHEME, baseHost, String.format(Locale.US, "/toplist/%d.json", count), null).toURL(); + URL url = new URI(BASE_SCHEME, null, baseHost, PORT, + String.format(Locale.US, "/toplist/%d.json", count), null, null).toURL(); Request.Builder request = new Request.Builder().url(url); String response = executeRequest(request); @@ -161,8 +166,8 @@ public class GpodnetService implements ISyncService { } try { - URL url = new URI(BASE_SCHEME, baseHost, - String.format(Locale.US, "/suggestions/%d.json", count), null).toURL(); + URL url = new URI(BASE_SCHEME, null, baseHost, PORT, + String.format(Locale.US, "/suggestions/%d.json", count), null, null).toURL(); Request.Builder request = new Request.Builder().url(url); String response = executeRequest(request); @@ -187,7 +192,7 @@ public class GpodnetService implements ISyncService { .format(Locale.US, "q=%s&scale_logo=%d", query, scaledLogoSize) : String .format("q=%s", query); try { - URL url = new URI(BASE_SCHEME, null, baseHost, -1, "/search.json", + URL url = new URI(BASE_SCHEME, null, baseHost, PORT, "/search.json", parameters, null).toURL(); Request.Builder request = new Request.Builder().url(url); String response = executeRequest(request); @@ -214,7 +219,8 @@ public class GpodnetService implements ISyncService { public List<GpodnetDevice> getDevices() throws GpodnetServiceException { requireLoggedIn(); try { - URL url = new URI(BASE_SCHEME, baseHost, String.format("/api/2/devices/%s.json", username), null).toURL(); + URL url = new URI(BASE_SCHEME, null, baseHost, PORT, + String.format("/api/2/devices/%s.json", username), null, null).toURL(); Request.Builder request = new Request.Builder().url(url); String response = executeRequest(request); JSONArray devicesArray = new JSONArray(response); @@ -226,6 +232,45 @@ public class GpodnetService implements ISyncService { } /** + * Returns synchronization status of devices. + * <p/> + * This method requires authentication. + * + * @throws GpodnetServiceAuthenticationException If there is an authentication error. + */ + public List<List<String>> getSynchronizedDevices() throws GpodnetServiceException { + requireLoggedIn(); + try { + URL url = new URI(BASE_SCHEME, null, baseHost, PORT, + String.format("/api/2/sync-devices/%s.json", username), null, null).toURL(); + Request.Builder request = new Request.Builder().url(url); + String response = executeRequest(request); + JSONObject syncStatus = new JSONObject(response); + List<List<String>> result = new ArrayList<>(); + + JSONArray synchronizedDevices = syncStatus.getJSONArray("synchronized"); + for (int i = 0; i < synchronizedDevices.length(); i++) { + JSONArray groupDevices = synchronizedDevices.getJSONArray(i); + List<String> group = new ArrayList<>(); + for (int j = 0; j < groupDevices.length(); j++) { + group.add(groupDevices.getString(j)); + } + result.add(group); + } + + JSONArray notSynchronizedDevices = syncStatus.getJSONArray("not-synchronized"); + for (int i = 0; i < notSynchronizedDevices.length(); i++) { + result.add(Collections.singletonList(notSynchronizedDevices.getString(i))); + } + + return result; + } catch (JSONException | MalformedURLException | URISyntaxException e) { + e.printStackTrace(); + throw new GpodnetServiceException(e); + } + } + + /** * Configures the device of a given user. * <p/> * This method requires authentication. @@ -237,8 +282,8 @@ public class GpodnetService implements ISyncService { throws GpodnetServiceException { requireLoggedIn(); try { - URL url = new URI(BASE_SCHEME, baseHost, String.format( - "/api/2/devices/%s/%s.json", username, deviceId), null).toURL(); + URL url = new URI(BASE_SCHEME, null, baseHost, PORT, + String.format("/api/2/devices/%s/%s.json", username, deviceId), null, null).toURL(); String content; if (caption != null || type != null) { JSONObject jsonContent = new JSONObject(); @@ -262,6 +307,39 @@ public class GpodnetService implements ISyncService { } /** + * Links devices for synchronization. + * <p/> + * This method requires authentication. + * + * @throws GpodnetServiceAuthenticationException If there is an authentication error. + */ + public void linkDevices(@NonNull List<String> deviceIds) throws GpodnetServiceException { + requireLoggedIn(); + try { + final URL url = new URI(BASE_SCHEME, null, baseHost, PORT, + String.format("/api/2/sync-devices/%s.json", username), null, null).toURL(); + JSONObject jsonContent = new JSONObject(); + JSONArray group = new JSONArray(); + for (String deviceId : deviceIds) { + group.put(deviceId); + } + + JSONArray synchronizedGroups = new JSONArray(); + synchronizedGroups.put(group); + jsonContent.put("synchronize", synchronizedGroups); + jsonContent.put("stop-synchronize", new JSONArray()); + + Log.d("aaaa", jsonContent.toString()); + RequestBody body = RequestBody.create(JSON, jsonContent.toString()); + Request.Builder request = new Request.Builder().post(body).url(url); + executeRequest(request); + } catch (JSONException | MalformedURLException | URISyntaxException e) { + e.printStackTrace(); + throw new GpodnetServiceException(e); + } + } + + /** * Returns the subscriptions of a specific device. * <p/> * This method requires authentication. @@ -273,8 +351,8 @@ public class GpodnetService implements ISyncService { public String getSubscriptionsOfDevice(@NonNull String deviceId) throws GpodnetServiceException { requireLoggedIn(); try { - URL url = new URI(BASE_SCHEME, baseHost, String.format( - "/subscriptions/%s/%s.opml", username, deviceId), null).toURL(); + URL url = new URI(BASE_SCHEME, null, baseHost, PORT, + String.format("/subscriptions/%s/%s.opml", username, deviceId), null, null).toURL(); Request.Builder request = new Request.Builder().url(url); return executeRequest(request); } catch (MalformedURLException | URISyntaxException e) { @@ -295,7 +373,8 @@ public class GpodnetService implements ISyncService { public String getSubscriptionsOfUser() throws GpodnetServiceException { requireLoggedIn(); try { - URL url = new URI(BASE_SCHEME, baseHost, String.format("/subscriptions/%s.opml", username), null).toURL(); + URL url = new URI(BASE_SCHEME, null, baseHost, PORT, + String.format("/subscriptions/%s.opml", username), null, null).toURL(); Request.Builder request = new Request.Builder().url(url); return executeRequest(request); } catch (MalformedURLException | URISyntaxException e) { @@ -319,8 +398,8 @@ public class GpodnetService implements ISyncService { throws GpodnetServiceException { requireLoggedIn(); try { - URL url = new URI(BASE_SCHEME, baseHost, String.format( - "/subscriptions/%s/%s.txt", username, deviceId), null).toURL(); + URL url = new URI(BASE_SCHEME, null, baseHost, PORT, + String.format("/subscriptions/%s/%s.txt", username, deviceId), null, null).toURL(); StringBuilder builder = new StringBuilder(); for (String s : subscriptions) { builder.append(s); @@ -353,8 +432,8 @@ public class GpodnetService implements ISyncService { @NonNull Collection<String> removed) throws GpodnetServiceException { requireLoggedIn(); try { - URL url = new URI(BASE_SCHEME, baseHost, String.format( - "/api/2/subscriptions/%s/%s.json", username, deviceId), null).toURL(); + URL url = new URI(BASE_SCHEME, null, baseHost, PORT, + String.format("/api/2/subscriptions/%s/%s.json", username, deviceId), null, null).toURL(); final JSONObject requestObject = new JSONObject(); requestObject.put("add", new JSONArray(added)); @@ -389,8 +468,7 @@ public class GpodnetService implements ISyncService { String params = String.format(Locale.US, "since=%d", timestamp); String path = String.format("/api/2/subscriptions/%s/%s.json", username, deviceId); try { - URL url = new URI(BASE_SCHEME, null, baseHost, -1, path, params, - null).toURL(); + URL url = new URI(BASE_SCHEME, null, baseHost, PORT, path, params, null).toURL(); Request.Builder request = new Request.Builder().url(url); String response = executeRequest(request); @@ -432,8 +510,8 @@ public class GpodnetService implements ISyncService { throws SyncServiceException { try { Log.d(TAG, "Uploading partial actions " + from + " to " + to + " of " + episodeActions.size()); - URL url = new URI(BASE_SCHEME, baseHost, String.format( - "/api/2/episodes/%s.json", username), null).toURL(); + URL url = new URI(BASE_SCHEME, null, baseHost, PORT, + String.format("/api/2/episodes/%s.json", username), null, null).toURL(); final JSONArray list = new JSONArray(); for (int i = from; i < to; i++) { @@ -471,7 +549,7 @@ public class GpodnetService implements ISyncService { String params = String.format(Locale.US, "since=%d", timestamp); String path = String.format("/api/2/episodes/%s.json", username); try { - URL url = new URI(BASE_SCHEME, null, baseHost, -1, path, params, null).toURL(); + URL url = new URI(BASE_SCHEME, null, baseHost, PORT, path, params, null).toURL(); Request.Builder request = new Request.Builder().url(url); String response = executeRequest(request); @@ -497,7 +575,8 @@ public class GpodnetService implements ISyncService { public void authenticate(@NonNull String username, @NonNull String password) throws GpodnetServiceException { URL url; try { - url = new URI(BASE_SCHEME, baseHost, String.format("/api/2/auth/%s/login.json", username), null).toURL(); + url = new URI(BASE_SCHEME, null, baseHost, PORT, + String.format("/api/2/auth/%s/login.json", username), null, null).toURL(); } catch (MalformedURLException | URISyntaxException e) { e.printStackTrace(); throw new GpodnetServiceException(e); @@ -567,6 +646,13 @@ public class GpodnetService implements ISyncService { if (responseCode == HttpURLConnection.HTTP_UNAUTHORIZED) { throw new GpodnetServiceAuthenticationException("Wrong username or password"); } else { + if (BuildConfig.DEBUG) { + try { + Log.d(TAG, response.body().string()); + } catch (IOException e) { + e.printStackTrace(); + } + } throw new GpodnetServiceBadStatusCodeException("Bad response code: " + responseCode, responseCode); } } diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/DateUtils.java b/core/src/main/java/de/danoeh/antennapod/core/util/DateUtils.java index e15ab2fdc..196583bcd 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/util/DateUtils.java +++ b/core/src/main/java/de/danoeh/antennapod/core/util/DateUtils.java @@ -22,14 +22,7 @@ public class DateUtils { private DateUtils(){} private static final String TAG = "DateUtils"; - private static final TimeZone defaultTimezone = TimeZone.getTimeZone("GMT"); - private static final SimpleDateFormat dateFormatParser = new SimpleDateFormat("", Locale.US); - - static { - dateFormatParser.setLenient(false); - dateFormatParser.setTimeZone(defaultTimezone); - } public static Date parse(final String input) { if (input == null) { @@ -37,9 +30,12 @@ public class DateUtils { } String date = input.trim().replace('/', '-').replaceAll("( ){2,}+", " "); + // remove colon from timezone to avoid differences between Android and Java SimpleDateFormat + date = date.replaceAll("([+-]\\d\\d):(\\d\\d)$", "$1$2"); + // CEST is widely used but not in the "ISO 8601 Time zone" list. Let's hack around. - date = date.replaceAll("CEST$", "+02:00"); - date = date.replaceAll("CET$", "+01:00"); + date = date.replaceAll("CEST$", "+0200"); + date = date.replaceAll("CET$", "+0100"); // some generators use "Sept" for September date = date.replaceAll("\\bSept\\b", "Sep"); @@ -99,12 +95,16 @@ public class DateUtils { "EEE d MMM yyyy HH:mm:ss 'GMT'Z (z)" }; + SimpleDateFormat parser = new SimpleDateFormat("", Locale.US); + parser.setLenient(false); + parser.setTimeZone(defaultTimezone); + ParsePosition pos = new ParsePosition(0); for (String pattern : patterns) { - dateFormatParser.applyPattern(pattern); + parser.applyPattern(pattern); pos.setIndex(0); try { - Date result = dateFormatParser.parse(date, pos); + Date result = parser.parse(date, pos); if (result != null && pos.getIndex() == date.length()) { return result; } diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/DownloadError.java b/core/src/main/java/de/danoeh/antennapod/core/util/DownloadError.java index 0c9989b43..babf3a846 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/util/DownloadError.java +++ b/core/src/main/java/de/danoeh/antennapod/core/util/DownloadError.java @@ -19,11 +19,11 @@ public enum DownloadError { ERROR_NOT_ENOUGH_SPACE(10, R.string.download_error_insufficient_space), ERROR_UNKNOWN_HOST(11, R.string.download_error_unknown_host), ERROR_REQUEST_ERROR(12, R.string.download_error_request_error), - ERROR_DB_ACCESS_ERROR(13, R.string.download_error_db_access), - ERROR_UNAUTHORIZED(14, R.string.download_error_unauthorized), + ERROR_DB_ACCESS_ERROR(13, R.string.download_error_db_access), + ERROR_UNAUTHORIZED(14, R.string.download_error_unauthorized), ERROR_FILE_TYPE(15, R.string.download_error_file_type_type), - ERROR_FORBIDDEN(16, R.string.download_error_forbidden); - + ERROR_FORBIDDEN(16, R.string.download_error_forbidden), + ERROR_IO_WRONG_SIZE(17, R.string.download_error_forbidden); private final int code; private final int resId; diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/FileNameGenerator.java b/core/src/main/java/de/danoeh/antennapod/core/util/FileNameGenerator.java index 2a387b7b0..69c23efc2 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/util/FileNameGenerator.java +++ b/core/src/main/java/de/danoeh/antennapod/core/util/FileNameGenerator.java @@ -13,7 +13,7 @@ import java.security.NoSuchAlgorithmException; /** Generates valid filenames for a given string. */ public class FileNameGenerator { @VisibleForTesting - public static final int MAX_FILENAME_LENGTH = 255; // Limited by ext4 + public static final int MAX_FILENAME_LENGTH = 242; // limited by CircleCI private static final int MD5_HEX_LENGTH = 32; private static final char[] validChars = diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/LangUtils.java b/core/src/main/java/de/danoeh/antennapod/core/util/LangUtils.java deleted file mode 100644 index 20af6415e..000000000 --- a/core/src/main/java/de/danoeh/antennapod/core/util/LangUtils.java +++ /dev/null @@ -1,124 +0,0 @@ -package de.danoeh.antennapod.core.util; - -import androidx.collection.ArrayMap; - -import java.nio.charset.Charset; - -public class LangUtils { - - private LangUtils(){} - - public static final Charset UTF_8 = Charset.forName("UTF-8"); - - private static final ArrayMap<String, String> languages; - static { - languages = new ArrayMap<>(); - languages.put("af", "Afrikaans"); - languages.put("sq", "Albanian"); - languages.put("sq", "Albanian"); - languages.put("eu", "Basque"); - languages.put("be", "Belarusian"); - languages.put("bg", "Bulgarian"); - languages.put("ca", "Catalan"); - languages.put("Chinese (Simplified)", "zh-cn"); - languages.put("Chinese (Traditional)", "zh-tw"); - languages.put("hr", "Croatian"); - languages.put("cs", "Czech"); - languages.put("da", "Danish"); - languages.put("nl", "Dutch"); - languages.put("nl-be", "Dutch (Belgium)"); - languages.put("nl-nl", "Dutch (Netherlands)"); - languages.put("en", "English"); - languages.put("en-au", "English (Australia)"); - languages.put("en-bz", "English (Belize)"); - languages.put("en-ca", "English (Canada)"); - languages.put("en-ie", "English (Ireland)"); - languages.put("en-jm", "English (Jamaica)"); - languages.put("en-nz", "English (New Zealand)"); - languages.put("en-ph", "English (Phillipines)"); - languages.put("en-za", "English (South Africa)"); - languages.put("en-tt", "English (Trinidad)"); - languages.put("en-gb", "English (United Kingdom)"); - languages.put("en-us", "English (United States)"); - languages.put("en-zw", "English (Zimbabwe)"); - languages.put("et", "Estonian"); - languages.put("fo", "Faeroese"); - languages.put("fi", "Finnish"); - languages.put("fr", "French"); - languages.put("fr-be", "French (Belgium)"); - languages.put("fr-ca", "French (Canada)"); - languages.put("fr-fr", "French (France)"); - languages.put("fr-lu", "French (Luxembourg)"); - languages.put("fr-mc", "French (Monaco)"); - languages.put("fr-ch", "French (Switzerland)"); - languages.put("gl", "Galician"); - languages.put("gd", "Gaelic"); - languages.put("de", "German"); - languages.put("de-at", "German (Austria)"); - languages.put("de-de", "German (Germany)"); - languages.put("de-li", "German (Liechtenstein)"); - languages.put("de-lu", "German (Luxembourg)"); - languages.put("de-ch", "German (Switzerland)"); - languages.put("el", "Greek"); - languages.put("haw", "Hawaiian"); - languages.put("hu", "Hungarian"); - languages.put("is", "Icelandic"); - languages.put("in", "Indonesian"); - languages.put("ga", "Irish"); - languages.put("it", "Italian"); - languages.put("it-it", "Italian (Italy)"); - languages.put("it-ch", "Italian (Switzerland)"); - languages.put("ja", "Japanese"); - languages.put("ko", "Korean"); - languages.put("mk", "Macedonian"); - languages.put("no", "Norwegian"); - languages.put("pl", "Polish"); - languages.put("pt", "Portugese"); - languages.put("pt-br", "Portugese (Brazil)"); - languages.put("pt-pt", "Portugese (Portugal"); - languages.put("ro", "Romanian"); - languages.put("ro-mo", "Romanian (Moldova)"); - languages.put("ro-ro", "Romanian (Romania"); - languages.put("ru", "Russian"); - languages.put("ru-mo", "Russian (Moldova)"); - languages.put("ru-ru", "Russian (Russia)"); - languages.put("sr", "Serbian"); - languages.put("sk", "Slovak"); - languages.put("sl", "Slovenian"); - languages.put("es", "Spanish"); - languages.put("es-ar", "Spanish (Argentinia)"); - languages.put("es=bo", "Spanish (Bolivia)"); - languages.put("es-cl", "Spanish (Chile)"); - languages.put("es-co", "Spanish (Colombia)"); - languages.put("es-cr", "Spanish (Costa Rica)"); - languages.put("es-do", "Spanish (Dominican Republic)"); - languages.put("es-ec", "Spanish (Ecuador)"); - languages.put("es-sv", "Spanish (El Salvador)"); - languages.put("es-gt", "Spanish (Guatemala)"); - languages.put("es-hn", "Spanish (Honduras)"); - languages.put("es-mx", "Spanish (Mexico)"); - languages.put("es-ni", "Spanish (Nicaragua)"); - languages.put("es-pa", "Spanish (Panama)"); - languages.put("es-py", "Spanish (Paraguay)"); - languages.put("es-pe", "Spanish (Peru)"); - languages.put("es-pr", "Spanish (Puerto Rico)"); - languages.put("es-es", "Spanish (Spain)"); - languages.put("es-uy", "Spanish (Uruguay)"); - languages.put("es-ve", "Spanish (Venezuela)"); - languages.put("sv", "Swedish"); - languages.put("sv-fi", "Swedish (Finland)"); - languages.put("sv-se", "Swedish (Sweden)"); - languages.put("tr", "Turkish"); - languages.put("uk", "Ukranian"); - } - - /** Finds language string for key or returns the language key if it can't be found. */ - public static String getLanguageString(String key) { - String language = languages.get(key); - if (language != null) { - return language; - } else { - return key; - } - } -} diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/ShareUtils.java b/core/src/main/java/de/danoeh/antennapod/core/util/ShareUtils.java index 3e9e8327e..2622d81aa 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/util/ShareUtils.java +++ b/core/src/main/java/de/danoeh/antennapod/core/util/ShareUtils.java @@ -19,74 +19,77 @@ import de.danoeh.antennapod.core.feed.FeedMedia; /** Utility methods for sharing data */ public class ShareUtils { - private static final String TAG = "ShareUtils"; - - private ShareUtils() {} - - public static void shareLink(Context context, String text) { - Intent i = new Intent(Intent.ACTION_SEND); - i.setType("text/plain"); - i.putExtra(Intent.EXTRA_TEXT, text); - context.startActivity(Intent.createChooser(i, context.getString(R.string.share_url_label))); - } + private static final String TAG = "ShareUtils"; - public static void shareFeedlink(Context context, Feed feed) { - shareLink(context, feed.getTitle() + ": " + feed.getLink()); - } - - public static void shareFeedDownloadLink(Context context, Feed feed) { - shareLink(context, feed.getTitle() + ": " + feed.getDownload_url()); - } + private ShareUtils() { + } - public static void shareFeedItemLink(Context context, FeedItem item) { - shareFeedItemLink(context, item, false); - } + public static void shareLink(Context context, String text) { + Intent i = new Intent(Intent.ACTION_SEND); + i.setType("text/plain"); + i.putExtra(Intent.EXTRA_TEXT, text); + context.startActivity(Intent.createChooser(i, context.getString(R.string.share_url_label))); + } - public static void shareFeedItemDownloadLink(Context context, FeedItem item) { - shareFeedItemDownloadLink(context, item, false); - } + public static void shareFeedlink(Context context, Feed feed) { + shareLink(context, feed.getTitle() + ": " + feed.getLink()); + } - private static String getItemShareText(FeedItem item) { - return item.getFeed().getTitle() + ": " + item.getTitle(); - } + public static void shareFeedDownloadLink(Context context, Feed feed) { + shareLink(context, feed.getTitle() + ": " + feed.getDownload_url()); + } + + public static void shareFeedItemLink(Context context, FeedItem item) { + shareFeedItemLink(context, item, false); + } + + public static void shareFeedItemDownloadLink(Context context, FeedItem item) { + shareFeedItemDownloadLink(context, item, false); + } + + private static String getItemShareText(FeedItem item) { + return item.getFeed().getTitle() + ": " + item.getTitle(); + } public static boolean hasLinkToShare(FeedItem item) { - return FeedItemUtil.getLinkWithFallback(item) != null; + return FeedItemUtil.getLinkWithFallback(item) != null; } - public static void shareFeedItemLink(Context context, FeedItem item, boolean withPosition) { - String text = getItemShareText(item) + " " + FeedItemUtil.getLinkWithFallback(item); - if(withPosition) { - int pos = item.getMedia().getPosition(); - text += " [" + Converter.getDurationStringLong(pos) + "]"; - } - shareLink(context, text); - } + public static void shareFeedItemLink(Context context, FeedItem item, boolean withPosition) { + String text = getItemShareText(item) + " " + FeedItemUtil.getLinkWithFallback(item); + if (withPosition) { + int pos = item.getMedia().getPosition(); + text += " [" + Converter.getDurationStringLong(pos) + "]"; + } + shareLink(context, text); + } - public static void shareFeedItemDownloadLink(Context context, FeedItem item, boolean withPosition) { - String text = getItemShareText(item) + " " + item.getMedia().getDownload_url(); - if(withPosition) { - int pos = item.getMedia().getPosition(); - text += " [" + Converter.getDurationStringLong(pos) + "]"; - } - shareLink(context, text); - } + public static void shareFeedItemDownloadLink(Context context, FeedItem item, boolean withPosition) { + String text = getItemShareText(item) + " " + item.getMedia().getDownload_url(); + if (withPosition) { + int pos = item.getMedia().getPosition(); + text += "#t=" + pos / 1000; + text += " [" + Converter.getDurationStringLong(pos) + "]"; + } + shareLink(context, text); + } - public static void shareFeedItemFile(Context context, FeedMedia media) { - Intent i = new Intent(Intent.ACTION_SEND); - i.setType(media.getMime_type()); - Uri fileUri = FileProvider.getUriForFile(context, context.getString(R.string.provider_authority), - new File(media.getLocalMediaUrl())); - i.putExtra(Intent.EXTRA_STREAM, fileUri); - i.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); - if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.KITKAT) { - List<ResolveInfo> resInfoList = context.getPackageManager().queryIntentActivities(i, PackageManager.MATCH_DEFAULT_ONLY); - for (ResolveInfo resolveInfo : resInfoList) { - String packageName = resolveInfo.activityInfo.packageName; - context.grantUriPermission(packageName, fileUri, Intent.FLAG_GRANT_READ_URI_PERMISSION); - } - } - context.startActivity(Intent.createChooser(i, context.getString(R.string.share_file_label))); - Log.e(TAG, "shareFeedItemFile called"); - } + public static void shareFeedItemFile(Context context, FeedMedia media) { + Intent i = new Intent(Intent.ACTION_SEND); + i.setType(media.getMime_type()); + Uri fileUri = FileProvider.getUriForFile(context, context.getString(R.string.provider_authority), + new File(media.getLocalMediaUrl())); + i.putExtra(Intent.EXTRA_STREAM, fileUri); + i.addFlags(Intent.FLAG_GRANT_READ_URI_PERMISSION); + if (Build.VERSION.SDK_INT <= Build.VERSION_CODES.KITKAT) { + List<ResolveInfo> resInfoList = context.getPackageManager() + .queryIntentActivities(i, PackageManager.MATCH_DEFAULT_ONLY); + for (ResolveInfo resolveInfo : resInfoList) { + String packageName = resolveInfo.activityInfo.packageName; + context.grantUriPermission(packageName, fileUri, Intent.FLAG_GRANT_READ_URI_PERMISSION); + } + } + context.startActivity(Intent.createChooser(i, context.getString(R.string.share_file_label))); + Log.e(TAG, "shareFeedItemFile called"); + } } diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/URLChecker.java b/core/src/main/java/de/danoeh/antennapod/core/util/URLChecker.java index ac7f4848c..cb7db1709 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/util/URLChecker.java +++ b/core/src/main/java/de/danoeh/antennapod/core/util/URLChecker.java @@ -9,7 +9,6 @@ import de.danoeh.antennapod.core.BuildConfig; import okhttp3.HttpUrl; import java.util.ArrayList; -import java.util.Collections; import java.util.List; /** diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/comparator/FeedItemPubdateComparator.java b/core/src/main/java/de/danoeh/antennapod/core/util/comparator/FeedItemPubdateComparator.java index ad81a1d17..766986bed 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/util/comparator/FeedItemPubdateComparator.java +++ b/core/src/main/java/de/danoeh/antennapod/core/util/comparator/FeedItemPubdateComparator.java @@ -14,8 +14,12 @@ public class FeedItemPubdateComparator implements Comparator<FeedItem> { */ @Override public int compare(FeedItem lhs, FeedItem rhs) { - if (rhs.getPubDate() == null || lhs.getPubDate() == null) { + if (rhs.getPubDate() == null && lhs.getPubDate() == null) { return 0; + } else if (rhs.getPubDate() == null) { + return 1; + } else if (lhs.getPubDate() == null) { + return -1; } return rhs.getPubDate().compareTo(lhs.getPubDate()); } diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/gui/NotificationUtils.java b/core/src/main/java/de/danoeh/antennapod/core/util/gui/NotificationUtils.java index ddbe68938..3101eac34 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/util/gui/NotificationUtils.java +++ b/core/src/main/java/de/danoeh/antennapod/core/util/gui/NotificationUtils.java @@ -2,28 +2,36 @@ package de.danoeh.antennapod.core.util.gui; import android.app.NotificationChannel; +import android.app.NotificationChannelGroup; import android.app.NotificationManager; import android.content.Context; import android.os.Build; import androidx.annotation.RequiresApi; import de.danoeh.antennapod.core.R; +import de.danoeh.antennapod.core.preferences.UserPreferences; public class NotificationUtils { public static final String CHANNEL_ID_USER_ACTION = "user_action"; public static final String CHANNEL_ID_DOWNLOADING = "downloading"; public static final String CHANNEL_ID_PLAYING = "playing"; - public static final String CHANNEL_ID_ERROR = "error"; + public static final String CHANNEL_ID_DOWNLOAD_ERROR = "error"; public static final String CHANNEL_ID_SYNC_ERROR = "sync_error"; public static final String CHANNEL_ID_AUTO_DOWNLOAD = "auto_download"; + public static final String GROUP_ID_ERRORS = "group_errors"; + public static final String GROUP_ID_NEWS = "group_news"; + public static void createChannels(Context context) { - if (android.os.Build.VERSION.SDK_INT < 26) { + if (Build.VERSION.SDK_INT < 26) { return; } NotificationManager mNotificationManager = (NotificationManager) context.getSystemService(Context.NOTIFICATION_SERVICE); if (mNotificationManager != null) { + mNotificationManager.createNotificationChannelGroup(createGroupErrors(context)); + mNotificationManager.createNotificationChannelGroup(createGroupNews(context)); + mNotificationManager.createNotificationChannel(createChannelUserAction(context)); mNotificationManager.createNotificationChannel(createChannelDownloading(context)); mNotificationManager.createNotificationChannel(createChannelPlaying(context)); @@ -35,36 +43,43 @@ public class NotificationUtils { @RequiresApi(api = Build.VERSION_CODES.O) private static NotificationChannel createChannelUserAction(Context c) { - NotificationChannel mChannel = new NotificationChannel(CHANNEL_ID_USER_ACTION, + NotificationChannel notificationChannel = new NotificationChannel(CHANNEL_ID_USER_ACTION, c.getString(R.string.notification_channel_user_action), NotificationManager.IMPORTANCE_HIGH); - mChannel.setDescription(c.getString(R.string.notification_channel_user_action_description)); - return mChannel; + notificationChannel.setDescription(c.getString(R.string.notification_channel_user_action_description)); + notificationChannel.setGroup(GROUP_ID_ERRORS); + return notificationChannel; } @RequiresApi(api = Build.VERSION_CODES.O) private static NotificationChannel createChannelDownloading(Context c) { - NotificationChannel mChannel = new NotificationChannel(CHANNEL_ID_DOWNLOADING, + NotificationChannel notificationChannel = new NotificationChannel(CHANNEL_ID_DOWNLOADING, c.getString(R.string.notification_channel_downloading), NotificationManager.IMPORTANCE_LOW); - mChannel.setDescription(c.getString(R.string.notification_channel_downloading_description)); - mChannel.setShowBadge(false); - return mChannel; + notificationChannel.setDescription(c.getString(R.string.notification_channel_downloading_description)); + notificationChannel.setShowBadge(false); + return notificationChannel; } @RequiresApi(api = Build.VERSION_CODES.O) private static NotificationChannel createChannelPlaying(Context c) { - NotificationChannel mChannel = new NotificationChannel(CHANNEL_ID_PLAYING, + NotificationChannel notificationChannel = new NotificationChannel(CHANNEL_ID_PLAYING, c.getString(R.string.notification_channel_playing), NotificationManager.IMPORTANCE_LOW); - mChannel.setDescription(c.getString(R.string.notification_channel_playing_description)); - mChannel.setShowBadge(false); - return mChannel; + notificationChannel.setDescription(c.getString(R.string.notification_channel_playing_description)); + notificationChannel.setShowBadge(false); + return notificationChannel; } @RequiresApi(api = Build.VERSION_CODES.O) private static NotificationChannel createChannelError(Context c) { - NotificationChannel mChannel = new NotificationChannel(CHANNEL_ID_ERROR, - c.getString(R.string.notification_channel_error), NotificationManager.IMPORTANCE_HIGH); - mChannel.setDescription(c.getString(R.string.notification_channel_error_description)); - return mChannel; + NotificationChannel notificationChannel = new NotificationChannel(CHANNEL_ID_DOWNLOAD_ERROR, + c.getString(R.string.notification_channel_download_error), NotificationManager.IMPORTANCE_HIGH); + notificationChannel.setDescription(c.getString(R.string.notification_channel_download_error_description)); + notificationChannel.setGroup(GROUP_ID_ERRORS); + + if (!UserPreferences.getShowDownloadReportRaw()) { + // Migration from app managed setting: disable notification + notificationChannel.setImportance(NotificationManager.IMPORTANCE_NONE); + } + return notificationChannel; } @RequiresApi(api = Build.VERSION_CODES.O) @@ -72,14 +87,38 @@ public class NotificationUtils { NotificationChannel notificationChannel = new NotificationChannel(CHANNEL_ID_SYNC_ERROR, c.getString(R.string.notification_channel_sync_error), NotificationManager.IMPORTANCE_HIGH); notificationChannel.setDescription(c.getString(R.string.notification_channel_sync_error_description)); + notificationChannel.setGroup(GROUP_ID_ERRORS); + + if (!UserPreferences.getGpodnetNotificationsEnabledRaw()) { + // Migration from app managed setting: disable notification + notificationChannel.setImportance(NotificationManager.IMPORTANCE_NONE); + } return notificationChannel; } @RequiresApi(api = Build.VERSION_CODES.O) private static NotificationChannel createChannelAutoDownload(Context c) { - NotificationChannel mChannel = new NotificationChannel(CHANNEL_ID_AUTO_DOWNLOAD, - c.getString(R.string.notification_channel_auto_download), NotificationManager.IMPORTANCE_DEFAULT); - mChannel.setDescription(c.getString(R.string.notification_channel_episode_auto_download)); - return mChannel; + NotificationChannel notificationChannel = new NotificationChannel(CHANNEL_ID_AUTO_DOWNLOAD, + c.getString(R.string.notification_channel_auto_download), NotificationManager.IMPORTANCE_NONE); + notificationChannel.setDescription(c.getString(R.string.notification_channel_episode_auto_download)); + notificationChannel.setGroup(GROUP_ID_NEWS); + + if (UserPreferences.getShowAutoDownloadReportRaw()) { + // Migration from app managed setting: enable notification + notificationChannel.setImportance(NotificationManager.IMPORTANCE_DEFAULT); + } + return notificationChannel; + } + + @RequiresApi(api = Build.VERSION_CODES.O) + private static NotificationChannelGroup createGroupErrors(Context c) { + return new NotificationChannelGroup(GROUP_ID_ERRORS, + c.getString(R.string.notification_group_errors)); + } + + @RequiresApi(api = Build.VERSION_CODES.O) + private static NotificationChannelGroup createGroupNews(Context c) { + return new NotificationChannelGroup(GROUP_ID_NEWS, + c.getString(R.string.notification_group_news)); } } diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/playback/AudioPlayer.java b/core/src/main/java/de/danoeh/antennapod/core/util/playback/AudioPlayer.java index fecb14d25..c948d98a3 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/util/playback/AudioPlayer.java +++ b/core/src/main/java/de/danoeh/antennapod/core/util/playback/AudioPlayer.java @@ -1,7 +1,6 @@ package de.danoeh.antennapod.core.util.playback; import android.content.Context; -import android.content.SharedPreferences; import androidx.preference.PreferenceManager; import android.util.Log; import android.view.SurfaceHolder; @@ -11,6 +10,7 @@ import org.antennapod.audio.MediaPlayer; import de.danoeh.antennapod.core.preferences.UserPreferences; +import java.io.IOException; import java.util.Collections; import java.util.List; @@ -21,7 +21,7 @@ public class AudioPlayer extends MediaPlayer implements IPlayer { super(context, true, ClientConfig.USER_AGENT); PreferenceManager.getDefaultSharedPreferences(context) .registerOnSharedPreferenceChangeListener((sharedPreferences, key) -> { - if (key.equals(UserPreferences.PREF_MEDIA_PLAYER)) { + if (UserPreferences.PREF_MEDIA_PLAYER.equals(key)) { checkMpi(); } }); @@ -65,4 +65,9 @@ public class AudioPlayer extends MediaPlayer implements IPlayer { public int getSelectedAudioTrack() { return -1; } + + @Override + public void setDataSource(String streamUrl, String username, String password) throws IOException { + setDataSource(streamUrl); + } } diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/playback/IPlayer.java b/core/src/main/java/de/danoeh/antennapod/core/util/playback/IPlayer.java index 363004709..a511916fa 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/util/playback/IPlayer.java +++ b/core/src/main/java/de/danoeh/antennapod/core/util/playback/IPlayer.java @@ -35,6 +35,8 @@ public interface IPlayer { void setDataSource(String path) throws IllegalStateException, IOException, IllegalArgumentException, SecurityException; + void setDataSource(String streamUrl, String username, String password) throws IOException; + void setDisplay(SurfaceHolder sh); void setPlaybackParams(float speed, boolean skipSilence); diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/playback/PlaybackController.java b/core/src/main/java/de/danoeh/antennapod/core/util/playback/PlaybackController.java index 425a07f4a..e1b4c967c 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/util/playback/PlaybackController.java +++ b/core/src/main/java/de/danoeh/antennapod/core/util/playback/PlaybackController.java @@ -564,6 +564,13 @@ public class PlaybackController { } } + public void extendSleepTimer(long extendTime) { + long timeLeft = getSleepTimerTimeLeft(); + if (playbackService != null && timeLeft != INVALID_TIME) { + setSleepTimer(timeLeft + extendTime); + } + } + public void setSleepTimer(long time) { if (playbackService != null) { playbackService.setSleepTimer(time); diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/playback/PlaybackServiceStarter.java b/core/src/main/java/de/danoeh/antennapod/core/util/playback/PlaybackServiceStarter.java index b12967264..107399e60 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/util/playback/PlaybackServiceStarter.java +++ b/core/src/main/java/de/danoeh/antennapod/core/util/playback/PlaybackServiceStarter.java @@ -2,7 +2,6 @@ package de.danoeh.antennapod.core.util.playback; import android.content.Context; import android.content.Intent; -import androidx.annotation.NonNull; import androidx.core.content.ContextCompat; import de.danoeh.antennapod.core.preferences.PlaybackPreferences; diff --git a/core/src/main/java/de/danoeh/antennapod/core/util/playback/VideoPlayer.java b/core/src/main/java/de/danoeh/antennapod/core/util/playback/VideoPlayer.java index d18801870..6728c027d 100644 --- a/core/src/main/java/de/danoeh/antennapod/core/util/playback/VideoPlayer.java +++ b/core/src/main/java/de/danoeh/antennapod/core/util/playback/VideoPlayer.java @@ -3,6 +3,7 @@ package de.danoeh.antennapod.core.util.playback; import android.media.MediaPlayer; import android.util.Log; +import java.io.IOException; import java.util.Collections; import java.util.List; @@ -52,4 +53,9 @@ public class VideoPlayer extends MediaPlayer implements IPlayer { public int getSelectedAudioTrack() { return -1; } + + @Override + public void setDataSource(String streamUrl, String username, String password) throws IOException { + setDataSource(streamUrl); + } } |