4.9.3 commit
This commit is contained in:
parent
ae82900ae0
commit
9476d7d681
|
@ -158,8 +158,8 @@ android {
|
|||
// Version code schema (not used):
|
||||
// "1.2.3-beta4" -> 1020304
|
||||
// "1.2.3" -> 1020395
|
||||
versionCode 3020133
|
||||
versionName "4.9.2"
|
||||
versionCode 3020134
|
||||
versionName "4.9.3"
|
||||
|
||||
def commit = ""
|
||||
try {
|
||||
|
|
|
@ -20,10 +20,9 @@ class CancelablePSMPCallback(private val originalCallback: PSMPCallback) : PSMPC
|
|||
}
|
||||
|
||||
override fun shouldStop() {
|
||||
if (isCancelled) {
|
||||
return
|
||||
}
|
||||
originalCallback.shouldStop()
|
||||
if (isCancelled) return
|
||||
|
||||
// originalCallback.shouldStop()
|
||||
}
|
||||
|
||||
override fun onMediaChanged(reloadUI: Boolean) {
|
||||
|
@ -33,7 +32,7 @@ class CancelablePSMPCallback(private val originalCallback: PSMPCallback) : PSMPC
|
|||
originalCallback.onMediaChanged(reloadUI)
|
||||
}
|
||||
|
||||
override fun onPostPlayback(media: Playable, ended: Boolean, skipped: Boolean, playingNext: Boolean) {
|
||||
override fun onPostPlayback(media: Playable?, ended: Boolean, skipped: Boolean, playingNext: Boolean) {
|
||||
if (isCancelled) {
|
||||
return
|
||||
}
|
||||
|
|
|
@ -15,7 +15,7 @@ open class DefaultPSMPCallback : PSMPCallback {
|
|||
override fun onMediaChanged(reloadUI: Boolean) {
|
||||
}
|
||||
|
||||
override fun onPostPlayback(media: Playable, ended: Boolean, skipped: Boolean, playingNext: Boolean) {
|
||||
override fun onPostPlayback(media: Playable?, ended: Boolean, skipped: Boolean, playingNext: Boolean) {
|
||||
}
|
||||
|
||||
override fun onPlaybackStart(playable: Playable, position: Int) {
|
||||
|
|
|
@ -25,8 +25,7 @@ class PodciniApp : Application() {
|
|||
override fun onCreate() {
|
||||
super.onCreate()
|
||||
ClientConfig.USER_AGENT = "Podcini/" + BuildConfig.VERSION_NAME
|
||||
ClientConfig.applicationCallbacks =
|
||||
ApplicationCallbacksImpl()
|
||||
ClientConfig.applicationCallbacks = ApplicationCallbacksImpl()
|
||||
|
||||
Thread.setDefaultUncaughtExceptionHandler(CrashReportWriter())
|
||||
RxJavaErrorHandlerSetup.setupRxJavaErrorHandler()
|
||||
|
|
|
@ -13,18 +13,10 @@ object ChapterMerger {
|
|||
fun merge(chapters1: List<Chapter>?, chapters2: List<Chapter>?): List<Chapter>? {
|
||||
Log.d(TAG, "Merging chapters")
|
||||
when {
|
||||
chapters1 == null -> {
|
||||
return chapters2
|
||||
}
|
||||
chapters2 == null -> {
|
||||
return chapters1
|
||||
}
|
||||
chapters2.size > chapters1.size -> {
|
||||
return chapters2
|
||||
}
|
||||
chapters2.size < chapters1.size -> {
|
||||
return chapters1
|
||||
}
|
||||
chapters1 == null -> return chapters2
|
||||
chapters2 == null -> return chapters1
|
||||
chapters2.size > chapters1.size -> return chapters2
|
||||
chapters2.size < chapters1.size -> return chapters1
|
||||
else -> {
|
||||
// Merge chapter lists of same length. Store in chapters2 array.
|
||||
// In case the lists can not be merged, return chapters1 array.
|
||||
|
@ -37,15 +29,9 @@ object ChapterMerger {
|
|||
return if (score(chapters1) > score(chapters2)) chapters1 else chapters2
|
||||
}
|
||||
|
||||
if (chapterTarget.imageUrl.isNullOrEmpty()) {
|
||||
chapterTarget.imageUrl = chapterOther.imageUrl
|
||||
}
|
||||
if (chapterTarget.link.isNullOrEmpty()) {
|
||||
chapterTarget.link = chapterOther.link
|
||||
}
|
||||
if (chapterTarget.title.isNullOrEmpty()) {
|
||||
chapterTarget.title = chapterOther.title
|
||||
}
|
||||
if (chapterTarget.imageUrl.isNullOrEmpty()) chapterTarget.imageUrl = chapterOther.imageUrl
|
||||
if (chapterTarget.link.isNullOrEmpty()) chapterTarget.link = chapterOther.link
|
||||
if (chapterTarget.title.isNullOrEmpty()) chapterTarget.title = chapterOther.title
|
||||
}
|
||||
return chapters2
|
||||
}
|
||||
|
|
|
@ -4,18 +4,12 @@ import ac.mdiq.podcini.R
|
|||
import ac.mdiq.podcini.storage.model.feed.FeedItemFilter
|
||||
|
||||
enum class FeedItemFilterGroup(vararg values: ItemProperties) {
|
||||
PLAYED(ItemProperties(R.string.hide_played_episodes_label, FeedItemFilter.PLAYED),
|
||||
ItemProperties(R.string.not_played, FeedItemFilter.UNPLAYED)),
|
||||
PAUSED(ItemProperties(R.string.hide_paused_episodes_label, FeedItemFilter.PAUSED),
|
||||
ItemProperties(R.string.not_paused, FeedItemFilter.NOT_PAUSED)),
|
||||
FAVORITE(ItemProperties(R.string.hide_is_favorite_label, FeedItemFilter.IS_FAVORITE),
|
||||
ItemProperties(R.string.not_favorite, FeedItemFilter.NOT_FAVORITE)),
|
||||
MEDIA(ItemProperties(R.string.has_media, FeedItemFilter.HAS_MEDIA),
|
||||
ItemProperties(R.string.no_media, FeedItemFilter.NO_MEDIA)),
|
||||
QUEUED(ItemProperties(R.string.queued_label, FeedItemFilter.QUEUED),
|
||||
ItemProperties(R.string.not_queued_label, FeedItemFilter.NOT_QUEUED)),
|
||||
DOWNLOADED(ItemProperties(R.string.hide_downloaded_episodes_label, FeedItemFilter.DOWNLOADED),
|
||||
ItemProperties(R.string.hide_not_downloaded_episodes_label, FeedItemFilter.NOT_DOWNLOADED));
|
||||
PLAYED(ItemProperties(R.string.hide_played_episodes_label, FeedItemFilter.PLAYED), ItemProperties(R.string.not_played, FeedItemFilter.UNPLAYED)),
|
||||
PAUSED(ItemProperties(R.string.hide_paused_episodes_label, FeedItemFilter.PAUSED), ItemProperties(R.string.not_paused, FeedItemFilter.NOT_PAUSED)),
|
||||
FAVORITE(ItemProperties(R.string.hide_is_favorite_label, FeedItemFilter.IS_FAVORITE), ItemProperties(R.string.not_favorite, FeedItemFilter.NOT_FAVORITE)),
|
||||
MEDIA(ItemProperties(R.string.has_media, FeedItemFilter.HAS_MEDIA), ItemProperties(R.string.no_media, FeedItemFilter.NO_MEDIA)),
|
||||
QUEUED(ItemProperties(R.string.queued_label, FeedItemFilter.QUEUED), ItemProperties(R.string.not_queued_label, FeedItemFilter.NOT_QUEUED)),
|
||||
DOWNLOADED(ItemProperties(R.string.hide_downloaded_episodes_label, FeedItemFilter.DOWNLOADED), ItemProperties(R.string.hide_not_downloaded_episodes_label, FeedItemFilter.NOT_DOWNLOADED));
|
||||
|
||||
@JvmField
|
||||
val values: Array<ItemProperties>
|
||||
|
|
|
@ -39,24 +39,17 @@ object LocalFeedUpdater {
|
|||
val PREFERRED_FEED_IMAGE_FILENAMES: Array<String> = arrayOf("folder.jpg", "Folder.jpg", "folder.png", "Folder.png")
|
||||
|
||||
@UnstableApi @JvmStatic
|
||||
fun updateFeed(feed: Feed, context: Context,
|
||||
updaterProgressListener: UpdaterProgressListener?
|
||||
) {
|
||||
fun updateFeed(feed: Feed, context: Context, updaterProgressListener: UpdaterProgressListener?) {
|
||||
if (feed.download_url.isNullOrEmpty()) return
|
||||
try {
|
||||
val uriString = feed.download_url!!.replace(Feed.PREFIX_LOCAL_FOLDER, "")
|
||||
val documentFolder = DocumentFile.fromTreeUri(context, Uri.parse(uriString))
|
||||
?: throw IOException("Unable to retrieve document tree. "
|
||||
+ "Try re-connecting the folder on the podcast info page.")
|
||||
if (!documentFolder.exists() || !documentFolder.canRead()) {
|
||||
throw IOException("Cannot read local directory. "
|
||||
+ "Try re-connecting the folder on the podcast info page.")
|
||||
}
|
||||
tryUpdateFeed(feed, context, documentFolder.uri, updaterProgressListener)
|
||||
?: throw IOException("Unable to retrieve document tree. Try re-connecting the folder on the podcast info page.")
|
||||
if (!documentFolder.exists() || !documentFolder.canRead())
|
||||
throw IOException("Cannot read local directory. Try re-connecting the folder on the podcast info page.")
|
||||
|
||||
if (mustReportDownloadSuccessful(feed)) {
|
||||
reportSuccess(feed)
|
||||
}
|
||||
tryUpdateFeed(feed, context, documentFolder.uri, updaterProgressListener)
|
||||
if (mustReportDownloadSuccessful(feed)) reportSuccess(feed)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
reportError(feed, e.message)
|
||||
|
@ -66,9 +59,7 @@ object LocalFeedUpdater {
|
|||
@UnstableApi @JvmStatic
|
||||
@VisibleForTesting
|
||||
@Throws(IOException::class)
|
||||
fun tryUpdateFeed(feed: Feed, context: Context, folderUri: Uri?,
|
||||
updaterProgressListener: UpdaterProgressListener?
|
||||
) {
|
||||
fun tryUpdateFeed(feed: Feed, context: Context, folderUri: Uri?, updaterProgressListener: UpdaterProgressListener?) {
|
||||
var feed = feed
|
||||
//make sure it is the latest 'version' of this feed from the db (all items etc)
|
||||
feed = DBTasks.updateFeed(context, feed, false)?: feed
|
||||
|
@ -99,9 +90,7 @@ object LocalFeedUpdater {
|
|||
val it = newItems.iterator()
|
||||
while (it.hasNext()) {
|
||||
val feedItem = it.next()
|
||||
if (!mediaFileNames.contains(feedItem.link)) {
|
||||
it.remove()
|
||||
}
|
||||
if (!mediaFileNames.contains(feedItem.link)) it.remove()
|
||||
}
|
||||
|
||||
if (folderUri != null) feed.imageUrl = getImageUrl(allFiles, folderUri)
|
||||
|
@ -120,18 +109,14 @@ object LocalFeedUpdater {
|
|||
// look for special file names
|
||||
for (iconLocation in PREFERRED_FEED_IMAGE_FILENAMES) {
|
||||
for (file in files) {
|
||||
if (iconLocation == file.name) {
|
||||
return file.uri.toString()
|
||||
}
|
||||
if (iconLocation == file.name) return file.uri.toString()
|
||||
}
|
||||
}
|
||||
|
||||
// use the first image in the folder if existing
|
||||
for (file in files) {
|
||||
val mime = file.type
|
||||
if (mime.startsWith("image/jpeg") || mime.startsWith("image/png")) {
|
||||
return file.uri.toString()
|
||||
}
|
||||
if (mime.startsWith("image/jpeg") || mime.startsWith("image/png")) return file.uri.toString()
|
||||
}
|
||||
|
||||
// use default icon as fallback
|
||||
|
@ -141,16 +126,13 @@ object LocalFeedUpdater {
|
|||
private fun feedContainsFile(feed: Feed, filename: String): FeedItem? {
|
||||
val items = feed.items
|
||||
for (i in items) {
|
||||
if (i.media != null && i.link == filename) {
|
||||
return i
|
||||
}
|
||||
if (i.media != null && i.link == filename) return i
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
private fun createFeedItem(feed: Feed, file: FastDocumentFile, context: Context): FeedItem {
|
||||
val item = FeedItem(0, file.name, UUID.randomUUID().toString(),
|
||||
file.name, Date(file.lastModified), FeedItem.UNPLAYED, feed)
|
||||
val item = FeedItem(0, file.name, UUID.randomUUID().toString(), file.name, Date(file.lastModified), FeedItem.UNPLAYED, feed)
|
||||
item.disableAutoDownload()
|
||||
|
||||
val size = file.length
|
||||
|
@ -159,9 +141,8 @@ object LocalFeedUpdater {
|
|||
item.media = media
|
||||
|
||||
for (existingItem in feed.items) {
|
||||
if (existingItem.media != null && existingItem.media!!
|
||||
.download_url == file.uri.toString() && file.length == existingItem.media!!
|
||||
.size) {
|
||||
if (existingItem.media != null && existingItem.media!!.download_url == file.uri.toString()
|
||||
&& file.length == existingItem.media!!.size) {
|
||||
// We found an old file that we already scanned. Re-use metadata.
|
||||
item.updateFromOther(existingItem)
|
||||
return item
|
||||
|
@ -187,16 +168,12 @@ object LocalFeedUpdater {
|
|||
item.pubDate = simpleDateFormat.parse(dateStr)
|
||||
} catch (parseException: ParseException) {
|
||||
val date = DateUtils.parse(dateStr)
|
||||
if (date != null) {
|
||||
item.pubDate = date
|
||||
}
|
||||
if (date != null) item.pubDate = date
|
||||
}
|
||||
}
|
||||
|
||||
val title = mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_TITLE)
|
||||
if (!title.isNullOrEmpty()) {
|
||||
item.title = title
|
||||
}
|
||||
if (!title.isNullOrEmpty()) item.title = title
|
||||
|
||||
val durationStr = mediaMetadataRetriever.extractMetadata(MediaMetadataRetriever.METADATA_KEY_DURATION)
|
||||
item.media!!.setDuration(durationStr!!.toLong().toInt())
|
||||
|
@ -204,8 +181,7 @@ object LocalFeedUpdater {
|
|||
item.media!!.setHasEmbeddedPicture(mediaMetadataRetriever.embeddedPicture != null)
|
||||
try {
|
||||
context.contentResolver.openInputStream(file.uri).use { inputStream ->
|
||||
val reader = Id3MetadataReader(
|
||||
CountingInputStream(BufferedInputStream(inputStream)))
|
||||
val reader = Id3MetadataReader(CountingInputStream(BufferedInputStream(inputStream)))
|
||||
reader.readInputStream()
|
||||
item.setDescriptionIfLonger(reader.comment)
|
||||
}
|
||||
|
@ -263,10 +239,8 @@ object LocalFeedUpdater {
|
|||
private fun mustReportDownloadSuccessful(feed: Feed): Boolean {
|
||||
val downloadResults = DBReader.getFeedDownloadLog(feed.id).toMutableList()
|
||||
|
||||
if (downloadResults.isEmpty()) {
|
||||
// report success if never reported before
|
||||
return true
|
||||
}
|
||||
if (downloadResults.isEmpty()) return true
|
||||
|
||||
downloadResults.sortWith { downloadStatus1: DownloadResult, downloadStatus2: DownloadResult ->
|
||||
downloadStatus1.getCompletionDate().compareTo(downloadStatus2.getCompletionDate())
|
||||
|
|
|
@ -4,14 +4,9 @@ import ac.mdiq.podcini.R
|
|||
|
||||
enum class SubscriptionsFilterGroup(vararg values: ItemProperties) {
|
||||
COUNTER_GREATER_ZERO(ItemProperties(R.string.subscriptions_counter_greater_zero, "counter_greater_zero")),
|
||||
AUTO_DOWNLOAD(ItemProperties(R.string.auto_downloaded, "enabled_auto_download"),
|
||||
ItemProperties(R.string.not_auto_downloaded, "disabled_auto_download")),
|
||||
UPDATED(ItemProperties(R.string.kept_updated, "enabled_updates"),
|
||||
ItemProperties(R.string.not_kept_updated, "disabled_updates")),
|
||||
NEW_EPISODE_NOTIFICATION(ItemProperties(R.string.new_episode_notification_enabled,
|
||||
"episode_notification_enabled"),
|
||||
ItemProperties(R.string.new_episode_notification_disabled, "episode_notification_disabled"));
|
||||
|
||||
AUTO_DOWNLOAD(ItemProperties(R.string.auto_downloaded, "enabled_auto_download"), ItemProperties(R.string.not_auto_downloaded, "disabled_auto_download")),
|
||||
UPDATED(ItemProperties(R.string.kept_updated, "enabled_updates"), ItemProperties(R.string.not_kept_updated, "disabled_updates")),
|
||||
NEW_EPISODE_NOTIFICATION(ItemProperties(R.string.new_episode_notification_enabled, "episode_notification_enabled"), ItemProperties(R.string.new_episode_notification_disabled, "episode_notification_disabled"));
|
||||
|
||||
@JvmField
|
||||
val values: Array<ItemProperties>
|
||||
|
|
|
@ -14,9 +14,7 @@ class SyndHandler(feed: Feed, type: TypeGetter.Type) : DefaultHandler() {
|
|||
val state: HandlerState = HandlerState(feed)
|
||||
|
||||
init {
|
||||
if (type == TypeGetter.Type.RSS20 || type == TypeGetter.Type.RSS091) {
|
||||
state.defaultNamespaces.push(Rss20())
|
||||
}
|
||||
if (type == TypeGetter.Type.RSS20 || type == TypeGetter.Type.RSS091) state.defaultNamespaces.push(Rss20())
|
||||
}
|
||||
|
||||
@Throws(SAXException::class)
|
||||
|
@ -31,9 +29,7 @@ class SyndHandler(feed: Feed, type: TypeGetter.Type) : DefaultHandler() {
|
|||
|
||||
@Throws(SAXException::class)
|
||||
override fun characters(ch: CharArray, start: Int, length: Int) {
|
||||
if (state.tagstack.size >= 2 && state.contentBuf != null) {
|
||||
state.contentBuf!!.appendRange(ch, start, start + length)
|
||||
}
|
||||
if (state.tagstack.size >= 2 && state.contentBuf != null) state.contentBuf!!.appendRange(ch, start, start + length)
|
||||
}
|
||||
|
||||
@Throws(SAXException::class)
|
||||
|
@ -48,9 +44,7 @@ class SyndHandler(feed: Feed, type: TypeGetter.Type) : DefaultHandler() {
|
|||
|
||||
@Throws(SAXException::class)
|
||||
override fun endPrefixMapping(prefix: String) {
|
||||
if (state.defaultNamespaces.size > 1 && prefix == DEFAULT_PREFIX) {
|
||||
state.defaultNamespaces.pop()
|
||||
}
|
||||
if (state.defaultNamespaces.size > 1 && prefix == DEFAULT_PREFIX) state.defaultNamespaces.pop()
|
||||
}
|
||||
|
||||
@Throws(SAXException::class)
|
||||
|
@ -60,9 +54,7 @@ class SyndHandler(feed: Feed, type: TypeGetter.Type) : DefaultHandler() {
|
|||
when {
|
||||
uri == Atom.NSURI -> {
|
||||
when (prefix) {
|
||||
DEFAULT_PREFIX -> {
|
||||
state.defaultNamespaces.push(Atom())
|
||||
}
|
||||
DEFAULT_PREFIX -> state.defaultNamespaces.push(Atom())
|
||||
Atom.NSTAG -> {
|
||||
state.namespaces[uri] = Atom()
|
||||
Log.d(TAG, "Recognized Atom namespace")
|
||||
|
@ -103,9 +95,7 @@ class SyndHandler(feed: Feed, type: TypeGetter.Type) : DefaultHandler() {
|
|||
|
||||
private fun getHandlingNamespace(uri: String, qualifiedName: String): Namespace? {
|
||||
var handler = state.namespaces[uri]
|
||||
if (handler == null && !state.defaultNamespaces.empty() && !qualifiedName.contains(":")) {
|
||||
handler = state.defaultNamespaces.peek()
|
||||
}
|
||||
if (handler == null && !state.defaultNamespaces.empty() && !qualifiedName.contains(":")) handler = state.defaultNamespaces.peek()
|
||||
return handler
|
||||
}
|
||||
|
||||
|
|
|
@ -9,15 +9,9 @@ class UnsupportedFeedtypeException : Exception {
|
|||
override var message: String? = null
|
||||
get() {
|
||||
return when {
|
||||
field != null -> {
|
||||
field!!
|
||||
}
|
||||
type == TypeGetter.Type.INVALID -> {
|
||||
"Invalid type"
|
||||
}
|
||||
else -> {
|
||||
"Type $type not supported"
|
||||
}
|
||||
field != null -> field!!
|
||||
type == TypeGetter.Type.INVALID -> "Invalid type"
|
||||
else -> "Type $type not supported"
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -11,18 +11,11 @@ class AtomText(name: String?, namespace: Namespace?, private val type: String?)
|
|||
val processedContent: String?
|
||||
/** Processes the content according to the type and returns it. */
|
||||
get() = when (type) {
|
||||
null -> {
|
||||
content
|
||||
}
|
||||
TYPE_HTML -> {
|
||||
HtmlCompat.fromHtml(content!!, HtmlCompat.FROM_HTML_MODE_LEGACY).toString()
|
||||
}
|
||||
TYPE_XHTML -> {
|
||||
content
|
||||
}
|
||||
else -> { // Handle as text by default
|
||||
content
|
||||
}
|
||||
null -> content
|
||||
TYPE_HTML -> HtmlCompat.fromHtml(content!!, HtmlCompat.FROM_HTML_MODE_LEGACY).toString()
|
||||
TYPE_XHTML -> content
|
||||
// Handle as text by default
|
||||
else -> content
|
||||
}
|
||||
|
||||
fun setContent(content: String?) {
|
||||
|
|
|
@ -75,15 +75,11 @@ class ChapterReader(input: CountingInputStream?) : ID3Reader(input!!) {
|
|||
if (MIME_IMAGE_URL == mime) {
|
||||
val link = readIsoStringNullTerminated(frameHeader.size)
|
||||
Log.d(TAG, "Link: $link")
|
||||
if (chapter.imageUrl.isNullOrEmpty() || type.toInt() == IMAGE_TYPE_COVER) {
|
||||
chapter.imageUrl = link
|
||||
}
|
||||
if (chapter.imageUrl.isNullOrEmpty() || type.toInt() == IMAGE_TYPE_COVER) chapter.imageUrl = link
|
||||
} else {
|
||||
val alreadyConsumed = position - frameStartPosition
|
||||
val rawImageDataLength = frameHeader.size - alreadyConsumed
|
||||
if (chapter.imageUrl.isNullOrEmpty() || type.toInt() == IMAGE_TYPE_COVER) {
|
||||
chapter.imageUrl = makeUrl(position, rawImageDataLength)
|
||||
}
|
||||
if (chapter.imageUrl.isNullOrEmpty() || type.toInt() == IMAGE_TYPE_COVER) chapter.imageUrl = makeUrl(position, rawImageDataLength)
|
||||
}
|
||||
}
|
||||
else -> Log.d(TAG, "Unknown chapter sub-frame.")
|
||||
|
|
|
@ -46,9 +46,8 @@ open class ID3Reader(private val inputStream: CountingInputStream) {
|
|||
*/
|
||||
@Throws(IOException::class, ID3ReaderException::class)
|
||||
fun skipBytes(number: Int) {
|
||||
if (number < 0) {
|
||||
throw ID3ReaderException("Trying to read a negative number of bytes")
|
||||
}
|
||||
if (number < 0) throw ID3ReaderException("Trying to read a negative number of bytes")
|
||||
|
||||
IOUtils.skipFully(inputStream, number.toLong())
|
||||
}
|
||||
|
||||
|
@ -76,9 +75,7 @@ open class ID3Reader(private val inputStream: CountingInputStream) {
|
|||
@Throws(ID3ReaderException::class, IOException::class)
|
||||
fun expectChar(expected: Char) {
|
||||
val read = inputStream.read().toChar()
|
||||
if (read != expected) {
|
||||
throw ID3ReaderException("Expected $expected and got $read")
|
||||
}
|
||||
if (read != expected) throw ID3ReaderException("Expected $expected and got $read")
|
||||
}
|
||||
|
||||
@Throws(ID3ReaderException::class, IOException::class)
|
||||
|
@ -100,9 +97,8 @@ open class ID3Reader(private val inputStream: CountingInputStream) {
|
|||
fun readFrameHeader(): FrameHeader {
|
||||
val id = readPlainBytesToString(FRAME_ID_LENGTH)
|
||||
var size = readInt()
|
||||
if (tagHeader != null && tagHeader!!.version >= 0x0400) {
|
||||
size = unsynchsafe(size)
|
||||
}
|
||||
if (tagHeader != null && tagHeader!!.version >= 0x0400) size = unsynchsafe(size)
|
||||
|
||||
val flags = readShort()
|
||||
return FrameHeader(id, size, flags)
|
||||
}
|
||||
|
@ -148,15 +144,9 @@ open class ID3Reader(private val inputStream: CountingInputStream) {
|
|||
@Throws(IOException::class)
|
||||
fun readEncodedString(encoding: Int, max: Int): String {
|
||||
return when (encoding) {
|
||||
ENCODING_UTF16_WITH_BOM.toInt(), ENCODING_UTF16_WITHOUT_BOM.toInt() -> {
|
||||
readEncodedString2(Charset.forName("UTF-16"), max)
|
||||
}
|
||||
ENCODING_UTF8.toInt() -> {
|
||||
readEncodedString2(Charset.forName("UTF-8"), max)
|
||||
}
|
||||
else -> {
|
||||
readEncodedString1(Charset.forName("ISO-8859-1"), max)
|
||||
}
|
||||
ENCODING_UTF16_WITH_BOM.toInt(), ENCODING_UTF16_WITHOUT_BOM.toInt() -> readEncodedString2(Charset.forName("UTF-16"), max)
|
||||
ENCODING_UTF8.toInt() -> readEncodedString2(Charset.forName("UTF-8"), max)
|
||||
else -> readEncodedString1(Charset.forName("ISO-8859-1"), max)
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -170,9 +160,8 @@ open class ID3Reader(private val inputStream: CountingInputStream) {
|
|||
while (bytesRead < max) {
|
||||
val c = readByte()
|
||||
bytesRead++
|
||||
if (c.toInt() == 0) {
|
||||
break
|
||||
}
|
||||
if (c.toInt() == 0) break
|
||||
|
||||
bytes.write(c.toInt())
|
||||
}
|
||||
return charset.newDecoder().decode(ByteBuffer.wrap(bytes.toByteArray())).toString()
|
||||
|
@ -200,9 +189,7 @@ open class ID3Reader(private val inputStream: CountingInputStream) {
|
|||
if (!foundEnd && bytesRead < max) {
|
||||
// Last character
|
||||
val c = readByte()
|
||||
if (c.toInt() != 0) {
|
||||
bytes.write(c.toInt())
|
||||
}
|
||||
if (c.toInt() != 0) bytes.write(c.toInt())
|
||||
}
|
||||
return try {
|
||||
charset.newDecoder().decode(ByteBuffer.wrap(bytes.toByteArray())).toString()
|
||||
|
|
|
@ -18,8 +18,7 @@ class Id3MetadataReader(input: CountingInputStream?) : ID3Reader(input!!) {
|
|||
val encoding = readByte().toInt()
|
||||
skipBytes(3) // Language
|
||||
val shortDescription = readEncodedString(encoding, frameHeader.size - 4)
|
||||
val longDescription = readEncodedString(encoding,
|
||||
(frameHeader.size - (position - frameStart)).toInt())
|
||||
val longDescription = readEncodedString(encoding, (frameHeader.size - (position - frameStart)).toInt())
|
||||
comment = if (shortDescription.length > longDescription.length) shortDescription else longDescription
|
||||
} else {
|
||||
super.readFrame(frameHeader)
|
||||
|
|
|
@ -15,9 +15,8 @@ class VorbisCommentChapterReader(input: InputStream?) : VorbisCommentReader(inpu
|
|||
|
||||
@Throws(VorbisCommentReaderException::class)
|
||||
public override fun onContentVectorValue(key: String?, value: String?) {
|
||||
if (BuildConfig.DEBUG) {
|
||||
Log.d(TAG, "Key: $key, value: $value")
|
||||
}
|
||||
if (BuildConfig.DEBUG) Log.d(TAG, "Key: $key, value: $value")
|
||||
|
||||
val attribute = getAttributeTypeFromKey(key)
|
||||
val id = getIdFromKey(key)
|
||||
var chapter = getChapterById(id.toLong())
|
||||
|
@ -30,24 +29,16 @@ class VorbisCommentChapterReader(input: InputStream?) : VorbisCommentReader(inpu
|
|||
chapter.chapterId = "" + id
|
||||
chapter.start = start
|
||||
chapters.add(chapter)
|
||||
} else {
|
||||
throw VorbisCommentReaderException("Found chapter with duplicate ID ($key, $value)")
|
||||
}
|
||||
}
|
||||
CHAPTER_ATTRIBUTE_TITLE -> {
|
||||
if (chapter != null) chapter.title = value
|
||||
}
|
||||
CHAPTER_ATTRIBUTE_LINK -> {
|
||||
if (chapter != null) chapter.link = value
|
||||
} else throw VorbisCommentReaderException("Found chapter with duplicate ID ($key, $value)")
|
||||
}
|
||||
CHAPTER_ATTRIBUTE_TITLE -> if (chapter != null) chapter.title = value
|
||||
CHAPTER_ATTRIBUTE_LINK -> if (chapter != null) chapter.link = value
|
||||
}
|
||||
}
|
||||
|
||||
private fun getChapterById(id: Long): Chapter? {
|
||||
for (c in chapters) {
|
||||
if (("" + id) == c.chapterId) {
|
||||
return c
|
||||
}
|
||||
if (("" + id) == c.chapterId) return c
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
@ -73,11 +64,8 @@ class VorbisCommentChapterReader(input: InputStream?) : VorbisCommentReader(inpu
|
|||
parts[0].toLong(), TimeUnit.HOURS)
|
||||
val minutes = TimeUnit.MILLISECONDS.convert(
|
||||
parts[1].toLong(), TimeUnit.MINUTES)
|
||||
if (parts[2].contains("-->")) {
|
||||
parts[2] = parts[2].substring(0, parts[2].indexOf("-->"))
|
||||
}
|
||||
val seconds = TimeUnit.MILLISECONDS.convert(
|
||||
(parts[2].toFloat().toLong()), TimeUnit.SECONDS)
|
||||
if (parts[2].contains("-->")) parts[2] = parts[2].substring(0, parts[2].indexOf("-->"))
|
||||
val seconds = TimeUnit.MILLISECONDS.convert((parts[2].toFloat().toLong()), TimeUnit.SECONDS)
|
||||
return hours + minutes + seconds
|
||||
} catch (e: NumberFormatException) {
|
||||
throw VorbisCommentReaderException(e)
|
||||
|
@ -111,9 +99,8 @@ class VorbisCommentChapterReader(input: InputStream?) : VorbisCommentReader(inpu
|
|||
* 'url'.
|
||||
*/
|
||||
private fun getAttributeTypeFromKey(key: String?): String? {
|
||||
if (key!!.length > CHAPTERXXX_LENGTH) {
|
||||
return key.substring(CHAPTERXXX_LENGTH)
|
||||
}
|
||||
if (key!!.length > CHAPTERXXX_LENGTH) return key.substring(CHAPTERXXX_LENGTH)
|
||||
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,9 +12,7 @@ class VorbisCommentMetadataReader(input: InputStream?) : VorbisCommentReader(inp
|
|||
|
||||
public override fun onContentVectorValue(key: String?, value: String?) {
|
||||
if (KEY_DESCRIPTION == key || KEY_COMMENT == key) {
|
||||
if (description == null || (value != null && value.length > description!!.length)) {
|
||||
description = value
|
||||
}
|
||||
if (description == null || (value != null && value.length > description!!.length)) description = value
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -33,13 +33,10 @@ abstract class VorbisCommentReader internal constructor(private val input: Input
|
|||
val oggPageHeader = byteArrayOf('O'.code.toByte(), 'g'.code.toByte(), 'g'.code.toByte(), 'S'.code.toByte())
|
||||
for (bytesRead in 0 until SECOND_PAGE_MAX_LENGTH) {
|
||||
val data = input.read()
|
||||
if (data == -1) {
|
||||
throw IOException("EOF while trying to find vorbis page")
|
||||
}
|
||||
if (data == -1) throw IOException("EOF while trying to find vorbis page")
|
||||
|
||||
buffer[bytesRead % buffer.size] = data.toByte()
|
||||
if (bufferMatches(buffer, oggPageHeader, bytesRead)) {
|
||||
break
|
||||
}
|
||||
if (bufferMatches(buffer, oggPageHeader, bytesRead)) break
|
||||
}
|
||||
// read segments
|
||||
IOUtils.skipFully(input, 22)
|
||||
|
@ -53,8 +50,7 @@ abstract class VorbisCommentReader internal constructor(private val input: Input
|
|||
val vectorLength = EndianUtils.readSwappedUnsignedInteger(input)
|
||||
if (vectorLength > 20 * 1024 * 1024) {
|
||||
val keyPart = readUtf8String(10)
|
||||
throw VorbisCommentReaderException("User comment unrealistically long. "
|
||||
+ "key=" + keyPart + ", length=" + vectorLength)
|
||||
throw VorbisCommentReaderException("User comment unrealistically long. key=$keyPart, length=$vectorLength")
|
||||
}
|
||||
val key = readContentVectorKey(vectorLength)!!.lowercase()
|
||||
val shouldReadValue = handles(key)
|
||||
|
@ -62,9 +58,8 @@ abstract class VorbisCommentReader internal constructor(private val input: Input
|
|||
if (shouldReadValue) {
|
||||
val value = readUtf8String(vectorLength - key.length - 1)
|
||||
onContentVectorValue(key, value)
|
||||
} else {
|
||||
IOUtils.skipFully(input, vectorLength - key.length - 1)
|
||||
}
|
||||
} else IOUtils.skipFully(input, vectorLength - key.length - 1)
|
||||
|
||||
} catch (e: IOException) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
|
@ -99,9 +94,7 @@ abstract class VorbisCommentReader internal constructor(private val input: Input
|
|||
IOUtils.skip(input, (FIRST_OGG_PAGE_LENGTH - FIRST_OPUS_PAGE_LENGTH).toLong())
|
||||
return
|
||||
}
|
||||
bufferMatches(buffer, "OpusHead".toByteArray(), i) -> {
|
||||
return
|
||||
}
|
||||
bufferMatches(buffer, "OpusHead".toByteArray(), i) -> return
|
||||
}
|
||||
}
|
||||
throw IOException("No vorbis identification header found")
|
||||
|
@ -120,12 +113,8 @@ abstract class VorbisCommentReader internal constructor(private val input: Input
|
|||
for (bytesRead in 0 until SECOND_PAGE_MAX_LENGTH) {
|
||||
buffer[bytesRead % buffer.size] = input.read().toByte()
|
||||
when {
|
||||
bufferMatches(buffer, oggCommentHeader, bytesRead) -> {
|
||||
return
|
||||
}
|
||||
bufferMatches(buffer, "OpusTags".toByteArray(), bytesRead) -> {
|
||||
return
|
||||
}
|
||||
bufferMatches(buffer, oggCommentHeader, bytesRead) -> return
|
||||
bufferMatches(buffer, "OpusTags".toByteArray(), bytesRead) -> return
|
||||
}
|
||||
}
|
||||
throw IOException("No comment header found")
|
||||
|
@ -142,9 +131,7 @@ abstract class VorbisCommentReader internal constructor(private val input: Input
|
|||
posInHaystack += haystack.size
|
||||
}
|
||||
posInHaystack %= haystack.size
|
||||
if (haystack[posInHaystack] != needle[needle.size - 1 - i]) {
|
||||
return false
|
||||
}
|
||||
if (haystack[posInHaystack] != needle[needle.size - 1 - i]) return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
@ -166,11 +153,8 @@ abstract class VorbisCommentReader internal constructor(private val input: Input
|
|||
val builder = StringBuilder()
|
||||
for (i in 0 until vectorLength) {
|
||||
val c = input.read().toChar()
|
||||
if (c == '=') {
|
||||
return builder.toString()
|
||||
} else {
|
||||
builder.append(c)
|
||||
}
|
||||
if (c == '=') return builder.toString()
|
||||
else builder.append(c)
|
||||
}
|
||||
return null // no key found
|
||||
}
|
||||
|
|
|
@ -33,29 +33,22 @@ class Atom : Namespace() {
|
|||
when {
|
||||
parent.name.matches(isFeedItem.toRegex()) -> {
|
||||
when {
|
||||
rel == null || LINK_REL_ALTERNATE == rel -> {
|
||||
if (state.currentItem != null) state.currentItem!!.link = href
|
||||
}
|
||||
rel == null || LINK_REL_ALTERNATE == rel -> if (state.currentItem != null) state.currentItem!!.link = href
|
||||
LINK_REL_ENCLOSURE == rel -> {
|
||||
val strSize: String? = attributes.getValue(LINK_LENGTH)
|
||||
var size: Long = 0
|
||||
try {
|
||||
if (strSize != null) {
|
||||
size = strSize.toLong()
|
||||
}
|
||||
if (strSize != null) size = strSize.toLong()
|
||||
} catch (e: NumberFormatException) {
|
||||
Log.d(TAG, "Length attribute could not be parsed.")
|
||||
}
|
||||
val mimeType: String? = getMimeType(attributes.getValue(LINK_TYPE), href)
|
||||
|
||||
val currItem = state.currentItem
|
||||
if (isMediaFile(mimeType) && currItem != null && !currItem.hasMedia()) {
|
||||
if (isMediaFile(mimeType) && currItem != null && !currItem.hasMedia())
|
||||
currItem.media = FeedMedia(currItem, href, size, mimeType)
|
||||
}
|
||||
}
|
||||
LINK_REL_PAYMENT == rel -> {
|
||||
if (state.currentItem != null) state.currentItem!!.paymentLink = href
|
||||
}
|
||||
LINK_REL_PAYMENT == rel -> if (state.currentItem != null) state.currentItem!!.paymentLink = href
|
||||
}
|
||||
}
|
||||
parent.name.matches(isFeed.toRegex()) -> {
|
||||
|
@ -68,15 +61,12 @@ class Atom : Namespace() {
|
|||
* LINK_TYPE_HTML or LINK_TYPE_XHTML
|
||||
*/
|
||||
when {
|
||||
type == null && state.feed.link == null || LINK_TYPE_HTML == type || LINK_TYPE_XHTML == type -> {
|
||||
type == null && state.feed.link == null || LINK_TYPE_HTML == type || LINK_TYPE_XHTML == type ->
|
||||
state.feed.link = href
|
||||
}
|
||||
LINK_TYPE_ATOM == type || LINK_TYPE_RSS == type -> {
|
||||
// treat as podlove alternate feed
|
||||
var title: String? = attributes.getValue(LINK_TITLE)
|
||||
if (title.isNullOrEmpty()) {
|
||||
title = href?:""
|
||||
}
|
||||
if (title.isNullOrEmpty()) title = href?:""
|
||||
if (!href.isNullOrEmpty()) state.addAlternateFeedUrl(title, href)
|
||||
}
|
||||
}
|
||||
|
@ -86,9 +76,7 @@ class Atom : Namespace() {
|
|||
when {
|
||||
LINK_TYPE_ATOM == type || LINK_TYPE_RSS == type -> {
|
||||
var title: String? = attributes.getValue(LINK_TITLE)
|
||||
if (title.isNullOrEmpty()) {
|
||||
title = href?:""
|
||||
}
|
||||
if (title.isNullOrEmpty()) title = href?:""
|
||||
if (!href.isNullOrEmpty()) state.addAlternateFeedUrl(title, href)
|
||||
}
|
||||
LINK_TYPE_HTML == type || LINK_TYPE_XHTML == type -> {
|
||||
|
@ -96,9 +84,7 @@ class Atom : Namespace() {
|
|||
}
|
||||
}
|
||||
}
|
||||
LINK_REL_PAYMENT == rel -> {
|
||||
state.feed.addPayment(FeedFunding(href, ""))
|
||||
}
|
||||
LINK_REL_PAYMENT == rel -> state.feed.addPayment(FeedFunding(href, ""))
|
||||
LINK_REL_NEXT == rel -> {
|
||||
state.feed.isPaged = true
|
||||
state.feed.nextPageLink = href
|
||||
|
@ -114,8 +100,7 @@ class Atom : Namespace() {
|
|||
override fun handleElementEnd(localName: String, state: HandlerState) {
|
||||
Log.d(TAG, "handleElementEnd $localName")
|
||||
if (ENTRY == localName) {
|
||||
if (state.currentItem != null &&
|
||||
state.tempObjects.containsKey(Itunes.DURATION)) {
|
||||
if (state.currentItem != null && state.tempObjects.containsKey(Itunes.DURATION)) {
|
||||
val currentItem = state.currentItem
|
||||
if (currentItem!!.hasMedia()) {
|
||||
val duration = state.tempObjects[Itunes.DURATION] as Int?
|
||||
|
@ -128,11 +113,7 @@ class Atom : Namespace() {
|
|||
|
||||
if (state.tagstack.size >= 2) {
|
||||
var textElement: AtomText? = null
|
||||
val contentRaw = if (state.contentBuf != null) {
|
||||
state.contentBuf.toString()
|
||||
} else {
|
||||
""
|
||||
}
|
||||
val contentRaw = if (state.contentBuf != null) state.contentBuf.toString() else ""
|
||||
val content = trimAllWhitespace(contentRaw)
|
||||
val topElement = state.tagstack.peek()
|
||||
val top = topElement.name
|
||||
|
@ -147,52 +128,30 @@ class Atom : Namespace() {
|
|||
when {
|
||||
ID == top -> {
|
||||
when {
|
||||
FEED == second -> {
|
||||
state.feed.feedIdentifier = contentRaw
|
||||
}
|
||||
ENTRY == second && state.currentItem != null -> {
|
||||
state.currentItem!!.itemIdentifier = contentRaw
|
||||
}
|
||||
FEED == second -> state.feed.feedIdentifier = contentRaw
|
||||
ENTRY == second && state.currentItem != null -> state.currentItem!!.itemIdentifier = contentRaw
|
||||
}
|
||||
}
|
||||
TITLE == top && textElement != null -> {
|
||||
when {
|
||||
FEED == second -> {
|
||||
state.feed.title = textElement.processedContent
|
||||
}
|
||||
ENTRY == second && state.currentItem != null -> {
|
||||
state.currentItem!!.title = textElement.processedContent
|
||||
FEED == second -> state.feed.title = textElement.processedContent
|
||||
ENTRY == second && state.currentItem != null -> state.currentItem!!.title = textElement.processedContent
|
||||
}
|
||||
}
|
||||
}
|
||||
SUBTITLE == top && FEED == second && textElement != null -> {
|
||||
state.feed.description = textElement.processedContent
|
||||
}
|
||||
CONTENT == top && ENTRY == second && textElement != null && state.currentItem != null -> {
|
||||
SUBTITLE == top && FEED == second && textElement != null -> state.feed.description = textElement.processedContent
|
||||
CONTENT == top && ENTRY == second && textElement != null && state.currentItem != null ->
|
||||
state.currentItem!!.setDescriptionIfLonger(textElement.processedContent)
|
||||
}
|
||||
SUMMARY == top && ENTRY == second && textElement != null && state.currentItem != null -> {
|
||||
SUMMARY == top && ENTRY == second && textElement != null && state.currentItem != null ->
|
||||
state.currentItem!!.setDescriptionIfLonger(textElement.processedContent)
|
||||
}
|
||||
UPDATED == top && ENTRY == second && state.currentItem != null && state.currentItem!!.pubDate == null -> {
|
||||
UPDATED == top && ENTRY == second && state.currentItem != null && state.currentItem!!.pubDate == null ->
|
||||
state.currentItem!!.pubDate = parseOrNullIfFuture(content)
|
||||
}
|
||||
PUBLISHED == top && ENTRY == second && state.currentItem != null -> {
|
||||
state.currentItem!!.pubDate = parseOrNullIfFuture(content)
|
||||
}
|
||||
IMAGE_LOGO == top && state.feed.imageUrl == null -> {
|
||||
state.feed.imageUrl = content
|
||||
}
|
||||
IMAGE_ICON == top -> {
|
||||
state.feed.imageUrl = content
|
||||
}
|
||||
PUBLISHED == top && ENTRY == second && state.currentItem != null -> state.currentItem!!.pubDate = parseOrNullIfFuture(content)
|
||||
IMAGE_LOGO == top && state.feed.imageUrl == null -> state.feed.imageUrl = content
|
||||
IMAGE_ICON == top -> state.feed.imageUrl = content
|
||||
AUTHOR_NAME == top && AUTHOR == second && state.currentItem == null -> {
|
||||
val currentName = state.feed.author
|
||||
if (currentName == null) {
|
||||
state.feed.author = content
|
||||
} else {
|
||||
state.feed.author = "$currentName, $content"
|
||||
}
|
||||
if (currentName == null) state.feed.author = content
|
||||
else state.feed.author = "$currentName, $content"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,29 +18,21 @@ class Itunes : Namespace() {
|
|||
} else {
|
||||
// this is the feed image
|
||||
// prefer to all other images
|
||||
if (!url.isNullOrEmpty()) {
|
||||
state.feed.imageUrl = url
|
||||
}
|
||||
if (!url.isNullOrEmpty()) state.feed.imageUrl = url
|
||||
}
|
||||
}
|
||||
return SyndElement(localName, this)
|
||||
}
|
||||
|
||||
override fun handleElementEnd(localName: String, state: HandlerState) {
|
||||
if (state.contentBuf == null) {
|
||||
return
|
||||
}
|
||||
if (state.contentBuf == null) return
|
||||
|
||||
val content = state.contentBuf.toString()
|
||||
val contentFromHtml = HtmlCompat.fromHtml(content, HtmlCompat.FROM_HTML_MODE_COMPACT).toString()
|
||||
if (content.isEmpty()) {
|
||||
return
|
||||
}
|
||||
if (content.isEmpty()) return
|
||||
|
||||
when {
|
||||
AUTHOR == localName && state.tagstack.size <= 3 -> {
|
||||
state.feed.author = contentFromHtml
|
||||
}
|
||||
AUTHOR == localName && state.tagstack.size <= 3 -> state.feed.author = contentFromHtml
|
||||
DURATION == localName -> {
|
||||
try {
|
||||
val durationMs = inMillis(content)
|
||||
|
@ -51,27 +43,17 @@ class Itunes : Namespace() {
|
|||
}
|
||||
SUBTITLE == localName -> {
|
||||
when {
|
||||
state.currentItem != null && state.currentItem?.description.isNullOrEmpty() -> {
|
||||
state.currentItem!!.setDescriptionIfLonger(content)
|
||||
}
|
||||
state.feed.description.isNullOrEmpty() -> {
|
||||
state.feed.description = content
|
||||
}
|
||||
state.currentItem != null && state.currentItem?.description.isNullOrEmpty() -> state.currentItem!!.setDescriptionIfLonger(content)
|
||||
state.feed.description.isNullOrEmpty() -> state.feed.description = content
|
||||
}
|
||||
}
|
||||
SUMMARY == localName -> {
|
||||
when {
|
||||
state.currentItem != null -> {
|
||||
state.currentItem!!.setDescriptionIfLonger(content)
|
||||
}
|
||||
Rss20.CHANNEL == state.secondTag.name -> {
|
||||
state.feed.description = content
|
||||
state.currentItem != null -> state.currentItem!!.setDescriptionIfLonger(content)
|
||||
Rss20.CHANNEL == state.secondTag.name -> state.feed.description = content
|
||||
}
|
||||
}
|
||||
}
|
||||
NEW_FEED_URL == localName && content.trim { it <= ' ' }.startsWith("http") -> {
|
||||
state.redirectUrl = content.trim { it <= ' ' }
|
||||
}
|
||||
NEW_FEED_URL == localName && content.trim { it <= ' ' }.startsWith("http") -> state.redirectUrl = content.trim { it <= ' ' }
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -34,20 +34,15 @@ class Media : Namespace() {
|
|||
validTypeMedia = true
|
||||
mimeType = "video/*"
|
||||
}
|
||||
MEDIUM_IMAGE == medium && (mimeType == null
|
||||
|| (!mimeType.startsWith("audio/") && !mimeType.startsWith("video/"))) -> {
|
||||
MEDIUM_IMAGE == medium && (mimeType == null || (!mimeType.startsWith("audio/") && !mimeType.startsWith("video/"))) -> {
|
||||
// Apparently, some publishers explicitly specify the audio file as an image
|
||||
validTypeImage = true
|
||||
mimeType = "image/*"
|
||||
}
|
||||
else -> {
|
||||
when {
|
||||
isMediaFile(mimeType) -> {
|
||||
validTypeMedia = true
|
||||
}
|
||||
isImageFile(mimeType) -> {
|
||||
validTypeImage = true
|
||||
}
|
||||
isMediaFile(mimeType) -> validTypeMedia = true
|
||||
isImageFile(mimeType) -> validTypeImage = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -75,9 +70,8 @@ class Media : Namespace() {
|
|||
}
|
||||
Log.d(TAG, "handleElementStart creating media: ${state.currentItem?.title} $url $size $mimeType")
|
||||
val media = FeedMedia(state.currentItem, url, size, mimeType)
|
||||
if (durationMs > 0) {
|
||||
media.setDuration( durationMs)
|
||||
}
|
||||
if (durationMs > 0) media.setDuration( durationMs)
|
||||
|
||||
state.currentItem!!.media = media
|
||||
}
|
||||
state.currentItem != null && url != null && validTypeImage -> {
|
||||
|
@ -89,14 +83,8 @@ class Media : Namespace() {
|
|||
val url: String? = attributes.getValue(IMAGE_URL)
|
||||
if (url != null) {
|
||||
when {
|
||||
state.currentItem != null -> {
|
||||
state.currentItem!!.imageUrl = url
|
||||
}
|
||||
else -> {
|
||||
if (state.feed.imageUrl == null) {
|
||||
state.feed.imageUrl = url
|
||||
}
|
||||
}
|
||||
state.currentItem != null -> state.currentItem!!.imageUrl = url
|
||||
else -> if (state.feed.imageUrl == null) state.feed.imageUrl = url
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -17,22 +17,17 @@ class PodcastIndex : Namespace() {
|
|||
}
|
||||
CHAPTERS == localName -> {
|
||||
val href: String? = attributes.getValue(URL)
|
||||
if (state.currentItem != null && !href.isNullOrEmpty()) {
|
||||
state.currentItem!!.podcastIndexChapterUrl = href
|
||||
}
|
||||
if (state.currentItem != null && !href.isNullOrEmpty()) state.currentItem!!.podcastIndexChapterUrl = href
|
||||
}
|
||||
}
|
||||
return SyndElement(localName, this)
|
||||
}
|
||||
|
||||
override fun handleElementEnd(localName: String, state: HandlerState) {
|
||||
if (state.contentBuf == null) {
|
||||
return
|
||||
}
|
||||
if (state.contentBuf == null) return
|
||||
|
||||
val content = state.contentBuf.toString()
|
||||
if (FUNDING == localName && state.currentFunding != null && content.isNotEmpty()) {
|
||||
state.currentFunding!!.setContent(content)
|
||||
}
|
||||
if (FUNDING == localName && state.currentFunding != null && content.isNotEmpty()) state.currentFunding!!.setContent(content)
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
|
|
@ -32,10 +32,8 @@ class Rss20 : Namespace() {
|
|||
var size: Long = 0
|
||||
try {
|
||||
size = attributes.getValue(ENC_LEN)?.toLong() ?: 0
|
||||
if (size < 16384) {
|
||||
// less than 16kb is suspicious, check manually
|
||||
size = 0
|
||||
}
|
||||
if (size < 16384) size = 0
|
||||
} catch (e: NumberFormatException) {
|
||||
Log.d(TAG, "Length attribute could not be parsed.")
|
||||
}
|
||||
|
@ -54,9 +52,7 @@ class Rss20 : Namespace() {
|
|||
val currentItem = state.currentItem!!
|
||||
// the title tag is optional in RSS 2.0. The description is used
|
||||
// as a title if the item has no title-tag.
|
||||
if (currentItem.title == null) {
|
||||
currentItem.title = currentItem.description
|
||||
}
|
||||
if (currentItem.title == null) currentItem.title = currentItem.description
|
||||
|
||||
if (state.tempObjects.containsKey(Itunes.DURATION)) {
|
||||
if (currentItem.hasMedia()) {
|
||||
|
@ -77,59 +73,38 @@ class Rss20 : Namespace() {
|
|||
val secondElement = state.secondTag
|
||||
val second = secondElement.name
|
||||
var third: String? = null
|
||||
if (state.tagstack.size >= 3) {
|
||||
third = state.thirdTag.name
|
||||
}
|
||||
if (state.tagstack.size >= 3) third = state.thirdTag.name
|
||||
|
||||
when {
|
||||
GUID == top && ITEM == second -> {
|
||||
// some feed creators include an empty or non-standard guid-element in their feed,
|
||||
// which should be ignored
|
||||
if (contentRaw.isNotEmpty() && state.currentItem != null) {
|
||||
state.currentItem!!.itemIdentifier = contentRaw
|
||||
}
|
||||
if (contentRaw.isNotEmpty() && state.currentItem != null) state.currentItem!!.itemIdentifier = contentRaw
|
||||
}
|
||||
TITLE == top -> {
|
||||
when {
|
||||
ITEM == second && state.currentItem != null -> {
|
||||
state.currentItem!!.title = contentFromHtml
|
||||
}
|
||||
CHANNEL == second -> {
|
||||
state.feed.title = contentFromHtml
|
||||
}
|
||||
ITEM == second && state.currentItem != null -> state.currentItem!!.title = contentFromHtml
|
||||
CHANNEL == second -> state.feed.title = contentFromHtml
|
||||
}
|
||||
}
|
||||
LINK == top -> {
|
||||
when {
|
||||
CHANNEL == second -> {
|
||||
state.feed.link = content
|
||||
}
|
||||
ITEM == second && state.currentItem != null -> {
|
||||
state.currentItem!!.link = content
|
||||
CHANNEL == second -> state.feed.link = content
|
||||
ITEM == second && state.currentItem != null -> state.currentItem!!.link = content
|
||||
}
|
||||
}
|
||||
}
|
||||
PUBDATE == top && ITEM == second && state.currentItem != null -> {
|
||||
state.currentItem!!.pubDate = parseOrNullIfFuture(content)
|
||||
}
|
||||
PUBDATE == top && ITEM == second && state.currentItem != null -> state.currentItem!!.pubDate = parseOrNullIfFuture(content)
|
||||
URL == top && IMAGE == second && CHANNEL == third -> {
|
||||
// prefer itunes:image
|
||||
if (state.feed.imageUrl == null) {
|
||||
state.feed.imageUrl = content
|
||||
}
|
||||
if (state.feed.imageUrl == null) state.feed.imageUrl = content
|
||||
}
|
||||
DESCR == localName -> {
|
||||
when {
|
||||
CHANNEL == second -> {
|
||||
state.feed.description = contentFromHtml
|
||||
}
|
||||
ITEM == second && state.currentItem != null -> {
|
||||
state.currentItem!!.setDescriptionIfLonger(content) // fromHtml here breaks \n when not html
|
||||
CHANNEL == second -> state.feed.description = contentFromHtml
|
||||
ITEM == second && state.currentItem != null -> state.currentItem!!.setDescriptionIfLonger(content) // fromHtml here breaks \n when not html
|
||||
}
|
||||
}
|
||||
}
|
||||
LANGUAGE == localName -> {
|
||||
state.feed.language = content.lowercase()
|
||||
}
|
||||
LANGUAGE == localName -> state.feed.language = content.lowercase()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -12,9 +12,7 @@ class SimpleChapters : Namespace() {
|
|||
val currentItem = state.currentItem
|
||||
if (currentItem != null) {
|
||||
when {
|
||||
localName == CHAPTERS -> {
|
||||
currentItem.chapters = mutableListOf()
|
||||
}
|
||||
localName == CHAPTERS -> currentItem.chapters = mutableListOf()
|
||||
localName == CHAPTER && !attributes.getValue(START).isNullOrEmpty() -> {
|
||||
// if the chapter's START is empty, we don't need to do anything
|
||||
try {
|
||||
|
|
|
@ -18,9 +18,7 @@ class YouTube : Namespace() {
|
|||
} else {
|
||||
// this is the feed image
|
||||
// prefer to all other images
|
||||
if (!url.isNullOrEmpty()) {
|
||||
state.feed.imageUrl = url
|
||||
}
|
||||
if (!url.isNullOrEmpty()) state.feed.imageUrl = url
|
||||
}
|
||||
}
|
||||
return SyndElement(localName, this)
|
||||
|
@ -28,20 +26,14 @@ class YouTube : Namespace() {
|
|||
|
||||
override fun handleElementEnd(localName: String, state: HandlerState) {
|
||||
Log.d(TAG, "handleElementEnd $localName")
|
||||
if (state.contentBuf == null) {
|
||||
return
|
||||
}
|
||||
if (state.contentBuf == null) return
|
||||
|
||||
val content = state.contentBuf.toString()
|
||||
val contentFromHtml = HtmlCompat.fromHtml(content, HtmlCompat.FROM_HTML_MODE_COMPACT).toString()
|
||||
if (content.isEmpty()) {
|
||||
return
|
||||
}
|
||||
if (content.isEmpty()) return
|
||||
|
||||
when {
|
||||
AUTHOR == localName && state.tagstack.size <= 3 -> {
|
||||
state.feed.author = contentFromHtml
|
||||
}
|
||||
AUTHOR == localName && state.tagstack.size <= 3 -> state.feed.author = contentFromHtml
|
||||
DURATION == localName -> {
|
||||
try {
|
||||
val durationMs = inMillis(content)
|
||||
|
@ -52,27 +44,17 @@ class YouTube : Namespace() {
|
|||
}
|
||||
SUBTITLE == localName -> {
|
||||
when {
|
||||
state.currentItem != null && state.currentItem?.description.isNullOrEmpty() -> {
|
||||
state.currentItem!!.setDescriptionIfLonger(content)
|
||||
}
|
||||
state.feed.description.isNullOrEmpty() -> {
|
||||
state.feed.description = content
|
||||
}
|
||||
state.currentItem != null && state.currentItem?.description.isNullOrEmpty() -> state.currentItem!!.setDescriptionIfLonger(content)
|
||||
state.feed.description.isNullOrEmpty() -> state.feed.description = content
|
||||
}
|
||||
}
|
||||
SUMMARY == localName -> {
|
||||
when {
|
||||
state.currentItem != null -> {
|
||||
state.currentItem!!.setDescriptionIfLonger(content)
|
||||
}
|
||||
Rss20.CHANNEL == state.secondTag.name -> {
|
||||
state.feed.description = content
|
||||
state.currentItem != null -> state.currentItem!!.setDescriptionIfLonger(content)
|
||||
Rss20.CHANNEL == state.secondTag.name -> state.feed.description = content
|
||||
}
|
||||
}
|
||||
}
|
||||
NEW_FEED_URL == localName && content.trim { it <= ' ' }.startsWith("http") -> {
|
||||
state.redirectUrl = content.trim { it <= ' ' }
|
||||
}
|
||||
NEW_FEED_URL == localName && content.trim { it <= ' ' }.startsWith("http") -> state.redirectUrl = content.trim { it <= ' ' }
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -38,20 +38,13 @@ object DateUtils {
|
|||
// even more precise than microseconds: discard further decimal places
|
||||
when {
|
||||
current - start > 4 -> {
|
||||
date = if (current < date.length - 1) {
|
||||
date.substring(0, start + 4) + date.substring(current)
|
||||
} else {
|
||||
date.substring(0, start + 4)
|
||||
}
|
||||
date = if (current < date.length - 1) date.substring(0, start + 4) + date.substring(current) else date.substring(0, start + 4)
|
||||
// less than 4 decimal places: pad to have a consistent format for the parser
|
||||
}
|
||||
current - start < 4 -> {
|
||||
date = if (current < date.length - 1) {
|
||||
(date.substring(0, current) + StringUtils.repeat("0", 4 - (current - start))
|
||||
+ date.substring(current))
|
||||
} else {
|
||||
date.substring(0, current) + StringUtils.repeat("0", 4 - (current - start))
|
||||
}
|
||||
date = if (current < date.length - 1)
|
||||
(date.substring(0, current) + StringUtils.repeat("0", 4 - (current - start)) + date.substring(current))
|
||||
else date.substring(0, current) + StringUtils.repeat("0", 4 - (current - start))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -96,18 +89,14 @@ object DateUtils {
|
|||
pos.index = 0
|
||||
try {
|
||||
val result = parser.parse(date, pos)
|
||||
if (result != null && pos.index == date.length) {
|
||||
return result
|
||||
}
|
||||
if (result != null && pos.index == date.length) return result
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, Log.getStackTraceString(e))
|
||||
}
|
||||
}
|
||||
|
||||
// if date string starts with a weekday, try parsing date string without it
|
||||
if (date.matches("^\\w+, .*$".toRegex())) {
|
||||
return parse(date.substring(date.indexOf(',') + 1))
|
||||
}
|
||||
if (date.matches("^\\w+, .*$".toRegex())) return parse(date.substring(date.indexOf(',') + 1))
|
||||
|
||||
Log.d(TAG, "Could not parse date string \"$input\" [$date]")
|
||||
return null
|
||||
|
@ -120,9 +109,7 @@ object DateUtils {
|
|||
fun parseOrNullIfFuture(input: String?): Date? {
|
||||
val date = parse(input) ?: return null
|
||||
val now = Date()
|
||||
if (date.after(now)) {
|
||||
return null
|
||||
}
|
||||
if (date.after(now)) return null
|
||||
return date
|
||||
}
|
||||
|
||||
|
|
|
@ -9,25 +9,15 @@ object DurationParser {
|
|||
val parts = durationStr.trim { it <= ' ' }.split(":".toRegex()).dropLastWhile { it.isEmpty() }.toTypedArray()
|
||||
|
||||
return when (parts.size) {
|
||||
1 -> {
|
||||
toMillis(parts[0])
|
||||
}
|
||||
2 -> {
|
||||
toMillis("0", parts[0], parts[1])
|
||||
}
|
||||
3 -> {
|
||||
toMillis(parts[0], parts[1], parts[2])
|
||||
}
|
||||
else -> {
|
||||
throw NumberFormatException()
|
||||
}
|
||||
1 -> toMillis(parts[0])
|
||||
2 -> toMillis("0", parts[0], parts[1])
|
||||
3 -> toMillis(parts[0], parts[1], parts[2])
|
||||
else -> throw NumberFormatException()
|
||||
}
|
||||
}
|
||||
|
||||
private fun toMillis(hours: String, minutes: String, seconds: String): Long {
|
||||
return (TimeUnit.HOURS.toMillis(hours.toLong())
|
||||
+ TimeUnit.MINUTES.toMillis(minutes.toLong())
|
||||
+ toMillis(seconds))
|
||||
return (TimeUnit.HOURS.toMillis(hours.toLong()) + TimeUnit.MINUTES.toMillis(minutes.toLong()) + toMillis(seconds))
|
||||
}
|
||||
|
||||
private fun toMillis(seconds: String): Long {
|
||||
|
|
|
@ -21,24 +21,19 @@ object MimeTypeUtils {
|
|||
|
||||
@JvmStatic
|
||||
fun getMimeType(type: String?, filename: String?): String? {
|
||||
if (isMediaFile(type) && OCTET_STREAM != type) {
|
||||
return type
|
||||
}
|
||||
if (isMediaFile(type) && OCTET_STREAM != type) return type
|
||||
|
||||
val filenameType = getMimeTypeFromUrl(filename)
|
||||
if (isMediaFile(filenameType)) {
|
||||
return filenameType
|
||||
}
|
||||
if (isMediaFile(filenameType)) return filenameType
|
||||
|
||||
return type
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun isMediaFile(type: String?): Boolean {
|
||||
return if (type == null) {
|
||||
false
|
||||
} else {
|
||||
type.startsWith("audio/") || type.startsWith("video/") ||
|
||||
type == "application/ogg" || type == "application/octet-stream" || type == "application/x-shockwave-flash"
|
||||
}
|
||||
return if (type == null) false
|
||||
else type.startsWith("audio/") || type.startsWith("video/") || type == "application/ogg"
|
||||
|| type == "application/octet-stream" || type == "application/x-shockwave-flash"
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
|
@ -51,23 +46,16 @@ object MimeTypeUtils {
|
|||
* method will return the mime-type of the file extension.
|
||||
*/
|
||||
private fun getMimeTypeFromUrl(url: String?): String? {
|
||||
if (url == null) {
|
||||
return null
|
||||
}
|
||||
if (url == null) return null
|
||||
|
||||
val extension = FilenameUtils.getExtension(url)
|
||||
val mapResult = MimeTypeMap.getSingleton().getMimeTypeFromExtension(extension)
|
||||
if (mapResult != null) {
|
||||
return mapResult
|
||||
}
|
||||
if (mapResult != null) return mapResult
|
||||
|
||||
when {
|
||||
AUDIO_FILE_EXTENSIONS.contains(extension) -> {
|
||||
return "audio/*"
|
||||
}
|
||||
VIDEO_FILE_EXTENSIONS.contains(extension) -> {
|
||||
return "video/*"
|
||||
}
|
||||
else -> return null
|
||||
return when {
|
||||
AUDIO_FILE_EXTENSIONS.contains(extension) -> "audio/*"
|
||||
VIDEO_FILE_EXTENSIONS.contains(extension) -> "video/*"
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -39,9 +39,7 @@ class TypeGetter {
|
|||
Log.d(TAG, "Recognized type Atom")
|
||||
|
||||
val strLang = xpp.getAttributeValue("http://www.w3.org/XML/1998/namespace", "lang")
|
||||
if (strLang != null) {
|
||||
feed.language = strLang
|
||||
}
|
||||
if (strLang != null) feed.language = strLang
|
||||
|
||||
return Type.ATOM
|
||||
}
|
||||
|
@ -62,9 +60,7 @@ class TypeGetter {
|
|||
Log.d(TAG, "Recognized type RSS 0.91/0.92")
|
||||
return Type.RSS091
|
||||
}
|
||||
else -> {
|
||||
throw UnsupportedFeedtypeException("Unsupported rss version")
|
||||
}
|
||||
else -> throw UnsupportedFeedtypeException("Unsupported rss version")
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
|
|
|
@ -14,11 +14,8 @@ object ImageResourceUtils {
|
|||
*/
|
||||
@JvmStatic
|
||||
fun getEpisodeListImageLocation(playable: Playable): String? {
|
||||
return if (UserPreferences.useEpisodeCoverSetting) {
|
||||
playable.getImageLocation()
|
||||
} else {
|
||||
getFallbackImageLocation(playable)
|
||||
}
|
||||
return if (UserPreferences.useEpisodeCoverSetting) playable.getImageLocation()
|
||||
else getFallbackImageLocation(playable)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -26,33 +23,20 @@ object ImageResourceUtils {
|
|||
*/
|
||||
@JvmStatic
|
||||
fun getEpisodeListImageLocation(feedItem: FeedItem): String? {
|
||||
return if (UserPreferences.useEpisodeCoverSetting) {
|
||||
feedItem.imageLocation
|
||||
} else {
|
||||
getFallbackImageLocation(feedItem)
|
||||
}
|
||||
return if (UserPreferences.useEpisodeCoverSetting) feedItem.imageLocation
|
||||
else getFallbackImageLocation(feedItem)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun getFallbackImageLocation(playable: Playable): String? {
|
||||
if (playable is FeedMedia) {
|
||||
val item = playable.item
|
||||
return if (item?.feed != null) {
|
||||
item.feed!!.imageUrl
|
||||
} else {
|
||||
null
|
||||
}
|
||||
} else {
|
||||
return playable.getImageLocation()
|
||||
}
|
||||
return item?.feed?.imageUrl
|
||||
} else return playable.getImageLocation()
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun getFallbackImageLocation(feedItem: FeedItem): String? {
|
||||
return if (feedItem.feed != null) {
|
||||
feedItem.feed!!.imageUrl
|
||||
} else {
|
||||
null
|
||||
}
|
||||
return feedItem.feed?.imageUrl
|
||||
}
|
||||
}
|
||||
|
|
|
@ -33,16 +33,12 @@ object PlaybackSpeedUtils {
|
|||
if (feed?.preferences != null) {
|
||||
playbackSpeed = feed.preferences!!.feedPlaybackSpeed
|
||||
Log.d(TAG, "using feed speed $playbackSpeed")
|
||||
} else {
|
||||
Log.d(TAG, "Can not get feed specific playback speed: $feed")
|
||||
}
|
||||
} else Log.d(TAG, "Can not get feed specific playback speed: $feed")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (mediaType != null && playbackSpeed == FeedPreferences.SPEED_USE_GLOBAL) {
|
||||
playbackSpeed = UserPreferences.getPlaybackSpeed(mediaType)
|
||||
}
|
||||
if (mediaType != null && playbackSpeed == FeedPreferences.SPEED_USE_GLOBAL) playbackSpeed = UserPreferences.getPlaybackSpeed(mediaType)
|
||||
|
||||
return playbackSpeed
|
||||
}
|
||||
|
|
|
@ -57,11 +57,7 @@ internal class ApOkHttpUrlLoader private constructor(private val client: OkHttpC
|
|||
}
|
||||
}
|
||||
|
||||
override fun buildLoadData(model: String,
|
||||
width: Int,
|
||||
height: Int,
|
||||
options: Options
|
||||
): ModelLoader.LoadData<InputStream> {
|
||||
override fun buildLoadData(model: String, width: Int, height: Int, options: Options): ModelLoader.LoadData<InputStream> {
|
||||
return ModelLoader.LoadData(ObjectKey(model), ResizingOkHttpStreamFetcher(client, GlideUrl(model)))
|
||||
}
|
||||
|
||||
|
|
|
@ -15,15 +15,11 @@ internal class AudioCoverFetcher(private val path: String, private val context:
|
|||
override fun loadData(priority: Priority, callback: DataFetcher.DataCallback<in InputStream?>) {
|
||||
try {
|
||||
MediaMetadataRetrieverCompat().use { retriever ->
|
||||
if (path.startsWith(ContentResolver.SCHEME_CONTENT)) {
|
||||
retriever.setDataSource(context, Uri.parse(path))
|
||||
} else {
|
||||
retriever.setDataSource(path)
|
||||
}
|
||||
if (path.startsWith(ContentResolver.SCHEME_CONTENT)) retriever.setDataSource(context, Uri.parse(path))
|
||||
else retriever.setDataSource(path)
|
||||
|
||||
val picture = retriever.embeddedPicture
|
||||
if (picture != null) {
|
||||
callback.onDataReady(ByteArrayInputStream(picture))
|
||||
}
|
||||
if (picture != null) callback.onDataReady(ByteArrayInputStream(picture))
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
callback.onLoadFailed(e)
|
||||
|
|
|
@ -29,11 +29,7 @@ class ChapterImageModelLoader : ModelLoader<EmbeddedChapterImage?, ByteBuffer?>
|
|||
}
|
||||
}
|
||||
|
||||
override fun buildLoadData(model: EmbeddedChapterImage,
|
||||
width: Int,
|
||||
height: Int,
|
||||
options: Options
|
||||
): ModelLoader.LoadData<ByteBuffer?> {
|
||||
override fun buildLoadData(model: EmbeddedChapterImage, width: Int, height: Int, options: Options): ModelLoader.LoadData<ByteBuffer?> {
|
||||
return ModelLoader.LoadData(ObjectKey(model), EmbeddedImageFetcher(model))
|
||||
}
|
||||
|
||||
|
@ -59,9 +55,8 @@ class ChapterImageModelLoader : ModelLoader<EmbeddedChapterImage?, ByteBuffer?>
|
|||
val url = image.media.getStreamUrl()
|
||||
if (url != null) httpReq.url(url)
|
||||
val response = getHttpClient()!!.newCall(httpReq.build()).execute()
|
||||
if (!response.isSuccessful || response.body == null) {
|
||||
throw IOException("Invalid response: " + response.code + " " + response.message)
|
||||
}
|
||||
if (!response.isSuccessful || response.body == null) throw IOException("Invalid response: " + response.code + " " + response.message)
|
||||
|
||||
callback.onDataReady(ByteBuffer.wrap(response.body!!.bytes()))
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
|
|
|
@ -12,11 +12,7 @@ import kotlin.math.max
|
|||
import kotlin.math.min
|
||||
|
||||
class FastBlurTransformation : BitmapTransformation() {
|
||||
override fun transform(pool: BitmapPool,
|
||||
source: Bitmap,
|
||||
outWidth: Int,
|
||||
outHeight: Int
|
||||
): Bitmap {
|
||||
override fun transform(pool: BitmapPool, source: Bitmap, outWidth: Int, outHeight: Int): Bitmap {
|
||||
val targetWidth = outWidth / 3
|
||||
val targetHeight = (1.0 * outHeight * targetWidth / outWidth).toInt()
|
||||
val resized = ThumbnailUtils.extractThumbnail(source, targetWidth, targetHeight)
|
||||
|
@ -76,9 +72,7 @@ class FastBlurTransformation : BitmapTransformation() {
|
|||
//
|
||||
// Stack Blur Algorithm by Mario Klingemann <mario@quasimondo.com>
|
||||
|
||||
if (radius < 1) {
|
||||
return null
|
||||
}
|
||||
if (radius < 1) return null
|
||||
|
||||
val w = bitmap.width
|
||||
val h = bitmap.height
|
||||
|
@ -182,9 +176,8 @@ class FastBlurTransformation : BitmapTransformation() {
|
|||
goutsum -= sir[1]
|
||||
boutsum -= sir[2]
|
||||
|
||||
if (y == 0) {
|
||||
vmin[x] = min((x + radius + 1).toDouble(), wm.toDouble()).toInt()
|
||||
}
|
||||
if (y == 0) vmin[x] = min((x + radius + 1).toDouble(), wm.toDouble()).toInt()
|
||||
|
||||
p = pix[yw + vmin[x]]
|
||||
|
||||
sir[0] = (p and 0xff0000) shr 16
|
||||
|
@ -254,9 +247,8 @@ class FastBlurTransformation : BitmapTransformation() {
|
|||
boutsum += sir[2]
|
||||
}
|
||||
|
||||
if (i < hm) {
|
||||
yp += w
|
||||
}
|
||||
if (i < hm) yp += w
|
||||
|
||||
i++
|
||||
}
|
||||
yi = x
|
||||
|
@ -277,9 +269,8 @@ class FastBlurTransformation : BitmapTransformation() {
|
|||
goutsum -= sir[1]
|
||||
boutsum -= sir[2]
|
||||
|
||||
if (x == 0) {
|
||||
vmin[y] = (min((y + r1).toDouble(), hm.toDouble()) * w).toInt()
|
||||
}
|
||||
if (x == 0) vmin[y] = (min((y + r1).toDouble(), hm.toDouble()) * w).toInt()
|
||||
|
||||
p = x + vmin[y]
|
||||
|
||||
sir[0] = r[p]
|
||||
|
|
|
@ -26,11 +26,7 @@ class GenerativePlaceholderImageModelLoader : ModelLoader<String, InputStream> {
|
|||
}
|
||||
}
|
||||
|
||||
override fun buildLoadData(model: String,
|
||||
width: Int,
|
||||
height: Int,
|
||||
options: Options
|
||||
): ModelLoader.LoadData<InputStream?> {
|
||||
override fun buildLoadData(model: String, width: Int, height: Int, options: Options): ModelLoader.LoadData<InputStream?> {
|
||||
return ModelLoader.LoadData(ObjectKey(model), EmbeddedImageFetcher(model, width, height))
|
||||
}
|
||||
|
||||
|
@ -38,8 +34,8 @@ class GenerativePlaceholderImageModelLoader : ModelLoader<String, InputStream> {
|
|||
return model.startsWith(Feed.PREFIX_GENERATIVE_COVER)
|
||||
}
|
||||
|
||||
internal class EmbeddedImageFetcher(private val model: String, private val width: Int, private val height: Int) :
|
||||
DataFetcher<InputStream?> {
|
||||
internal class EmbeddedImageFetcher(private val model: String, private val width: Int, private val height: Int) : DataFetcher<InputStream?> {
|
||||
|
||||
override fun loadData(priority: Priority, callback: DataFetcher.DataCallback<in InputStream?>) {
|
||||
val bitmap = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
|
||||
val canvas = Canvas(bitmap)
|
||||
|
@ -70,22 +66,14 @@ class GenerativePlaceholderImageModelLoader : ModelLoader<String, InputStream> {
|
|||
}
|
||||
color = newColor
|
||||
paint.color = newColor
|
||||
canvas.drawLine(linePos + slope + shadowWidth, -slope.toFloat(),
|
||||
linePos - slope + shadowWidth, (height + slope).toFloat(), paintShadow)
|
||||
canvas.drawLine(linePos + slope + shadowWidth, -slope.toFloat(), linePos - slope + shadowWidth, (height + slope).toFloat(), paintShadow)
|
||||
}
|
||||
canvas.drawLine(linePos + slope, -slope.toFloat(),
|
||||
linePos - slope, (height + slope).toFloat(), paint)
|
||||
canvas.drawLine(linePos + slope, -slope.toFloat(), linePos - slope, (height + slope).toFloat(), paint)
|
||||
}
|
||||
|
||||
val gradientPaint = Paint()
|
||||
paint.isDither = true
|
||||
gradientPaint.setShader(LinearGradient(0f,
|
||||
0f,
|
||||
0f,
|
||||
height.toFloat(),
|
||||
0x00000000,
|
||||
0x55000000,
|
||||
Shader.TileMode.CLAMP))
|
||||
gradientPaint.setShader(LinearGradient(0f, 0f, 0f, height.toFloat(), 0x00000000, 0x55000000, Shader.TileMode.CLAMP))
|
||||
canvas.drawRect(0f, 0f, width.toFloat(), height.toFloat(), gradientPaint)
|
||||
|
||||
val baos = ByteArrayOutputStream()
|
||||
|
@ -111,8 +99,7 @@ class GenerativePlaceholderImageModelLoader : ModelLoader<String, InputStream> {
|
|||
}
|
||||
|
||||
companion object {
|
||||
private val PALETTES = intArrayOf(-0x876f64, -0x9100, -0xc771c4,
|
||||
-0xff7c71, -0x84e05e, -0x48e3e4, -0xde690d)
|
||||
private val PALETTES = intArrayOf(-0x876f64, -0x9100, -0xc771c4, -0xff7c71, -0x84e05e, -0x48e3e4, -0xde690d)
|
||||
|
||||
private fun randomShadeOfGrey(generator: Random): Int {
|
||||
return -0x888889 + 0x222222 * generator.nextInt(5)
|
||||
|
|
|
@ -9,8 +9,7 @@ import com.bumptech.glide.signature.ObjectKey
|
|||
import ac.mdiq.podcini.storage.model.feed.FeedMedia
|
||||
import java.io.InputStream
|
||||
|
||||
internal class MetadataRetrieverLoader private constructor(private val context: Context) :
|
||||
ModelLoader<String, InputStream> {
|
||||
internal class MetadataRetrieverLoader private constructor(private val context: Context) : ModelLoader<String, InputStream> {
|
||||
/**
|
||||
* The default factory for [MetadataRetrieverLoader]s.
|
||||
*/
|
||||
|
@ -24,11 +23,8 @@ internal class MetadataRetrieverLoader private constructor(private val context:
|
|||
}
|
||||
}
|
||||
|
||||
override fun buildLoadData(model: String,
|
||||
width: Int, height: Int, options: Options
|
||||
): ModelLoader.LoadData<InputStream?> {
|
||||
return ModelLoader.LoadData(ObjectKey(model),
|
||||
AudioCoverFetcher(model.replace(FeedMedia.FILENAME_PREFIX_EMBEDDED_COVER, ""), context))
|
||||
override fun buildLoadData(model: String, width: Int, height: Int, options: Options): ModelLoader.LoadData<InputStream?> {
|
||||
return ModelLoader.LoadData(ObjectKey(model), AudioCoverFetcher(model.replace(FeedMedia.FILENAME_PREFIX_EMBEDDED_COVER, ""), context))
|
||||
}
|
||||
|
||||
override fun handles(model: String): Boolean {
|
||||
|
|
|
@ -54,9 +54,7 @@ class ResizingOkHttpStreamFetcher(client: Call.Factory?, url: GlideUrl?) : OkHtt
|
|||
IOUtils.closeQuietly(inVal)
|
||||
|
||||
when {
|
||||
options.outWidth == -1 || options.outHeight == -1 -> {
|
||||
throw IOException("Not a valid image")
|
||||
}
|
||||
options.outWidth == -1 || options.outHeight == -1 -> throw IOException("Not a valid image")
|
||||
max(options.outHeight.toDouble(), options.outWidth.toDouble()) >= MAX_DIMENSIONS -> {
|
||||
val sampleSize = max(options.outHeight.toDouble(), options.outWidth.toDouble()) / MAX_DIMENSIONS
|
||||
options.inSampleSize = 2.0.pow(floor(ln(sampleSize) / ln(2.0))).toInt()
|
||||
|
@ -68,8 +66,7 @@ class ResizingOkHttpStreamFetcher(client: Call.Factory?, url: GlideUrl?) : OkHtt
|
|||
val bitmap = BitmapFactory.decodeStream(inVal, null, options)
|
||||
IOUtils.closeQuietly(inVal)
|
||||
|
||||
val format = if (Build.VERSION.SDK_INT < 30
|
||||
) CompressFormat.WEBP else CompressFormat.WEBP_LOSSY
|
||||
val format = if (Build.VERSION.SDK_INT < 30) CompressFormat.WEBP else CompressFormat.WEBP_LOSSY
|
||||
|
||||
var quality = 100
|
||||
if (tempOut != null) while (true) {
|
||||
|
@ -78,29 +75,19 @@ class ResizingOkHttpStreamFetcher(client: Call.Factory?, url: GlideUrl?) : OkHtt
|
|||
IOUtils.closeQuietly(out)
|
||||
|
||||
quality -= when {
|
||||
tempOut!!.length() > 3 * MAX_FILE_SIZE && quality >= 45 -> {
|
||||
40
|
||||
}
|
||||
tempOut!!.length() > 2 * MAX_FILE_SIZE && quality >= 25 -> {
|
||||
20
|
||||
}
|
||||
tempOut!!.length() > MAX_FILE_SIZE && quality >= 15 -> {
|
||||
10
|
||||
}
|
||||
tempOut!!.length() > MAX_FILE_SIZE && quality >= 10 -> {
|
||||
5
|
||||
}
|
||||
else -> {
|
||||
break
|
||||
}
|
||||
tempOut!!.length() > 3 * MAX_FILE_SIZE && quality >= 45 -> 40
|
||||
tempOut!!.length() > 2 * MAX_FILE_SIZE && quality >= 25 -> 20
|
||||
tempOut!!.length() > MAX_FILE_SIZE && quality >= 15 -> 10
|
||||
tempOut!!.length() > MAX_FILE_SIZE && quality >= 10 -> 5
|
||||
else -> break
|
||||
}
|
||||
}
|
||||
bitmap?.recycle()
|
||||
|
||||
stream = FileInputStream(tempOut)
|
||||
callback.onDataReady(stream)
|
||||
if (tempIn != null && tempOut != null) Log.d(TAG,
|
||||
"Compressed image from " + tempIn!!.length() / 1024 + " to " + tempOut!!.length() / 1024 + " kB (quality: " + quality + "%)")
|
||||
if (tempIn != null && tempOut != null)
|
||||
Log.d(TAG, "Compressed image from ${tempIn!!.length() / 1024} to ${tempOut!!.length() / 1024} kB (quality: $quality%)")
|
||||
} catch (e: Throwable) {
|
||||
e.printStackTrace()
|
||||
|
||||
|
|
|
@ -61,9 +61,7 @@ object UrlChecker {
|
|||
Log.d(TAG, "Adding http:// at the beginning of the URL")
|
||||
return "http://$url"
|
||||
}
|
||||
else -> {
|
||||
return url
|
||||
}
|
||||
else -> return url
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -84,18 +82,12 @@ object UrlChecker {
|
|||
base = prepareUrl(base)
|
||||
val urlUri = Uri.parse(url)
|
||||
val baseUri = Uri.parse(base)
|
||||
return if (urlUri.isRelative && baseUri.isAbsolute) {
|
||||
urlUri.buildUpon().scheme(baseUri.scheme).build().toString()
|
||||
} else {
|
||||
prepareUrl(url)
|
||||
}
|
||||
return if (urlUri.isRelative && baseUri.isAbsolute) urlUri.buildUpon().scheme(baseUri.scheme).build().toString() else prepareUrl(url)
|
||||
}
|
||||
|
||||
fun containsUrl(list: List<String?>, url: String?): Boolean {
|
||||
for (item in list) {
|
||||
if (urlEquals(item, url)) {
|
||||
return true
|
||||
}
|
||||
if (urlEquals(item, url)) return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
@ -104,17 +96,14 @@ object UrlChecker {
|
|||
fun urlEquals(string1: String?, string2: String?): Boolean {
|
||||
val url1 = string1!!.toHttpUrlOrNull()
|
||||
val url2 = string2!!.toHttpUrlOrNull()
|
||||
if (url1!!.host != url2!!.host) {
|
||||
return false
|
||||
}
|
||||
if (url1!!.host != url2!!.host) return false
|
||||
|
||||
val pathSegments1 = normalizePathSegments(url1.pathSegments)
|
||||
val pathSegments2 = normalizePathSegments(url2.pathSegments)
|
||||
if (pathSegments1 != pathSegments2) {
|
||||
return false
|
||||
}
|
||||
if (url1.query.isNullOrEmpty()) {
|
||||
return url2.query.isNullOrEmpty()
|
||||
}
|
||||
if (pathSegments1 != pathSegments2) return false
|
||||
|
||||
if (url1.query.isNullOrEmpty()) return url2.query.isNullOrEmpty()
|
||||
|
||||
return url1.query == url2.query
|
||||
}
|
||||
|
||||
|
@ -126,9 +115,7 @@ object UrlChecker {
|
|||
private fun normalizePathSegments(input: List<String>): List<String> {
|
||||
val result: MutableList<String> = ArrayList()
|
||||
for (string in input) {
|
||||
if (string.isNotEmpty()) {
|
||||
result.add(string.lowercase())
|
||||
}
|
||||
if (string.isNotEmpty()) result.add(string.lowercase())
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
|
|
@ -58,9 +58,8 @@ class CombinedSearcher : PodcastSearcher {
|
|||
urlToResult[result!!.feedUrl] = result
|
||||
|
||||
var ranking = 0f
|
||||
if (resultRanking.containsKey(result.feedUrl)) {
|
||||
ranking = resultRanking[result.feedUrl]!!
|
||||
}
|
||||
if (resultRanking.containsKey(result.feedUrl)) ranking = resultRanking[result.feedUrl]!!
|
||||
|
||||
ranking += 1f / (position + 1f)
|
||||
resultRanking[result.feedUrl] = ranking * providerPriority
|
||||
}
|
||||
|
@ -92,9 +91,7 @@ class CombinedSearcher : PodcastSearcher {
|
|||
for (i in PodcastSearcherRegistry.searchProviders.indices) {
|
||||
val searchProviderInfo = PodcastSearcherRegistry.searchProviders[i]
|
||||
val searcher = searchProviderInfo.searcher
|
||||
if (searchProviderInfo.weight > 0.00001f && searcher.javaClass != CombinedSearcher::class.java) {
|
||||
names.add(searcher.name)
|
||||
}
|
||||
if (searchProviderInfo.weight > 0.00001f && searcher.javaClass != CombinedSearcher::class.java) names.add(searcher.name)
|
||||
}
|
||||
return TextUtils.join(", ", names)
|
||||
}
|
||||
|
|
|
@ -26,8 +26,7 @@ class ItunesPodcastSearcher : PodcastSearcher {
|
|||
val formattedUrl = String.format(ITUNES_API_URL, encodedQuery)
|
||||
|
||||
val client = getHttpClient()
|
||||
val httpReq: Builder = Builder()
|
||||
.url(formattedUrl)
|
||||
val httpReq: Builder = Builder().url(formattedUrl)
|
||||
val podcasts: MutableList<PodcastSearchResult?> = ArrayList()
|
||||
try {
|
||||
val response = client.newCall(httpReq.build()).execute()
|
||||
|
@ -40,9 +39,7 @@ class ItunesPodcastSearcher : PodcastSearcher {
|
|||
for (i in 0 until j.length()) {
|
||||
val podcastJson = j.getJSONObject(i)
|
||||
val podcast = PodcastSearchResult.fromItunes(podcastJson)
|
||||
if (podcast.feedUrl != null) {
|
||||
podcasts.add(podcast)
|
||||
}
|
||||
if (podcast.feedUrl != null) podcasts.add(podcast)
|
||||
}
|
||||
} else {
|
||||
subscriber.onError(IOException(response.toString()))
|
||||
|
@ -80,9 +77,8 @@ class ItunesPodcastSearcher : PodcastSearcher {
|
|||
}
|
||||
val feedUrl = results.getString(feedUrlName)
|
||||
emitter.onSuccess(feedUrl)
|
||||
} else {
|
||||
emitter.onError(IOException(response.toString()))
|
||||
}
|
||||
} else emitter.onError(IOException(response.toString()))
|
||||
|
||||
} catch (e: IOException) {
|
||||
emitter.onError(e)
|
||||
} catch (e: JSONException) {
|
||||
|
|
|
@ -22,17 +22,13 @@ class ItunesTopListLoader(private val context: Context) {
|
|||
val client = getHttpClient()
|
||||
val feedString: String
|
||||
var loadCountry = country
|
||||
if (COUNTRY_CODE_UNSET == country) {
|
||||
loadCountry = Locale.getDefault().country
|
||||
}
|
||||
if (COUNTRY_CODE_UNSET == country) loadCountry = Locale.getDefault().country
|
||||
|
||||
feedString = try {
|
||||
getTopListFeed(client, loadCountry)
|
||||
} catch (e: IOException) {
|
||||
if (COUNTRY_CODE_UNSET == country) {
|
||||
getTopListFeed(client, "US")
|
||||
} else {
|
||||
throw e
|
||||
}
|
||||
if (COUNTRY_CODE_UNSET == country) getTopListFeed(client, "US")
|
||||
else throw e
|
||||
}
|
||||
return removeSubscribed(parseFeed(feedString), subscribed, limit)
|
||||
}
|
||||
|
@ -46,12 +42,9 @@ class ItunesTopListLoader(private val context: Context) {
|
|||
.url(String.format(url, country))
|
||||
|
||||
client!!.newCall(httpReq.build()).execute().use { response ->
|
||||
if (response.isSuccessful) {
|
||||
return response.body!!.string()
|
||||
}
|
||||
if (response.code == 400) {
|
||||
throw IOException("iTunes does not have data for the selected country.")
|
||||
}
|
||||
if (response.isSuccessful) return response.body!!.string()
|
||||
if (response.code == 400) throw IOException("iTunes does not have data for the selected country.")
|
||||
|
||||
val prefix = context.getString(R.string.error_msg_prefix)
|
||||
throw IOException(prefix + response)
|
||||
}
|
||||
|
@ -87,9 +80,7 @@ class ItunesTopListLoader(private val context: Context) {
|
|||
const val COUNTRY_CODE_UNSET: String = "99"
|
||||
private const val NUM_LOADED = 25
|
||||
|
||||
private fun removeSubscribed(
|
||||
suggestedPodcasts: List<PodcastSearchResult>, subscribedFeeds: List<Feed>, limit: Int
|
||||
): List<PodcastSearchResult> {
|
||||
private fun removeSubscribed(suggestedPodcasts: List<PodcastSearchResult>, subscribedFeeds: List<Feed>, limit: Int): List<PodcastSearchResult> {
|
||||
val subscribedPodcastsSet: MutableSet<String> = HashSet()
|
||||
for (subscribedFeed in subscribedFeeds) {
|
||||
if (subscribedFeed.title != null && subscribedFeed.author != null) {
|
||||
|
@ -98,12 +89,8 @@ class ItunesTopListLoader(private val context: Context) {
|
|||
}
|
||||
val suggestedNotSubscribed: MutableList<PodcastSearchResult> = ArrayList()
|
||||
for (suggested in suggestedPodcasts) {
|
||||
if (!subscribedPodcastsSet.contains(suggested.title.trim { it <= ' ' })) {
|
||||
suggestedNotSubscribed.add(suggested)
|
||||
}
|
||||
if (suggestedNotSubscribed.size == limit) {
|
||||
return suggestedNotSubscribed
|
||||
}
|
||||
if (!subscribedPodcastsSet.contains(suggested.title.trim { it <= ' ' })) suggestedNotSubscribed.add(suggested)
|
||||
if (suggestedNotSubscribed.size == limit) return suggestedNotSubscribed
|
||||
}
|
||||
return suggestedNotSubscribed
|
||||
}
|
||||
|
|
|
@ -40,9 +40,7 @@ class PodcastIndexPodcastSearcher : PodcastSearcher {
|
|||
for (i in 0 until j.length()) {
|
||||
val podcastJson = j.getJSONObject(i)
|
||||
val podcast = PodcastSearchResult.fromPodcastIndex(podcastJson)
|
||||
if (podcast.feedUrl != null) {
|
||||
podcasts.add(podcast)
|
||||
}
|
||||
if (podcast.feedUrl != null) podcasts.add(podcast)
|
||||
}
|
||||
} else {
|
||||
subscriber.onError(IOException(response.toString()))
|
||||
|
|
|
@ -7,15 +7,8 @@ import org.json.JSONObject
|
|||
import java.text.SimpleDateFormat
|
||||
import java.util.*
|
||||
|
||||
class PodcastSearchResult private constructor(
|
||||
val title: String,
|
||||
val imageUrl: String?,
|
||||
val feedUrl: String?,
|
||||
val author: String?,
|
||||
val count: Int?,
|
||||
val update: String?,
|
||||
val source: String
|
||||
) {
|
||||
class PodcastSearchResult private constructor(val title: String, val imageUrl: String?, val feedUrl: String?,
|
||||
val author: String?, val count: Int?, val update: String?, val source: String) {
|
||||
companion object {
|
||||
fun dummy(): PodcastSearchResult {
|
||||
return PodcastSearchResult("", "", "", "", 0, "", "dummy")
|
||||
|
@ -50,13 +43,10 @@ class PodcastSearchResult private constructor(
|
|||
while (imageUrl == null && i < images.length()) {
|
||||
val image = images.getJSONObject(i)
|
||||
val height = image.getJSONObject("attributes").getString("height")
|
||||
if (height.toInt() >= 100) {
|
||||
imageUrl = image.getString("label")
|
||||
}
|
||||
if (height.toInt() >= 100) imageUrl = image.getString("label")
|
||||
i++
|
||||
}
|
||||
val feedUrl = "https://itunes.apple.com/lookup?id=" +
|
||||
json.getJSONObject("id").getJSONObject("attributes").getString("im:id")
|
||||
val feedUrl = "https://itunes.apple.com/lookup?id=" + json.getJSONObject("id").getJSONObject("attributes").getString("im:id")
|
||||
|
||||
var author: String? = null
|
||||
try {
|
||||
|
|
|
@ -20,22 +20,15 @@ object PodcastSearcherRegistry {
|
|||
|
||||
fun lookupUrl(url: String): Single<String> {
|
||||
for (searchProviderInfo in searchProviders) {
|
||||
if (searchProviderInfo.searcher.javaClass != CombinedSearcher::class.java
|
||||
&& searchProviderInfo.searcher.urlNeedsLookup(url)) {
|
||||
|
||||
if (searchProviderInfo.searcher.javaClass != CombinedSearcher::class.java && searchProviderInfo.searcher.urlNeedsLookup(url))
|
||||
return searchProviderInfo.searcher.lookupUrl(url)
|
||||
}
|
||||
}
|
||||
return Single.just(url)
|
||||
}
|
||||
|
||||
fun urlNeedsLookup(url: String): Boolean {
|
||||
for (searchProviderInfo in searchProviders) {
|
||||
if (searchProviderInfo.searcher.javaClass != CombinedSearcher::class.java
|
||||
&& searchProviderInfo.searcher.urlNeedsLookup(url)) {
|
||||
|
||||
return true
|
||||
}
|
||||
if (searchProviderInfo.searcher.javaClass != CombinedSearcher::class.java && searchProviderInfo.searcher.urlNeedsLookup(url)) return true
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
|
|
@ -22,15 +22,13 @@ class ConnectionStateMonitor
|
|||
}
|
||||
|
||||
fun enable(context: Context) {
|
||||
val connectivityManager =
|
||||
context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
|
||||
val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
|
||||
connectivityManager.registerNetworkCallback(networkRequest, this)
|
||||
connectivityManager.addDefaultNetworkActiveListener(this)
|
||||
}
|
||||
|
||||
fun disable(context: Context) {
|
||||
val connectivityManager =
|
||||
context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
|
||||
val connectivityManager = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
|
||||
connectivityManager.unregisterNetworkCallback(this)
|
||||
connectivityManager.removeDefaultNetworkActiveListener(this)
|
||||
}
|
||||
|
|
|
@ -39,8 +39,7 @@ object FeedUpdateManager {
|
|||
val workRequest: PeriodicWorkRequest = PeriodicWorkRequest.Builder(
|
||||
FeedUpdateWorker::class.java, UserPreferences.updateInterval, TimeUnit.HOURS)
|
||||
.setConstraints(Builder()
|
||||
.setRequiredNetworkType(if (UserPreferences.isAllowMobileFeedRefresh
|
||||
) NetworkType.CONNECTED else NetworkType.UNMETERED).build())
|
||||
.setRequiredNetworkType(if (UserPreferences.isAllowMobileFeedRefresh) NetworkType.CONNECTED else NetworkType.UNMETERED).build())
|
||||
.build()
|
||||
WorkManager.getInstance(context).enqueueUniquePeriodicWork(WORK_ID_FEED_UPDATE,
|
||||
if (replace) ExistingPeriodicWorkPolicy.REPLACE else ExistingPeriodicWorkPolicy.KEEP, workRequest)
|
||||
|
@ -54,10 +53,8 @@ object FeedUpdateManager {
|
|||
.setInitialDelay(0L, TimeUnit.MILLISECONDS)
|
||||
.setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST)
|
||||
.addTag(WORK_TAG_FEED_UPDATE)
|
||||
if (feed == null || !feed.isLocalFeed) {
|
||||
workRequest.setConstraints(Builder()
|
||||
.setRequiredNetworkType(NetworkType.CONNECTED).build())
|
||||
}
|
||||
if (feed == null || !feed.isLocalFeed) workRequest.setConstraints(Builder().setRequiredNetworkType(NetworkType.CONNECTED).build())
|
||||
|
||||
val builder = Data.Builder()
|
||||
builder.putBoolean(EXTRA_EVEN_ON_MOBILE, true)
|
||||
if (feed != null) {
|
||||
|
@ -65,8 +62,7 @@ object FeedUpdateManager {
|
|||
builder.putBoolean(EXTRA_NEXT_PAGE, nextPage)
|
||||
}
|
||||
workRequest.setInputData(builder.build())
|
||||
WorkManager.getInstance(context).enqueueUniqueWork(WORK_ID_FEED_UPDATE_MANUAL,
|
||||
ExistingWorkPolicy.REPLACE, workRequest.build())
|
||||
WorkManager.getInstance(context).enqueueUniqueWork(WORK_ID_FEED_UPDATE_MANUAL, ExistingWorkPolicy.REPLACE, workRequest.build())
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
|
@ -74,36 +70,25 @@ object FeedUpdateManager {
|
|||
fun runOnceOrAsk(context: Context, feed: Feed? = null) {
|
||||
Log.d(TAG, "Run auto update immediately in background.")
|
||||
when {
|
||||
feed != null && feed.isLocalFeed -> {
|
||||
runOnce(context, feed)
|
||||
}
|
||||
!networkAvailable() -> {
|
||||
EventBus.getDefault().post(MessageEvent(context.getString(R.string.download_error_no_connection)))
|
||||
}
|
||||
isFeedRefreshAllowed -> {
|
||||
runOnce(context, feed)
|
||||
}
|
||||
else -> {
|
||||
confirmMobileRefresh(context, feed)
|
||||
}
|
||||
feed != null && feed.isLocalFeed -> runOnce(context, feed)
|
||||
!networkAvailable() -> EventBus.getDefault().post(MessageEvent(context.getString(R.string.download_error_no_connection)))
|
||||
isFeedRefreshAllowed -> runOnce(context, feed)
|
||||
else -> confirmMobileRefresh(context, feed)
|
||||
}
|
||||
}
|
||||
|
||||
private fun confirmMobileRefresh(context: Context, feed: Feed?) {
|
||||
val builder = MaterialAlertDialogBuilder(context)
|
||||
.setTitle(R.string.feed_refresh_title)
|
||||
.setPositiveButton(R.string.confirm_mobile_streaming_button_once
|
||||
) { _: DialogInterface?, _: Int -> runOnce(context, feed) }
|
||||
.setPositiveButton(R.string.confirm_mobile_streaming_button_once) { _: DialogInterface?, _: Int -> runOnce(context, feed) }
|
||||
.setNeutralButton(R.string.confirm_mobile_streaming_button_always) { _: DialogInterface?, _: Int ->
|
||||
UserPreferences.isAllowMobileFeedRefresh = true
|
||||
runOnce(context, feed)
|
||||
}
|
||||
.setNegativeButton(R.string.no, null)
|
||||
if (isNetworkRestricted && isVpnOverWifi) {
|
||||
builder.setMessage(R.string.confirm_mobile_feed_refresh_dialog_message_vpn)
|
||||
} else {
|
||||
builder.setMessage(R.string.confirm_mobile_feed_refresh_dialog_message)
|
||||
}
|
||||
if (isNetworkRestricted && isVpnOverWifi) builder.setMessage(R.string.confirm_mobile_feed_refresh_dialog_message_vpn)
|
||||
else builder.setMessage(R.string.confirm_mobile_feed_refresh_dialog_message)
|
||||
|
||||
builder.show()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -29,9 +29,7 @@ object MediaSizeLoader {
|
|||
when {
|
||||
media.isDownloaded() -> {
|
||||
val mediaFile = File(media.getLocalMediaUrl())
|
||||
if (mediaFile.exists()) {
|
||||
size = mediaFile.length()
|
||||
}
|
||||
if (mediaFile.exists()) size = mediaFile.length()
|
||||
}
|
||||
!media.checkedOnSizeButUnknown() -> {
|
||||
// only query the network if we haven't already checked
|
||||
|
@ -65,12 +63,10 @@ object MediaSizeLoader {
|
|||
}
|
||||
}
|
||||
Log.d(TAG, "new size: $size")
|
||||
if (size <= 0) {
|
||||
// they didn't tell us the size, but we don't want to keep querying on it
|
||||
media.setCheckedOnSizeButUnknown()
|
||||
} else {
|
||||
media.size = size
|
||||
}
|
||||
if (size <= 0) media.setCheckedOnSizeButUnknown()
|
||||
else media.size = size
|
||||
|
||||
emitter.onSuccess(size)
|
||||
DBWriter.setFeedMedia(media)
|
||||
})
|
||||
|
|
|
@ -21,9 +21,7 @@ class BasicAuthorizationInterceptor : Interceptor {
|
|||
|
||||
var response: Response = chain.proceed(request)
|
||||
|
||||
if (response.code != HttpURLConnection.HTTP_UNAUTHORIZED) {
|
||||
return response
|
||||
}
|
||||
if (response.code != HttpURLConnection.HTTP_UNAUTHORIZED) return response
|
||||
|
||||
val newRequest: Builder = request.newBuilder()
|
||||
if (!TextUtils.equals(response.request.url.toString(), request.url.toString())) {
|
||||
|
@ -43,13 +41,10 @@ class BasicAuthorizationInterceptor : Interceptor {
|
|||
val downloadRequest = request.tag() as? DownloadRequest
|
||||
if (downloadRequest?.source != null) {
|
||||
userInfo = URIUtil.getURIFromRequestUrl(downloadRequest.source!!).userInfo
|
||||
if (userInfo.isEmpty() && (!downloadRequest.username.isNullOrEmpty() || !downloadRequest.password.isNullOrEmpty())) {
|
||||
if (userInfo.isEmpty() && (!downloadRequest.username.isNullOrEmpty() || !downloadRequest.password.isNullOrEmpty()))
|
||||
userInfo = downloadRequest.username + ":" + downloadRequest.password
|
||||
}
|
||||
}
|
||||
} else {
|
||||
userInfo = DBReader.getImageAuthentication(request.url.toString())
|
||||
}
|
||||
} else userInfo = DBReader.getImageAuthentication(request.url.toString())
|
||||
|
||||
if (userInfo.isEmpty()) {
|
||||
Log.d(TAG, "no credentials for '" + request.url + "'")
|
||||
|
@ -67,9 +62,7 @@ class BasicAuthorizationInterceptor : Interceptor {
|
|||
newRequest.header(HEADER_AUTHORIZATION, encode(username, password, "ISO-8859-1"))
|
||||
response = chain.proceed(newRequest.build())
|
||||
|
||||
if (response.code != HttpURLConnection.HTTP_UNAUTHORIZED) {
|
||||
return response
|
||||
}
|
||||
if (response.code != HttpURLConnection.HTTP_UNAUTHORIZED) return response
|
||||
|
||||
Log.d(TAG, "Authorization failed, re-trying with UTF-8 encoded credentials")
|
||||
newRequest.header(HEADER_AUTHORIZATION, encode(username, password, "UTF-8"))
|
||||
|
|
|
@ -21,9 +21,8 @@ object DownloadRequestCreator {
|
|||
@JvmStatic
|
||||
fun create(feed: Feed): DownloadRequest.Builder {
|
||||
val dest = File(feedfilePath, getFeedfileName(feed))
|
||||
if (dest.exists()) {
|
||||
dest.delete()
|
||||
}
|
||||
if (dest.exists()) dest.delete()
|
||||
|
||||
Log.d(TAG, "Requesting download feed from url " + feed.download_url)
|
||||
|
||||
val username = feed.preferences?.username
|
||||
|
@ -39,15 +38,10 @@ object DownloadRequestCreator {
|
|||
val pdFile = if (media.file_url != null) File(media.file_url!!) else null
|
||||
val partiallyDownloadedFileExists = pdFile?.exists() ?: false
|
||||
var dest: File
|
||||
dest = if (partiallyDownloadedFileExists) {
|
||||
pdFile!!
|
||||
} else {
|
||||
File(getMediafilePath(media), getMediafilename(media))
|
||||
}
|
||||
dest = if (partiallyDownloadedFileExists) pdFile!! else File(getMediafilePath(media), getMediafilename(media))
|
||||
|
||||
if (dest.exists() && !partiallyDownloadedFileExists) dest = findUnusedFile(dest)!!
|
||||
|
||||
if (dest.exists() && !partiallyDownloadedFileExists) {
|
||||
dest = findUnusedFile(dest)!!
|
||||
}
|
||||
Log.d(TAG, "Requesting download media from url " + media.download_url)
|
||||
|
||||
val username = media.item?.feed?.preferences?.username
|
||||
|
@ -60,11 +54,7 @@ object DownloadRequestCreator {
|
|||
// find different name
|
||||
var newDest: File? = null
|
||||
for (i in 1 until Int.MAX_VALUE) {
|
||||
val newName = (FilenameUtils.getBaseName(dest.name)
|
||||
+ "-"
|
||||
+ i
|
||||
+ FilenameUtils.EXTENSION_SEPARATOR
|
||||
+ FilenameUtils.getExtension(dest.name))
|
||||
val newName = (FilenameUtils.getBaseName(dest.name) + "-" + i + FilenameUtils.EXTENSION_SEPARATOR + FilenameUtils.getExtension(dest.name))
|
||||
Log.d(TAG, "Testing filename $newName")
|
||||
newDest = File(dest.parent, newName)
|
||||
if (!newDest.exists()) {
|
||||
|
@ -80,17 +70,15 @@ object DownloadRequestCreator {
|
|||
|
||||
private fun getFeedfileName(feed: Feed): String {
|
||||
var filename = feed.download_url
|
||||
if (!feed.title.isNullOrEmpty()) {
|
||||
filename = feed.title
|
||||
}
|
||||
if (!feed.title.isNullOrEmpty()) filename = feed.title
|
||||
|
||||
if (filename == null) return ""
|
||||
return "feed-" + FileNameGenerator.generateFileName(filename) + feed.id
|
||||
}
|
||||
|
||||
private fun getMediafilePath(media: FeedMedia): String {
|
||||
val title = media.item?.feed?.title?:return ""
|
||||
val mediaPath = (MEDIA_DOWNLOADPATH
|
||||
+ FileNameGenerator.generateFileName(title))
|
||||
val mediaPath = (MEDIA_DOWNLOADPATH + FileNameGenerator.generateFileName(title))
|
||||
return UserPreferences.getDataFolder(mediaPath).toString() + "/"
|
||||
}
|
||||
|
||||
|
@ -106,16 +94,10 @@ object DownloadRequestCreator {
|
|||
val urlBaseFilename = URLUtil.guessFileName(media.download_url, null, media.mime_type)
|
||||
|
||||
var baseFilename: String
|
||||
baseFilename = if (titleBaseFilename != "") {
|
||||
titleBaseFilename
|
||||
} else {
|
||||
urlBaseFilename
|
||||
}
|
||||
baseFilename = if (titleBaseFilename != "") titleBaseFilename else urlBaseFilename
|
||||
val filenameMaxLength = 220
|
||||
if (baseFilename.length > filenameMaxLength) {
|
||||
baseFilename = baseFilename.substring(0, filenameMaxLength)
|
||||
}
|
||||
return (baseFilename + FilenameUtils.EXTENSION_SEPARATOR + media.id
|
||||
+ FilenameUtils.EXTENSION_SEPARATOR + FilenameUtils.getExtension(urlBaseFilename))
|
||||
if (baseFilename.length > filenameMaxLength) baseFilename = baseFilename.substring(0, filenameMaxLength)
|
||||
|
||||
return (baseFilename + FilenameUtils.EXTENSION_SEPARATOR + media.id + FilenameUtils.EXTENSION_SEPARATOR + FilenameUtils.getExtension(urlBaseFilename))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -18,24 +18,20 @@ import java.util.concurrent.TimeUnit
|
|||
|
||||
class DownloadServiceInterfaceImpl : DownloadServiceInterface() {
|
||||
override fun downloadNow(context: Context, item: FeedItem, ignoreConstraints: Boolean) {
|
||||
val workRequest: OneTimeWorkRequest.Builder =
|
||||
getRequest(context, item)
|
||||
val workRequest: OneTimeWorkRequest.Builder = getRequest(context, item)
|
||||
workRequest.setExpedited(OutOfQuotaPolicy.RUN_AS_NON_EXPEDITED_WORK_REQUEST)
|
||||
if (ignoreConstraints) {
|
||||
workRequest.setConstraints(Builder().setRequiredNetworkType(NetworkType.CONNECTED).build())
|
||||
} else {
|
||||
workRequest.setConstraints(constraints)
|
||||
}
|
||||
if (item.media?.download_url != null) WorkManager.getInstance(context).enqueueUniqueWork(item.media!!.download_url!!,
|
||||
ExistingWorkPolicy.KEEP, workRequest.build())
|
||||
if (ignoreConstraints) workRequest.setConstraints(Builder().setRequiredNetworkType(NetworkType.CONNECTED).build())
|
||||
else workRequest.setConstraints(constraints)
|
||||
|
||||
if (item.media?.download_url != null)
|
||||
WorkManager.getInstance(context).enqueueUniqueWork(item.media!!.download_url!!, ExistingWorkPolicy.KEEP, workRequest.build())
|
||||
}
|
||||
|
||||
override fun download(context: Context, item: FeedItem) {
|
||||
val workRequest: OneTimeWorkRequest.Builder =
|
||||
getRequest(context, item)
|
||||
val workRequest: OneTimeWorkRequest.Builder = getRequest(context, item)
|
||||
workRequest.setConstraints(constraints)
|
||||
if (item.media?.download_url != null) WorkManager.getInstance(context).enqueueUniqueWork(item.media!!.download_url!!,
|
||||
ExistingWorkPolicy.KEEP, workRequest.build())
|
||||
if (item.media?.download_url != null)
|
||||
WorkManager.getInstance(context).enqueueUniqueWork(item.media!!.download_url!!, ExistingWorkPolicy.KEEP, workRequest.build())
|
||||
}
|
||||
|
||||
@OptIn(UnstableApi::class) override fun cancel(context: Context, media: FeedMedia) {
|
||||
|
@ -65,7 +61,7 @@ class DownloadServiceInterfaceImpl : DownloadServiceInterface() {
|
|||
}
|
||||
|
||||
companion object {
|
||||
private fun getRequest(context: Context, item: FeedItem): OneTimeWorkRequest.Builder {
|
||||
@OptIn(UnstableApi::class) private fun getRequest(context: Context, item: FeedItem): OneTimeWorkRequest.Builder {
|
||||
val workRequest: OneTimeWorkRequest.Builder = OneTimeWorkRequest.Builder(EpisodeDownloadWorker::class.java)
|
||||
.setInitialDelay(0L, TimeUnit.MILLISECONDS)
|
||||
.addTag(WORK_TAG)
|
||||
|
@ -74,19 +70,16 @@ class DownloadServiceInterfaceImpl : DownloadServiceInterface() {
|
|||
DBWriter.addQueueItem(context, false, item.id)
|
||||
workRequest.addTag(WORK_DATA_WAS_QUEUED)
|
||||
}
|
||||
workRequest.setInputData(Data.Builder().putLong(WORK_DATA_MEDIA_ID, item.media!!
|
||||
.id).build())
|
||||
workRequest.setInputData(Data.Builder().putLong(WORK_DATA_MEDIA_ID, item.media!!.id).build())
|
||||
return workRequest
|
||||
}
|
||||
|
||||
private val constraints: Constraints
|
||||
get() {
|
||||
val constraints = Builder()
|
||||
if (UserPreferences.isAllowMobileEpisodeDownload) {
|
||||
constraints.setRequiredNetworkType(NetworkType.CONNECTED)
|
||||
} else {
|
||||
constraints.setRequiredNetworkType(NetworkType.UNMETERED)
|
||||
}
|
||||
if (UserPreferences.isAllowMobileEpisodeDownload) constraints.setRequiredNetworkType(NetworkType.CONNECTED)
|
||||
else constraints.setRequiredNetworkType(NetworkType.UNMETERED)
|
||||
|
||||
return constraints.build()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -50,9 +50,7 @@ class EpisodeDownloadWorker(context: Context, params: WorkerParameters) : Worker
|
|||
while (true) {
|
||||
try {
|
||||
synchronized(notificationProgress) {
|
||||
if (isInterrupted) {
|
||||
return
|
||||
}
|
||||
if (isInterrupted) return
|
||||
notificationProgress.put(media.getEpisodeTitle(), request.progressPercent)
|
||||
}
|
||||
setProgressAsync(
|
||||
|
@ -60,8 +58,7 @@ class EpisodeDownloadWorker(context: Context, params: WorkerParameters) : Worker
|
|||
.putInt(DownloadServiceInterface.WORK_DATA_PROGRESS, request.progressPercent)
|
||||
.build())
|
||||
.get()
|
||||
val nm = applicationContext
|
||||
.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||
val nm = applicationContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||
nm.notify(R.id.notification_downloading, generateProgressNotification())
|
||||
sleep(1000)
|
||||
} catch (e: InterruptedException) {
|
||||
|
@ -80,9 +77,9 @@ class EpisodeDownloadWorker(context: Context, params: WorkerParameters) : Worker
|
|||
e.printStackTrace()
|
||||
result = Result.failure()
|
||||
}
|
||||
if (result == Result.failure() && downloader?.downloadRequest?.destination != null) {
|
||||
if (result == Result.failure() && downloader?.downloadRequest?.destination != null)
|
||||
FileUtils.deleteQuietly(File(downloader!!.downloadRequest.destination!!))
|
||||
}
|
||||
|
||||
progressUpdaterThread.interrupt()
|
||||
try {
|
||||
progressUpdaterThread.join()
|
||||
|
@ -92,8 +89,7 @@ class EpisodeDownloadWorker(context: Context, params: WorkerParameters) : Worker
|
|||
synchronized(notificationProgress) {
|
||||
notificationProgress.remove(media.getEpisodeTitle())
|
||||
if (notificationProgress.isEmpty()) {
|
||||
val nm = applicationContext
|
||||
.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||
val nm = applicationContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||
nm.cancel(R.id.notification_downloading)
|
||||
}
|
||||
}
|
||||
|
@ -107,8 +103,7 @@ class EpisodeDownloadWorker(context: Context, params: WorkerParameters) : Worker
|
|||
}
|
||||
|
||||
override fun getForegroundInfoAsync(): ListenableFuture<ForegroundInfo> {
|
||||
return Futures.immediateFuture(
|
||||
ForegroundInfo(R.id.notification_downloading, generateProgressNotification()))
|
||||
return Futures.immediateFuture(ForegroundInfo(R.id.notification_downloading, generateProgressNotification()))
|
||||
}
|
||||
|
||||
@OptIn(UnstableApi::class) private fun performDownload(media: FeedMedia, request: DownloadRequest): Result {
|
||||
|
@ -144,22 +139,18 @@ class EpisodeDownloadWorker(context: Context, params: WorkerParameters) : Worker
|
|||
return Result.failure()
|
||||
}
|
||||
|
||||
if (downloader!!.cancelled) {
|
||||
// This also happens when the worker was preempted, not just when the user cancelled it
|
||||
return Result.success()
|
||||
}
|
||||
if (downloader!!.cancelled) return Result.success()
|
||||
|
||||
val status = downloader!!.result
|
||||
if (status.isSuccessful) {
|
||||
val handler = MediaDownloadedHandler(
|
||||
applicationContext, downloader!!.result, request)
|
||||
val handler = MediaDownloadedHandler(applicationContext, downloader!!.result, request)
|
||||
handler.run()
|
||||
DBWriter.addDownloadStatus(handler.updatedStatus)
|
||||
return Result.success()
|
||||
}
|
||||
|
||||
if (status.reason == DownloadError.ERROR_HTTP_DATA_ERROR
|
||||
&& status.reasonDetailed.toInt() == 416) {
|
||||
if (status.reason == DownloadError.ERROR_HTTP_DATA_ERROR && status.reasonDetailed.toInt() == 416) {
|
||||
Log.d(TAG, "Requested invalid range, restarting download from the beginning")
|
||||
if (downloader?.downloadRequest?.destination != null) FileUtils.deleteQuietly(File(downloader!!.downloadRequest.destination!!))
|
||||
sendMessage(request.title?:"", false)
|
||||
|
@ -181,9 +172,7 @@ class EpisodeDownloadWorker(context: Context, params: WorkerParameters) : Worker
|
|||
if (isLastRunAttempt) {
|
||||
sendErrorNotification(downloader!!.downloadRequest.title?:"")
|
||||
return Result.failure()
|
||||
} else {
|
||||
return Result.retry()
|
||||
}
|
||||
} else return Result.retry()
|
||||
}
|
||||
|
||||
private val isLastRunAttempt: Boolean
|
||||
|
@ -192,14 +181,11 @@ class EpisodeDownloadWorker(context: Context, params: WorkerParameters) : Worker
|
|||
private fun sendMessage(episodeTitle: String, isImmediateFail: Boolean) {
|
||||
var episodeTitle = episodeTitle
|
||||
val retrying = !isLastRunAttempt && !isImmediateFail
|
||||
if (episodeTitle.length > 20) {
|
||||
episodeTitle = episodeTitle.substring(0, 19) + "…"
|
||||
}
|
||||
EventBus.getDefault().post(MessageEvent(
|
||||
applicationContext.getString(
|
||||
if (episodeTitle.length > 20) episodeTitle = episodeTitle.substring(0, 19) + "…"
|
||||
|
||||
EventBus.getDefault().post(MessageEvent(applicationContext.getString(
|
||||
if (retrying) R.string.download_error_retrying else R.string.download_error_not_retrying,
|
||||
episodeTitle), { ctx: Context -> MainActivityStarter(ctx).withDownloadLogsOpen().start() },
|
||||
applicationContext.getString(R.string.download_error_details)))
|
||||
episodeTitle), { ctx: Context -> MainActivityStarter(ctx).withDownloadLogsOpen().start() }, applicationContext.getString(R.string.download_error_details)))
|
||||
}
|
||||
|
||||
private fun getDownloadLogsIntent(context: Context): PendingIntent {
|
||||
|
@ -220,8 +206,7 @@ class EpisodeDownloadWorker(context: Context, params: WorkerParameters) : Worker
|
|||
return
|
||||
}
|
||||
|
||||
val builder = NotificationCompat.Builder(applicationContext,
|
||||
NotificationUtils.CHANNEL_ID_DOWNLOAD_ERROR)
|
||||
val builder = NotificationCompat.Builder(applicationContext, NotificationUtils.CHANNEL_ID_DOWNLOAD_ERROR)
|
||||
builder.setTicker(applicationContext.getString(R.string.download_report_title))
|
||||
.setContentTitle(applicationContext.getString(R.string.download_report_title))
|
||||
.setContentText(applicationContext.getString(R.string.download_error_tap_for_details))
|
||||
|
@ -229,8 +214,7 @@ class EpisodeDownloadWorker(context: Context, params: WorkerParameters) : Worker
|
|||
.setContentIntent(getDownloadLogsIntent(applicationContext))
|
||||
.setAutoCancel(true)
|
||||
builder.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
|
||||
val nm = applicationContext
|
||||
.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||
val nm = applicationContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||
nm.notify(R.id.notification_download_report, builder.build())
|
||||
}
|
||||
|
||||
|
@ -244,12 +228,9 @@ class EpisodeDownloadWorker(context: Context, params: WorkerParameters) : Worker
|
|||
bigTextB.append(String.format(Locale.getDefault(), "%s (%d%%)\n", key, value))
|
||||
}
|
||||
val bigText = bigTextB.toString().trim { it <= ' ' }
|
||||
val contentText = if (progressCopy.size == 1) {
|
||||
bigText
|
||||
} else {
|
||||
applicationContext.resources.getQuantityString(R.plurals.downloads_left,
|
||||
progressCopy.size, progressCopy.size)
|
||||
}
|
||||
val contentText = if (progressCopy.size == 1) bigText
|
||||
else applicationContext.resources.getQuantityString(R.plurals.downloads_left, progressCopy.size, progressCopy.size)
|
||||
|
||||
val builder = NotificationCompat.Builder(applicationContext, NotificationUtils.CHANNEL_ID_DOWNLOADING)
|
||||
builder.setTicker(applicationContext.getString(R.string.download_notification_title_episodes))
|
||||
.setContentTitle(applicationContext.getString(R.string.download_notification_title_episodes))
|
||||
|
|
|
@ -48,20 +48,14 @@ class FeedUpdateWorker(context: Context, params: WorkerParameters) : Worker(cont
|
|||
val itr = toUpdate.iterator()
|
||||
while (itr.hasNext()) {
|
||||
val feed = itr.next()
|
||||
if (feed.preferences?.keepUpdated == false) {
|
||||
itr.remove()
|
||||
}
|
||||
if (!feed.isLocalFeed) {
|
||||
allAreLocal = false
|
||||
}
|
||||
if (feed.preferences?.keepUpdated == false) itr.remove()
|
||||
if (!feed.isLocalFeed) allAreLocal = false
|
||||
}
|
||||
toUpdate.shuffle() // If the worker gets cancelled early, every feed has a chance to be updated
|
||||
} else {
|
||||
val feed = DBReader.getFeed(feedId) ?: return Result.success()
|
||||
Log.d(TAG, "doWork feed.download_url: ${feed.download_url}")
|
||||
if (!feed.isLocalFeed) {
|
||||
allAreLocal = false
|
||||
}
|
||||
if (!feed.isLocalFeed) allAreLocal = false
|
||||
toUpdate = ArrayList()
|
||||
toUpdate.add(feed) // Needs to be updatable, so no singletonList
|
||||
force = true
|
||||
|
@ -87,8 +81,7 @@ class FeedUpdateWorker(context: Context, params: WorkerParameters) : Worker(cont
|
|||
if (toUpdate != null) {
|
||||
contentText = context.resources.getQuantityString(R.plurals.downloads_left,
|
||||
toUpdate.size, toUpdate.size)
|
||||
bigText = Stream.of(toUpdate).map { feed: Feed? -> "• " + feed!!.title }
|
||||
.collect(Collectors.joining("\n"))
|
||||
bigText = Stream.of(toUpdate).map { feed: Feed? -> "• " + feed!!.title }.collect(Collectors.joining("\n"))
|
||||
}
|
||||
return NotificationCompat.Builder(context, NotificationUtils.CHANNEL_ID_DOWNLOADING)
|
||||
.setContentTitle(context.getString(R.string.download_notification_title_feeds))
|
||||
|
@ -96,8 +89,7 @@ class FeedUpdateWorker(context: Context, params: WorkerParameters) : Worker(cont
|
|||
.setStyle(NotificationCompat.BigTextStyle().bigText(bigText))
|
||||
.setSmallIcon(R.drawable.ic_notification_sync)
|
||||
.setOngoing(true)
|
||||
.addAction(R.drawable.ic_cancel, context.getString(R.string.cancel_label),
|
||||
WorkManager.getInstance(context).createCancelPendingIntent(id))
|
||||
.addAction(R.drawable.ic_cancel, context.getString(R.string.cancel_label), WorkManager.getInstance(context).createCancelPendingIntent(id))
|
||||
.build()
|
||||
}
|
||||
|
||||
|
@ -122,21 +114,16 @@ class FeedUpdateWorker(context: Context, params: WorkerParameters) : Worker(cont
|
|||
}
|
||||
|
||||
while (toUpdate.isNotEmpty()) {
|
||||
if (isStopped) {
|
||||
return
|
||||
}
|
||||
if (isStopped) return
|
||||
|
||||
notificationManager.notify(R.id.notification_updating_feeds, createNotification(toUpdate))
|
||||
val feed = toUpdate[0]
|
||||
try {
|
||||
if (feed.isLocalFeed) {
|
||||
LocalFeedUpdater.updateFeed(feed, applicationContext, null)
|
||||
} else {
|
||||
refreshFeed(feed, force)
|
||||
}
|
||||
if (feed.isLocalFeed) LocalFeedUpdater.updateFeed(feed, applicationContext, null)
|
||||
else refreshFeed(feed, force)
|
||||
} catch (e: Exception) {
|
||||
DBWriter.setFeedLastUpdateFailed(feed.id, true)
|
||||
val status = DownloadResult(feed, feed.title?:"",
|
||||
DownloadError.ERROR_IO_ERROR, false, e.message?:"")
|
||||
val status = DownloadResult(feed, feed.title?:"", DownloadError.ERROR_IO_ERROR, false, e.message?:"")
|
||||
DBWriter.addDownloadStatus(status)
|
||||
}
|
||||
toUpdate.removeAt(0)
|
||||
|
@ -145,16 +132,12 @@ class FeedUpdateWorker(context: Context, params: WorkerParameters) : Worker(cont
|
|||
|
||||
@UnstableApi @Throws(Exception::class)
|
||||
fun refreshFeed(feed: Feed, force: Boolean) {
|
||||
val nextPage = (inputData.getBoolean(FeedUpdateManager.EXTRA_NEXT_PAGE, false)
|
||||
&& feed.nextPageLink != null)
|
||||
if (nextPage) {
|
||||
feed.pageNr += 1
|
||||
}
|
||||
val nextPage = (inputData.getBoolean(FeedUpdateManager.EXTRA_NEXT_PAGE, false) && feed.nextPageLink != null)
|
||||
if (nextPage) feed.pageNr += 1
|
||||
|
||||
val builder = create(feed)
|
||||
builder.setForce(force || feed.hasLastUpdateFailed())
|
||||
if (nextPage) {
|
||||
builder.source = feed.nextPageLink
|
||||
}
|
||||
if (nextPage) builder.source = feed.nextPageLink
|
||||
val request = builder.build()
|
||||
|
||||
val downloader = DefaultDownloaderFactory().create(request) ?: throw Exception("Unable to create downloader")
|
||||
|
@ -162,9 +145,8 @@ class FeedUpdateWorker(context: Context, params: WorkerParameters) : Worker(cont
|
|||
downloader.call()
|
||||
|
||||
if (!downloader.result.isSuccessful) {
|
||||
if (downloader.cancelled || downloader.result.reason == DownloadError.ERROR_DOWNLOAD_CANCELLED) {
|
||||
return
|
||||
}
|
||||
if (downloader.cancelled || downloader.result.reason == DownloadError.ERROR_DOWNLOAD_CANCELLED) return
|
||||
|
||||
DBWriter.setFeedLastUpdateFailed(request.feedfileId, true)
|
||||
DBWriter.addDownloadStatus(downloader.result)
|
||||
return
|
||||
|
@ -179,26 +161,21 @@ class FeedUpdateWorker(context: Context, params: WorkerParameters) : Worker(cont
|
|||
return
|
||||
}
|
||||
|
||||
if (request.feedfileId == 0L) {
|
||||
return // No download logs for new subscriptions
|
||||
}
|
||||
if (request.feedfileId == 0L) return // No download logs for new subscriptions
|
||||
|
||||
// we create a 'successful' download log if the feed's last refresh failed
|
||||
val log = DBReader.getFeedDownloadLog(request.feedfileId)
|
||||
if (log.isNotEmpty() && !log[0].isSuccessful) {
|
||||
DBWriter.addDownloadStatus(feedSyncTask.downloadStatus)
|
||||
}
|
||||
if (log.isNotEmpty() && !log[0].isSuccessful) DBWriter.addDownloadStatus(feedSyncTask.downloadStatus)
|
||||
|
||||
// newEpisodesNotification.showIfNeeded(applicationContext, feedSyncTask.savedFeed!!)
|
||||
if (!request.source.isNullOrEmpty()) {
|
||||
when {
|
||||
!downloader.permanentRedirectUrl.isNullOrEmpty() -> {
|
||||
DBWriter.updateFeedDownloadURL(request.source, downloader.permanentRedirectUrl!!)
|
||||
}
|
||||
feedSyncTask.redirectUrl.isNotEmpty() && feedSyncTask.redirectUrl != request.source -> {
|
||||
!downloader.permanentRedirectUrl.isNullOrEmpty() -> DBWriter.updateFeedDownloadURL(request.source, downloader.permanentRedirectUrl!!)
|
||||
feedSyncTask.redirectUrl.isNotEmpty() && feedSyncTask.redirectUrl != request.source ->
|
||||
DBWriter.updateFeedDownloadURL(request.source, feedSyncTask.redirectUrl)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TAG = "FeedUpdateWorker"
|
||||
|
|
|
@ -50,9 +50,7 @@ class HttpDownloader(request: DownloadRequest) : Downloader(request) {
|
|||
httpReq.cacheControl(CacheControl.Builder().noCache().build()) // noStore breaks CDNs
|
||||
}
|
||||
|
||||
if (uri.scheme == "http") {
|
||||
httpReq.addHeader("Upgrade-Insecure-Requests", "1")
|
||||
}
|
||||
if (uri.scheme == "http") httpReq.addHeader("Upgrade-Insecure-Requests", "1")
|
||||
|
||||
if (!downloadRequest.lastModified.isNullOrEmpty()) {
|
||||
val lastModified = downloadRequest.lastModified
|
||||
|
@ -80,9 +78,7 @@ class HttpDownloader(request: DownloadRequest) : Downloader(request) {
|
|||
responseBody = response.body
|
||||
val contentEncodingHeader = response.header("Content-Encoding")
|
||||
var isGzip = false
|
||||
if (!contentEncodingHeader.isNullOrEmpty()) {
|
||||
isGzip = TextUtils.equals(contentEncodingHeader.lowercase(Locale.getDefault()), "gzip")
|
||||
}
|
||||
if (!contentEncodingHeader.isNullOrEmpty()) isGzip = TextUtils.equals(contentEncodingHeader.lowercase(Locale.getDefault()), "gzip")
|
||||
|
||||
Log.d(TAG, "Response code is " + response.code)// check if size specified in the response header is the same as the size of the
|
||||
// written file. This check cannot be made if compression was used
|
||||
|
@ -117,9 +113,8 @@ class HttpDownloader(request: DownloadRequest) : Downloader(request) {
|
|||
} else {
|
||||
var success = destination.delete()
|
||||
success = success or destination.createNewFile()
|
||||
if (!success) {
|
||||
throw IOException("Unable to recreate partially downloaded file")
|
||||
}
|
||||
if (!success) throw IOException("Unable to recreate partially downloaded file")
|
||||
|
||||
out = RandomAccessFile(destination, "rw")
|
||||
}
|
||||
|
||||
|
@ -129,9 +124,7 @@ class HttpDownloader(request: DownloadRequest) : Downloader(request) {
|
|||
Log.d(TAG, "Getting size of download")
|
||||
downloadRequest.size = responseBody.contentLength() + downloadRequest.soFar
|
||||
Log.d(TAG, "Size is " + downloadRequest.size)
|
||||
if (downloadRequest.size < 0) {
|
||||
downloadRequest.size = DownloadResult.SIZE_UNKNOWN.toLong()
|
||||
}
|
||||
if (downloadRequest.size < 0) downloadRequest.size = DownloadResult.SIZE_UNKNOWN.toLong()
|
||||
|
||||
val freeSpace = freeSpaceAvailable
|
||||
Log.d(TAG, "Free space is $freeSpace")
|
||||
|
@ -159,8 +152,8 @@ class HttpDownloader(request: DownloadRequest) : Downloader(request) {
|
|||
// written file. This check cannot be made if compression was used
|
||||
when {
|
||||
!isGzip && downloadRequest.size != DownloadResult.SIZE_UNKNOWN.toLong() && downloadRequest.soFar != downloadRequest.size -> {
|
||||
onFail(DownloadError.ERROR_IO_WRONG_SIZE, "Download completed but size: "
|
||||
+ downloadRequest.soFar + " does not equal expected size " + downloadRequest.size)
|
||||
onFail(DownloadError.ERROR_IO_WRONG_SIZE,
|
||||
"Download completed but size: ${downloadRequest.soFar} does not equal expected size ${downloadRequest.size}")
|
||||
return
|
||||
}
|
||||
downloadRequest.size > 0 && downloadRequest.soFar == 0L -> {
|
||||
|
@ -169,11 +162,8 @@ class HttpDownloader(request: DownloadRequest) : Downloader(request) {
|
|||
}
|
||||
else -> {
|
||||
val lastModified = response.header("Last-Modified")
|
||||
if (lastModified != null) {
|
||||
downloadRequest.setLastModified(lastModified)
|
||||
} else {
|
||||
downloadRequest.setLastModified(response.header("ETag"))
|
||||
}
|
||||
if (lastModified != null) downloadRequest.setLastModified(lastModified)
|
||||
else downloadRequest.setLastModified(response.header("ETag"))
|
||||
onSuccess()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -31,9 +31,7 @@ object PodciniHttpClient {
|
|||
@JvmStatic
|
||||
@Synchronized
|
||||
fun getHttpClient(): OkHttpClient {
|
||||
if (httpClient == null) {
|
||||
httpClient = newBuilder().build()
|
||||
}
|
||||
if (httpClient == null) httpClient = newBuilder().build()
|
||||
return httpClient!!
|
||||
}
|
||||
|
||||
|
|
|
@ -24,10 +24,8 @@ class FeedParserTask(private val request: DownloadRequest) : Callable<FeedHandle
|
|||
private set
|
||||
|
||||
init {
|
||||
downloadStatus = DownloadResult(
|
||||
0, request.title?:"", 0, request.feedfileType, false,
|
||||
DownloadError.ERROR_REQUEST_ERROR, Date(),
|
||||
"Unknown error: Status not set")
|
||||
downloadStatus = DownloadResult(0, request.title?:"", 0, request.feedfileType, false,
|
||||
DownloadError.ERROR_REQUEST_ERROR, Date(), "Unknown error: Status not set")
|
||||
}
|
||||
|
||||
override fun call(): FeedHandlerResult? {
|
||||
|
@ -48,9 +46,7 @@ class FeedParserTask(private val request: DownloadRequest) : Callable<FeedHandle
|
|||
result = feedHandler.parseFeed(feed)
|
||||
Log.d(TAG, feed.title + " parsed")
|
||||
checkFeedData(feed)
|
||||
if (feed.imageUrl.isNullOrEmpty()) {
|
||||
feed.imageUrl = Feed.PREFIX_GENERATIVE_COVER + feed.download_url
|
||||
}
|
||||
if (feed.imageUrl.isNullOrEmpty()) feed.imageUrl = Feed.PREFIX_GENERATIVE_COVER + feed.download_url
|
||||
} catch (e: SAXException) {
|
||||
isSuccessful = false
|
||||
e.printStackTrace()
|
||||
|
@ -70,9 +66,7 @@ class FeedParserTask(private val request: DownloadRequest) : Callable<FeedHandle
|
|||
e.printStackTrace()
|
||||
isSuccessful = false
|
||||
reason = DownloadError.ERROR_UNSUPPORTED_TYPE
|
||||
if ("html".equals(e.rootElement, ignoreCase = true)) {
|
||||
reason = DownloadError.ERROR_UNSUPPORTED_TYPE_HTML
|
||||
}
|
||||
if ("html".equals(e.rootElement, ignoreCase = true)) reason = DownloadError.ERROR_UNSUPPORTED_TYPE_HTML
|
||||
reasonDetailed = e.message
|
||||
} catch (e: InvalidFeedException) {
|
||||
e.printStackTrace()
|
||||
|
@ -88,8 +82,7 @@ class FeedParserTask(private val request: DownloadRequest) : Callable<FeedHandle
|
|||
}
|
||||
|
||||
if (isSuccessful) {
|
||||
downloadStatus = DownloadResult(feed, feed.getHumanReadableIdentifier()?:"", DownloadError.SUCCESS,
|
||||
isSuccessful, reasonDetailed?:"")
|
||||
downloadStatus = DownloadResult(feed, feed.getHumanReadableIdentifier()?:"", DownloadError.SUCCESS, isSuccessful, reasonDetailed?:"")
|
||||
return result
|
||||
} else {
|
||||
downloadStatus = DownloadResult(feed, feed.getHumanReadableIdentifier()?:"", reason?:DownloadError.ERROR_NOT_FOUND,
|
||||
|
@ -103,18 +96,14 @@ class FeedParserTask(private val request: DownloadRequest) : Callable<FeedHandle
|
|||
*/
|
||||
@Throws(InvalidFeedException::class)
|
||||
private fun checkFeedData(feed: Feed) {
|
||||
if (feed.title == null) {
|
||||
throw InvalidFeedException("Feed has no title")
|
||||
}
|
||||
if (feed.title == null) throw InvalidFeedException("Feed has no title")
|
||||
checkFeedItems(feed)
|
||||
}
|
||||
|
||||
@Throws(InvalidFeedException::class)
|
||||
private fun checkFeedItems(feed: Feed) {
|
||||
for (item in feed.items) {
|
||||
if (item.title == null) {
|
||||
throw InvalidFeedException("Item has no title: $item")
|
||||
}
|
||||
if (item.title == null) throw InvalidFeedException("Item has no title: $item")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -17,9 +17,7 @@ class FeedSyncTask(private val context: Context, request: DownloadRequest) {
|
|||
|
||||
fun run(): Boolean {
|
||||
feedHandlerResult = task.call()
|
||||
if (!task.isSuccessful) {
|
||||
return false
|
||||
}
|
||||
if (!task.isSuccessful) return false
|
||||
|
||||
savedFeed = DBTasks.updateFeed(context, feedHandlerResult!!.feed, false)
|
||||
return true
|
||||
|
|
|
@ -21,9 +21,7 @@ import java.util.concurrent.ExecutionException
|
|||
/**
|
||||
* Handles a completed media download.
|
||||
*/
|
||||
class MediaDownloadedHandler(private val context: Context, var updatedStatus: DownloadResult,
|
||||
private val request: DownloadRequest
|
||||
) : Runnable {
|
||||
class MediaDownloadedHandler(private val context: Context, var updatedStatus: DownloadResult, private val request: DownloadRequest) : Runnable {
|
||||
@UnstableApi override fun run() {
|
||||
val media = DBReader.getFeedMedia(request.feedfileId)
|
||||
if (media == null) {
|
||||
|
@ -38,13 +36,10 @@ class MediaDownloadedHandler(private val context: Context, var updatedStatus: Do
|
|||
media.checkEmbeddedPicture() // enforce check
|
||||
|
||||
// check if file has chapters
|
||||
if (media.item != null && !media.item!!.hasChapters()) {
|
||||
media.setChapters(ChapterUtils.loadChaptersFromMediaFile(media, context))
|
||||
}
|
||||
if (media.item != null && !media.item!!.hasChapters()) media.setChapters(ChapterUtils.loadChaptersFromMediaFile(media, context))
|
||||
|
||||
if (media.item?.podcastIndexChapterUrl != null) ChapterUtils.loadChaptersFromUrl(media.item!!.podcastIndexChapterUrl!!, false)
|
||||
|
||||
if (media.item?.podcastIndexChapterUrl != null) {
|
||||
ChapterUtils.loadChaptersFromUrl(media.item!!.podcastIndexChapterUrl!!, false)
|
||||
}
|
||||
// Get duration
|
||||
var durationStr: String? = null
|
||||
try {
|
||||
|
@ -73,16 +68,13 @@ class MediaDownloadedHandler(private val context: Context, var updatedStatus: Do
|
|||
// so we do it after the enclosing media has been updated above,
|
||||
// to ensure subscribers will get the updated FeedMedia as well
|
||||
DBWriter.setFeedItem(item).get()
|
||||
if (broadcastUnreadStateUpdate) {
|
||||
EventBus.getDefault().post(UnreadItemsUpdateEvent())
|
||||
}
|
||||
if (broadcastUnreadStateUpdate) EventBus.getDefault().post(UnreadItemsUpdateEvent())
|
||||
}
|
||||
} catch (e: InterruptedException) {
|
||||
Log.e(TAG, "MediaHandlerThread was interrupted")
|
||||
} catch (e: ExecutionException) {
|
||||
Log.e(TAG, "ExecutionException in MediaHandlerThread: " + e.message)
|
||||
updatedStatus = DownloadResult(media, media.getEpisodeTitle(),
|
||||
DownloadError.ERROR_DB_ACCESS_ERROR, false, e.message?:"")
|
||||
updatedStatus = DownloadResult(media, media.getEpisodeTitle(), DownloadError.ERROR_DB_ACCESS_ERROR, false, e.message?:"")
|
||||
}
|
||||
|
||||
if (item != null) {
|
||||
|
|
|
@ -17,8 +17,9 @@ class DownloadRequest private constructor(@JvmField val destination: String?,
|
|||
@JvmField var password: String?,
|
||||
private val mediaEnqueued: Boolean,
|
||||
@JvmField val arguments: Bundle?,
|
||||
private val initiatedByUser: Boolean
|
||||
) : Parcelable {
|
||||
private val initiatedByUser: Boolean)
|
||||
: Parcelable {
|
||||
|
||||
var progressPercent: Int = 0
|
||||
var soFar: Long = 0
|
||||
var size: Long = 0
|
||||
|
@ -26,9 +27,8 @@ class DownloadRequest private constructor(@JvmField val destination: String?,
|
|||
|
||||
constructor(destination: String, source: String, title: String, feedfileId: Long,
|
||||
feedfileType: Int, username: String?, password: String?,
|
||||
arguments: Bundle?, initiatedByUser: Boolean
|
||||
) : this(destination, source, title, feedfileId, feedfileType, null, username, password, false,
|
||||
arguments, initiatedByUser)
|
||||
arguments: Bundle?, initiatedByUser: Boolean)
|
||||
: this(destination, source, title, feedfileId, feedfileType, null, username, password, false, arguments, initiatedByUser)
|
||||
|
||||
private constructor(builder: Builder) : this(builder.destination,
|
||||
builder.source,
|
||||
|
@ -70,10 +70,8 @@ class DownloadRequest private constructor(@JvmField val destination: String?,
|
|||
// of them from a Parcel (from an Intent extra to submit a request to DownloadService) will fail.
|
||||
//
|
||||
// see: https://stackoverflow.com/a/22926342
|
||||
dest.writeString(nonNullString(
|
||||
username))
|
||||
dest.writeString(nonNullString(
|
||||
password))
|
||||
dest.writeString(nonNullString(username))
|
||||
dest.writeString(nonNullString(password))
|
||||
dest.writeByte(if ((mediaEnqueued)) 1.toByte() else 0)
|
||||
dest.writeBundle(arguments)
|
||||
dest.writeByte(if (initiatedByUser) 1.toByte() else 0)
|
||||
|
@ -170,9 +168,7 @@ class DownloadRequest private constructor(@JvmField val destination: String?,
|
|||
// }
|
||||
|
||||
fun setForce(force: Boolean) {
|
||||
if (force) {
|
||||
lastModified = null
|
||||
}
|
||||
if (force) lastModified = null
|
||||
}
|
||||
|
||||
fun lastModified(lastModified: String?): Builder {
|
||||
|
|
|
@ -27,13 +27,11 @@ abstract class DownloadServiceInterface {
|
|||
abstract fun cancelAll(context: Context)
|
||||
|
||||
fun isDownloadingEpisode(url: String): Boolean {
|
||||
return (currentDownloads.containsKey(url)
|
||||
&& currentDownloads[url]!!.state != DownloadStatus.STATE_COMPLETED)
|
||||
return (currentDownloads.containsKey(url) && currentDownloads[url]!!.state != DownloadStatus.STATE_COMPLETED)
|
||||
}
|
||||
|
||||
fun isEpisodeQueued(url: String): Boolean {
|
||||
return (currentDownloads.containsKey(url)
|
||||
&& currentDownloads[url]!!.state == DownloadStatus.STATE_QUEUED)
|
||||
return (currentDownloads.containsKey(url) && currentDownloads[url]!!.state == DownloadStatus.STATE_QUEUED)
|
||||
}
|
||||
|
||||
fun getProgress(url: String): Int {
|
||||
|
|
|
@ -5,15 +5,11 @@ import ac.mdiq.podcini.storage.model.feed.FeedItem
|
|||
import ac.mdiq.podcini.storage.model.feed.FeedMedia
|
||||
|
||||
class DownloadServiceInterfaceStub : DownloadServiceInterface() {
|
||||
override fun downloadNow(context: Context, item: FeedItem, ignoreConstraints: Boolean) {
|
||||
}
|
||||
override fun downloadNow(context: Context, item: FeedItem, ignoreConstraints: Boolean) {}
|
||||
|
||||
override fun download(context: Context, item: FeedItem) {
|
||||
}
|
||||
override fun download(context: Context, item: FeedItem) {}
|
||||
|
||||
override fun cancel(context: Context, media: FeedMedia) {
|
||||
}
|
||||
override fun cancel(context: Context, media: FeedMedia) {}
|
||||
|
||||
override fun cancelAll(context: Context) {
|
||||
}
|
||||
override fun cancelAll(context: Context) {}
|
||||
}
|
||||
|
|
|
@ -24,9 +24,7 @@ object BackportTrustManager {
|
|||
factory = TrustManagerFactory.getInstance(TrustManagerFactory.getDefaultAlgorithm())
|
||||
factory.init(keystore)
|
||||
for (manager in factory.trustManagers) {
|
||||
if (manager is X509TrustManager) {
|
||||
return manager
|
||||
}
|
||||
if (manager is X509TrustManager) return manager
|
||||
}
|
||||
} catch (e: NoSuchAlgorithmException) {
|
||||
e.printStackTrace()
|
||||
|
|
|
@ -8,10 +8,7 @@ import ac.mdiq.podcini.net.sync.model.EpisodeAction
|
|||
object EpisodeActionFilter {
|
||||
const val TAG: String = "EpisodeActionFilter"
|
||||
|
||||
fun getRemoteActionsOverridingLocalActions(
|
||||
remoteActions: List<EpisodeAction>,
|
||||
queuedEpisodeActions: List<EpisodeAction>
|
||||
): Map<Pair<String, String>, EpisodeAction> {
|
||||
fun getRemoteActionsOverridingLocalActions(remoteActions: List<EpisodeAction>, queuedEpisodeActions: List<EpisodeAction>): Map<Pair<String, String>, EpisodeAction> {
|
||||
// make sure more recent local actions are not overwritten by older remote actions
|
||||
val remoteActionsThatOverrideLocalActions: MutableMap<Pair<String, String>, EpisodeAction> = ArrayMap()
|
||||
val localMostRecentPlayActions = createUniqueLocalMostRecentPlayActions(queuedEpisodeActions)
|
||||
|
@ -22,13 +19,9 @@ object EpisodeActionFilter {
|
|||
EpisodeAction.Action.NEW, EpisodeAction.Action.DOWNLOAD -> {}
|
||||
EpisodeAction.Action.PLAY -> {
|
||||
val localMostRecent = localMostRecentPlayActions[key]
|
||||
if (secondActionOverridesFirstAction(remoteAction, localMostRecent)) {
|
||||
break
|
||||
}
|
||||
if (secondActionOverridesFirstAction(remoteAction, localMostRecent)) break
|
||||
val remoteMostRecentAction = remoteActionsThatOverrideLocalActions[key]
|
||||
if (secondActionOverridesFirstAction(remoteAction, remoteMostRecentAction)) {
|
||||
break
|
||||
}
|
||||
if (secondActionOverridesFirstAction(remoteAction, remoteMostRecentAction)) break
|
||||
remoteActionsThatOverrideLocalActions[key] = remoteAction
|
||||
}
|
||||
EpisodeAction.Action.DELETE -> {}
|
||||
|
@ -39,9 +32,7 @@ object EpisodeActionFilter {
|
|||
return remoteActionsThatOverrideLocalActions
|
||||
}
|
||||
|
||||
private fun createUniqueLocalMostRecentPlayActions(
|
||||
queuedEpisodeActions: List<EpisodeAction>
|
||||
): Map<Pair<String, String>, EpisodeAction> {
|
||||
private fun createUniqueLocalMostRecentPlayActions(queuedEpisodeActions: List<EpisodeAction>): Map<Pair<String, String>, EpisodeAction> {
|
||||
val localMostRecentPlayAction: MutableMap<Pair<String, String>, EpisodeAction> =
|
||||
ArrayMap()
|
||||
for (action in queuedEpisodeActions) {
|
||||
|
@ -49,20 +40,14 @@ object EpisodeActionFilter {
|
|||
val key = Pair(action.podcast!!, action.episode!!)
|
||||
val mostRecent = localMostRecentPlayAction[key]
|
||||
when {
|
||||
mostRecent?.timestamp == null -> {
|
||||
localMostRecentPlayAction[key] = action
|
||||
}
|
||||
mostRecent.timestamp!!.before(action.timestamp) -> {
|
||||
localMostRecentPlayAction[key] = action
|
||||
}
|
||||
mostRecent?.timestamp == null -> localMostRecentPlayAction[key] = action
|
||||
mostRecent.timestamp!!.before(action.timestamp) -> localMostRecentPlayAction[key] = action
|
||||
}
|
||||
}
|
||||
return localMostRecentPlayAction
|
||||
}
|
||||
|
||||
private fun secondActionOverridesFirstAction(firstAction: EpisodeAction,
|
||||
secondAction: EpisodeAction?
|
||||
): Boolean {
|
||||
private fun secondActionOverridesFirstAction(firstAction: EpisodeAction, secondAction: EpisodeAction?): Boolean {
|
||||
return secondAction?.timestamp != null && (firstAction.timestamp == null || secondAction.timestamp!!.after(firstAction.timestamp))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -3,8 +3,7 @@ package ac.mdiq.podcini.net.sync
|
|||
object GuidValidator {
|
||||
@JvmStatic
|
||||
fun isValidGuid(guid: String?): Boolean {
|
||||
return (guid != null && !guid.trim { it <= ' ' }.isEmpty()
|
||||
&& guid != "null")
|
||||
return (guid != null && !guid.trim { it <= ' ' }.isEmpty() && guid != "null")
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -19,16 +19,9 @@ class HostnameParser(hosturl: String?) {
|
|||
if (m.matches()) {
|
||||
scheme = m.group(1)
|
||||
host = IDN.toASCII(m.group(2))
|
||||
port = if (m.group(3) == null) {
|
||||
-1
|
||||
} else {
|
||||
m.group(3).toInt() // regex -> can only be digits
|
||||
}
|
||||
subfolder = if (m.group(4) == null) {
|
||||
""
|
||||
} else {
|
||||
StringUtils.stripEnd(m.group(4), "/")
|
||||
}
|
||||
// regex -> can only be digits
|
||||
port = if (m.group(3) == null) -1 else m.group(3).toInt()
|
||||
subfolder = if (m.group(4) == null) "" else StringUtils.stripEnd(m.group(4), "/")
|
||||
} else {
|
||||
// URL does not match regex: use it anyway -> this will cause an exception on connect
|
||||
scheme = "https"
|
||||
|
@ -37,21 +30,12 @@ class HostnameParser(hosturl: String?) {
|
|||
}
|
||||
|
||||
when {
|
||||
scheme == null && port == 80 -> {
|
||||
scheme = "http"
|
||||
scheme == null && port == 80 -> scheme = "http"
|
||||
scheme == null -> scheme = "https" // assume https
|
||||
}
|
||||
scheme == null -> {
|
||||
scheme = "https" // assume https
|
||||
}
|
||||
}
|
||||
|
||||
when {
|
||||
scheme == "https" && port == -1 -> {
|
||||
port = 443
|
||||
}
|
||||
scheme == "http" && port == -1 -> {
|
||||
port = 80
|
||||
}
|
||||
scheme == "https" && port == -1 -> port = 443
|
||||
scheme == "http" && port == -1 -> port = 80
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -73,10 +73,8 @@ class SyncService(context: Context, params: WorkerParameters) : Worker(context,
|
|||
Log.e(TAG, Log.getStackTraceString(e))
|
||||
|
||||
if (e is SyncServiceException) {
|
||||
if (runAttemptCount % 3 == 2) {
|
||||
// Do not spam users with notification and retry before notifying
|
||||
updateErrorNotification(e)
|
||||
}
|
||||
if (runAttemptCount % 3 == 2) updateErrorNotification(e)
|
||||
return Result.retry()
|
||||
} else {
|
||||
updateErrorNotification(e)
|
||||
|
@ -115,9 +113,7 @@ class SyncService(context: Context, params: WorkerParameters) : Worker(context,
|
|||
|
||||
// remove subscription if not just subscribed (again)
|
||||
for (downloadUrl in subscriptionChanges.removed) {
|
||||
if (!queuedAddedFeeds.contains(downloadUrl)) {
|
||||
removeFeedWithDownloadUrl(applicationContext, downloadUrl)
|
||||
}
|
||||
if (!queuedAddedFeeds.contains(downloadUrl)) removeFeedWithDownloadUrl(applicationContext, downloadUrl)
|
||||
}
|
||||
|
||||
if (lastSync == 0L) {
|
||||
|
@ -148,11 +144,8 @@ class SyncService(context: Context, params: WorkerParameters) : Worker(context,
|
|||
try {
|
||||
while (true) {
|
||||
Thread.sleep(1000)
|
||||
val event = EventBus.getDefault().getStickyEvent(
|
||||
FeedUpdateRunningEvent::class.java)
|
||||
if (event == null || !event.isFeedUpdateRunning) {
|
||||
return
|
||||
}
|
||||
val event = EventBus.getDefault().getStickyEvent(FeedUpdateRunningEvent::class.java)
|
||||
if (event == null || !event.isFeedUpdateRunning) return
|
||||
}
|
||||
} catch (e: InterruptedException) {
|
||||
e.printStackTrace()
|
||||
|
@ -173,8 +166,7 @@ class SyncService(context: Context, params: WorkerParameters) : Worker(context,
|
|||
val queuedEpisodeActions: MutableList<EpisodeAction> = synchronizationQueueStorage.queuedEpisodeActions
|
||||
if (lastSync == 0L) {
|
||||
EventBus.getDefault().postSticky(SyncServiceEvent(R.string.sync_status_upload_played))
|
||||
val readItems = getEpisodes(0, Int.MAX_VALUE,
|
||||
FeedItemFilter(FeedItemFilter.PLAYED), SortOrder.DATE_NEW_OLD)
|
||||
val readItems = getEpisodes(0, Int.MAX_VALUE, FeedItemFilter(FeedItemFilter.PLAYED), SortOrder.DATE_NEW_OLD)
|
||||
Log.d(TAG, "First sync. Upload state for all " + readItems.size + " played episodes")
|
||||
for (item in readItems) {
|
||||
val media = item.media ?: continue
|
||||
|
@ -190,8 +182,7 @@ class SyncService(context: Context, params: WorkerParameters) : Worker(context,
|
|||
if (queuedEpisodeActions.size > 0) {
|
||||
LockingAsyncExecutor.lock.lock()
|
||||
try {
|
||||
Log.d(TAG, "Uploading " + queuedEpisodeActions.size + " actions: "
|
||||
+ StringUtils.join(queuedEpisodeActions, ", "))
|
||||
Log.d(TAG, "Uploading ${queuedEpisodeActions.size} actions: ${StringUtils.join(queuedEpisodeActions, ", ")}")
|
||||
val postResponse = syncServiceImpl.uploadEpisodeActions(queuedEpisodeActions)
|
||||
newTimeStamp = postResponse?.timestamp?:0L
|
||||
Log.d(TAG, "Upload episode response: $postResponse")
|
||||
|
@ -206,12 +197,9 @@ class SyncService(context: Context, params: WorkerParameters) : Worker(context,
|
|||
@UnstableApi @Synchronized
|
||||
private fun processEpisodeActions(remoteActions: List<EpisodeAction>) {
|
||||
Log.d(TAG, "Processing " + remoteActions.size + " actions")
|
||||
if (remoteActions.isEmpty()) {
|
||||
return
|
||||
}
|
||||
if (remoteActions.isEmpty()) return
|
||||
|
||||
val playActionsToUpdate = getRemoteActionsOverridingLocalActions(remoteActions,
|
||||
synchronizationQueueStorage.queuedEpisodeActions)
|
||||
val playActionsToUpdate = getRemoteActionsOverridingLocalActions(remoteActions, synchronizationQueueStorage.queuedEpisodeActions)
|
||||
val queueToBeRemoved = LongList()
|
||||
val updatedItems: MutableList<FeedItem> = ArrayList()
|
||||
for (action in playActionsToUpdate.values) {
|
||||
|
@ -231,9 +219,8 @@ class SyncService(context: Context, params: WorkerParameters) : Worker(context,
|
|||
feedItem.setPlayed(true)
|
||||
feedItem.media!!.setPosition(0)
|
||||
queueToBeRemoved.add(feedItem.id)
|
||||
} else {
|
||||
Log.d(TAG, "Setting position: $action")
|
||||
}
|
||||
} else Log.d(TAG, "Setting position: $action")
|
||||
|
||||
updatedItems.add(feedItem)
|
||||
}
|
||||
removeQueueItem(applicationContext, false, *queueToBeRemoved.toArray())
|
||||
|
@ -242,16 +229,14 @@ class SyncService(context: Context, params: WorkerParameters) : Worker(context,
|
|||
}
|
||||
|
||||
private fun clearErrorNotifications() {
|
||||
val nm = applicationContext
|
||||
.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||
val nm = applicationContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||
nm.cancel(R.id.notification_gpodnet_sync_error)
|
||||
nm.cancel(R.id.notification_gpodnet_sync_autherror)
|
||||
}
|
||||
|
||||
private fun updateErrorNotification(exception: Exception) {
|
||||
Log.d(TAG, "Posting sync error notification")
|
||||
val description = (applicationContext.getString(R.string.gpodnetsync_error_descr)
|
||||
+ exception.message)
|
||||
val description = ("${applicationContext.getString(R.string.gpodnetsync_error_descr)}${exception.message}")
|
||||
|
||||
if (!gpodnetNotificationsEnabled()) {
|
||||
Log.d(TAG, "Skipping sync error notification because of user setting")
|
||||
|
@ -265,8 +250,7 @@ class SyncService(context: Context, params: WorkerParameters) : Worker(context,
|
|||
val intent = applicationContext.packageManager.getLaunchIntentForPackage(
|
||||
applicationContext.packageName)
|
||||
val pendingIntent = PendingIntent.getActivity(applicationContext,
|
||||
R.id.pending_intent_sync_error, intent, PendingIntent.FLAG_UPDATE_CURRENT
|
||||
or PendingIntent.FLAG_IMMUTABLE)
|
||||
R.id.pending_intent_sync_error, intent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
|
||||
val notification = NotificationCompat.Builder(applicationContext,
|
||||
NotificationUtils.CHANNEL_ID_SYNC_ERROR)
|
||||
.setContentTitle(applicationContext.getString(R.string.gpodnetsync_error_title))
|
||||
|
@ -277,17 +261,15 @@ class SyncService(context: Context, params: WorkerParameters) : Worker(context,
|
|||
.setAutoCancel(true)
|
||||
.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
|
||||
.build()
|
||||
val nm = applicationContext
|
||||
.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||
val nm = applicationContext.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||
nm.notify(R.id.notification_gpodnet_sync_error, notification)
|
||||
}
|
||||
|
||||
private fun getActiveSyncProvider(): ISyncService? {
|
||||
val selectedSyncProviderKey = SynchronizationSettings.selectedSyncProviderKey
|
||||
val selectedService = fromIdentifier(selectedSyncProviderKey?:"")
|
||||
if (selectedService == null) {
|
||||
return null
|
||||
}
|
||||
if (selectedService == null) return null
|
||||
|
||||
return when (selectedService) {
|
||||
SynchronizationProviderViewData.GPODDER_NET -> GpodnetService(getHttpClient(),
|
||||
hosturl, deviceID?:"", username?:"", password?:"")
|
||||
|
@ -307,11 +289,8 @@ class SyncService(context: Context, params: WorkerParameters) : Worker(context,
|
|||
|
||||
private fun getWorkRequest(): OneTimeWorkRequest.Builder {
|
||||
val constraints = Builder()
|
||||
if (isAllowMobileSync) {
|
||||
constraints.setRequiredNetworkType(NetworkType.CONNECTED)
|
||||
} else {
|
||||
constraints.setRequiredNetworkType(NetworkType.UNMETERED)
|
||||
}
|
||||
if (isAllowMobileSync) constraints.setRequiredNetworkType(NetworkType.CONNECTED)
|
||||
else constraints.setRequiredNetworkType(NetworkType.UNMETERED)
|
||||
|
||||
val builder: OneTimeWorkRequest.Builder = OneTimeWorkRequest.Builder(SyncService::class.java)
|
||||
.setConstraints(constraints.build())
|
||||
|
@ -346,8 +325,7 @@ class SyncService(context: Context, params: WorkerParameters) : Worker(context,
|
|||
val workRequest: OneTimeWorkRequest = getWorkRequest()
|
||||
.setInitialDelay(0L, TimeUnit.SECONDS)
|
||||
.build()
|
||||
WorkManager.getInstance(context!!)
|
||||
.enqueueUniqueWork(WORK_ID_SYNC, ExistingWorkPolicy.REPLACE, workRequest)
|
||||
WorkManager.getInstance(context!!).enqueueUniqueWork(WORK_ID_SYNC, ExistingWorkPolicy.REPLACE, workRequest)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -19,9 +19,7 @@ enum class SynchronizationProviderViewData(@JvmField val identifier: String, val
|
|||
@JvmStatic
|
||||
fun fromIdentifier(provider: String): SynchronizationProviderViewData? {
|
||||
for (synchronizationProvider in entries) {
|
||||
if (synchronizationProvider.identifier == provider) {
|
||||
return synchronizationProvider
|
||||
}
|
||||
if (synchronizationProvider.identifier == provider) return synchronizationProvider
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
|
|
@ -40,35 +40,27 @@ object SynchronizationSettings {
|
|||
get() = sharedPreferences.getString(SELECTED_SYNC_PROVIDER, null)
|
||||
|
||||
fun updateLastSynchronizationAttempt() {
|
||||
sharedPreferences.edit()
|
||||
.putLong(LAST_SYNC_ATTEMPT_TIMESTAMP, System.currentTimeMillis())
|
||||
.apply()
|
||||
sharedPreferences.edit().putLong(LAST_SYNC_ATTEMPT_TIMESTAMP, System.currentTimeMillis()).apply()
|
||||
}
|
||||
|
||||
fun setLastSynchronizationAttemptSuccess(isSuccess: Boolean) {
|
||||
sharedPreferences.edit()
|
||||
.putBoolean(LAST_SYNC_ATTEMPT_SUCCESS, isSuccess)
|
||||
.apply()
|
||||
sharedPreferences.edit().putBoolean(LAST_SYNC_ATTEMPT_SUCCESS, isSuccess).apply()
|
||||
}
|
||||
|
||||
val lastSubscriptionSynchronizationTimestamp: Long
|
||||
get() = sharedPreferences.getLong(LAST_SUBSCRIPTION_SYNC_TIMESTAMP, 0)
|
||||
|
||||
fun setLastSubscriptionSynchronizationAttemptTimestamp(newTimeStamp: Long) {
|
||||
sharedPreferences.edit()
|
||||
.putLong(LAST_SUBSCRIPTION_SYNC_TIMESTAMP, newTimeStamp).apply()
|
||||
sharedPreferences.edit().putLong(LAST_SUBSCRIPTION_SYNC_TIMESTAMP, newTimeStamp).apply()
|
||||
}
|
||||
|
||||
val lastEpisodeActionSynchronizationTimestamp: Long
|
||||
get() = sharedPreferences
|
||||
.getLong(LAST_EPISODE_ACTIONS_SYNC_TIMESTAMP, 0)
|
||||
get() = sharedPreferences.getLong(LAST_EPISODE_ACTIONS_SYNC_TIMESTAMP, 0)
|
||||
|
||||
fun setLastEpisodeActionSynchronizationAttemptTimestamp(timestamp: Long) {
|
||||
sharedPreferences.edit()
|
||||
.putLong(LAST_EPISODE_ACTIONS_SYNC_TIMESTAMP, timestamp).apply()
|
||||
sharedPreferences.edit().putLong(LAST_EPISODE_ACTIONS_SYNC_TIMESTAMP, timestamp).apply()
|
||||
}
|
||||
|
||||
private val sharedPreferences: SharedPreferences
|
||||
get() = ClientConfig.applicationCallbacks!!.getApplicationInstance()!!
|
||||
.getSharedPreferences(NAME, Context.MODE_PRIVATE)
|
||||
get() = ClientConfig.applicationCallbacks!!.getApplicationInstance()!!.getSharedPreferences(NAME, Context.MODE_PRIVATE)
|
||||
}
|
||||
|
|
|
@ -29,8 +29,7 @@ import kotlin.math.min
|
|||
* Communicates with the gpodder.net service.
|
||||
*/
|
||||
class GpodnetService(private val httpClient: OkHttpClient, baseHosturl: String?,
|
||||
private val deviceId: String, private var username: String, private var password: String
|
||||
) : ISyncService {
|
||||
private val deviceId: String, private var username: String, private var password: String) : ISyncService {
|
||||
private val baseScheme: String?
|
||||
private val basePort: Int
|
||||
private val baseHost: String?
|
||||
|
@ -62,8 +61,7 @@ class GpodnetService(private val httpClient: OkHttpClient, baseHosturl: String?,
|
|||
query,
|
||||
scaledLogoSize) else String.format("q=%s", query)
|
||||
try {
|
||||
val url = URI(baseScheme, null, baseHost, basePort, "/search.json",
|
||||
parameters, null).toURL()
|
||||
val url = URI(baseScheme, null, baseHost, basePort, "/search.json", parameters, null).toURL()
|
||||
val request: Builder = Builder().url(url)
|
||||
val response = executeRequest(request)
|
||||
|
||||
|
@ -94,8 +92,7 @@ class GpodnetService(private val httpClient: OkHttpClient, baseHosturl: String?,
|
|||
get() {
|
||||
requireLoggedIn()
|
||||
try {
|
||||
val url = URI(baseScheme, null, baseHost, basePort,
|
||||
String.format("/api/2/devices/%s.json", username), null, null).toURL()
|
||||
val url = URI(baseScheme, null, baseHost, basePort, String.format("/api/2/devices/%s.json", username), null, null).toURL()
|
||||
val request: Builder = Builder().url(url)
|
||||
val response = executeRequest(request)
|
||||
val devicesArray = JSONArray(response)
|
||||
|
|
|
@ -44,9 +44,7 @@ object ResponseMapper {
|
|||
for (i in 0 until jsonActions.length()) {
|
||||
val jsonAction = jsonActions.getJSONObject(i)
|
||||
val episodeAction = readFromJsonObject(jsonAction)
|
||||
if (episodeAction != null) {
|
||||
episodeActions.add(episodeAction)
|
||||
}
|
||||
if (episodeAction != null) episodeActions.add(episodeAction)
|
||||
}
|
||||
return EpisodeActionChanges(episodeActions, timestamp)
|
||||
}
|
||||
|
|
|
@ -1,10 +1,6 @@
|
|||
package ac.mdiq.podcini.net.sync.gpoddernet.model
|
||||
|
||||
class GpodnetDevice(val id: String,
|
||||
val caption: String,
|
||||
type: String?,
|
||||
val subscriptions: Int
|
||||
) {
|
||||
class GpodnetDevice(val id: String, val caption: String, type: String?, val subscriptions: Int) {
|
||||
val type: DeviceType
|
||||
|
||||
init {
|
||||
|
@ -12,8 +8,7 @@ class GpodnetDevice(val id: String,
|
|||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return ("GpodnetDevice [id=" + id + ", caption=" + caption + ", type="
|
||||
+ type + ", subscriptions=" + subscriptions + "]")
|
||||
return ("GpodnetDevice [id=" + id + ", caption=" + caption + ", type=" + type + ", subscriptions=" + subscriptions + "]")
|
||||
}
|
||||
|
||||
enum class DeviceType {
|
||||
|
@ -25,9 +20,7 @@ class GpodnetDevice(val id: String,
|
|||
|
||||
companion object {
|
||||
fun fromString(s: String?): DeviceType {
|
||||
if (s == null) {
|
||||
return OTHER
|
||||
}
|
||||
if (s == null) return OTHER
|
||||
|
||||
return when (s) {
|
||||
"desktop" -> DESKTOP
|
||||
|
|
|
@ -7,13 +7,13 @@ import org.apache.commons.lang3.builder.ToStringStyle
|
|||
import org.json.JSONException
|
||||
import org.json.JSONObject
|
||||
|
||||
class GpodnetEpisodeActionPostResponse private constructor(timestamp: Long,
|
||||
/**
|
||||
* URLs that should be updated. The key of the map is the original URL, the value of the map
|
||||
* is the sanitized URL.
|
||||
*/
|
||||
private val updatedUrls: Map<String, String>
|
||||
) : UploadChangesResponse(timestamp) {
|
||||
class GpodnetEpisodeActionPostResponse private constructor(timestamp: Long, private val updatedUrls: Map<String, String>)
|
||||
: UploadChangesResponse(timestamp) {
|
||||
|
||||
override fun toString(): String {
|
||||
return ToStringBuilder.reflectionToString(this, ToStringStyle.SHORT_PREFIX_STYLE)
|
||||
}
|
||||
|
|
|
@ -1,18 +1,9 @@
|
|||
package ac.mdiq.podcini.net.sync.gpoddernet.model
|
||||
|
||||
class GpodnetPodcast(val url: String,
|
||||
val title: String,
|
||||
val description: String,
|
||||
val subscribers: Int,
|
||||
val logoUrl: String,
|
||||
val website: String,
|
||||
val mygpoLink: String,
|
||||
val author: String
|
||||
) {
|
||||
class GpodnetPodcast(val url: String, val title: String, val description: String, val subscribers: Int, val logoUrl: String,
|
||||
val website: String, val mygpoLink: String, val author: String) {
|
||||
|
||||
override fun toString(): String {
|
||||
return ("GpodnetPodcast [url=" + url + ", title=" + title
|
||||
+ ", description=" + description + ", subscribers="
|
||||
+ subscribers + ", logoUrl=" + logoUrl + ", website=" + website
|
||||
+ ", mygpoLink=" + mygpoLink + "]")
|
||||
return ("GpodnetPodcast [url=$url, title=$title, description=$description, subscribers=$subscribers, logoUrl=$logoUrl, website=$website, mygpoLink=$mygpoLink]")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -8,18 +8,14 @@ import org.json.JSONObject
|
|||
/**
|
||||
* Object returned by [GpodnetService] in uploadChanges method.
|
||||
*/
|
||||
class GpodnetUploadChangesResponse(timestamp: Long,
|
||||
/**
|
||||
* URLs that should be updated. The key of the map is the original URL, the value of the map
|
||||
* is the sanitized URL.
|
||||
*/
|
||||
val updatedUrls: Map<String, String>
|
||||
) : UploadChangesResponse(timestamp) {
|
||||
class GpodnetUploadChangesResponse(timestamp: Long, val updatedUrls: Map<String, String>) : UploadChangesResponse(timestamp) {
|
||||
|
||||
override fun toString(): String {
|
||||
return "GpodnetUploadChangesResponse{" +
|
||||
"timestamp=" + timestamp +
|
||||
", updatedUrls=" + updatedUrls +
|
||||
'}'
|
||||
return "GpodnetUploadChangesResponse{timestamp=$timestamp, updatedUrls=$updatedUrls}"
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
|
|
@ -49,12 +49,9 @@ class EpisodeAction private constructor(builder: Builder) {
|
|||
get() = action!!.name.lowercase()
|
||||
|
||||
override fun equals(o: Any?): Boolean {
|
||||
if (this === o) {
|
||||
return true
|
||||
}
|
||||
if (o !is EpisodeAction) {
|
||||
return false
|
||||
}
|
||||
if (this === o) return true
|
||||
|
||||
if (o !is EpisodeAction) return false
|
||||
|
||||
val that = o
|
||||
return started == that.started && position == that.position && total == that.total && action != that.action && podcast == that.podcast && episode == that.episode && timestamp == that.timestamp && guid == that.guid
|
||||
|
@ -100,16 +97,7 @@ class EpisodeAction private constructor(builder: Builder) {
|
|||
}
|
||||
|
||||
override fun toString(): String {
|
||||
return ("EpisodeAction{"
|
||||
+ "podcast='" + podcast + '\''
|
||||
+ ", episode='" + episode + '\''
|
||||
+ ", guid='" + guid + '\''
|
||||
+ ", action=" + action
|
||||
+ ", timestamp=" + timestamp
|
||||
+ ", started=" + started
|
||||
+ ", position=" + position
|
||||
+ ", total=" + total
|
||||
+ '}')
|
||||
return ("EpisodeAction{podcast='$podcast', episode='$episode', guid='$guid', action=$action, timestamp=$timestamp, started=$started, position=$position, total=$total}")
|
||||
}
|
||||
|
||||
enum class Action {
|
||||
|
@ -144,23 +132,17 @@ class EpisodeAction private constructor(builder: Builder) {
|
|||
}
|
||||
|
||||
fun started(seconds: Int): Builder {
|
||||
if (action == Action.PLAY) {
|
||||
this.started = seconds
|
||||
}
|
||||
if (action == Action.PLAY) this.started = seconds
|
||||
return this
|
||||
}
|
||||
|
||||
fun position(seconds: Int): Builder {
|
||||
if (action == Action.PLAY) {
|
||||
this.position = seconds
|
||||
}
|
||||
if (action == Action.PLAY) this.position = seconds
|
||||
return this
|
||||
}
|
||||
|
||||
fun total(seconds: Int): Builder {
|
||||
if (action == Action.PLAY) {
|
||||
this.total = seconds
|
||||
}
|
||||
if (action == Action.PLAY) this.total = seconds
|
||||
return this
|
||||
}
|
||||
|
||||
|
@ -189,9 +171,8 @@ class EpisodeAction private constructor(builder: Builder) {
|
|||
val podcast = `object`.optString("podcast")
|
||||
val episode = `object`.optString("episode")
|
||||
val actionString = `object`.optString("action")
|
||||
if (podcast.isNullOrEmpty() || episode.isNullOrEmpty() || actionString.isNullOrEmpty()) {
|
||||
return null
|
||||
}
|
||||
if (podcast.isNullOrEmpty() || episode.isNullOrEmpty() || actionString.isNullOrEmpty()) return null
|
||||
|
||||
val action: Action
|
||||
try {
|
||||
action = Action.valueOf(actionString.uppercase())
|
||||
|
@ -210,19 +191,13 @@ class EpisodeAction private constructor(builder: Builder) {
|
|||
}
|
||||
}
|
||||
val guid = `object`.optString("guid")
|
||||
if (guid.isNotEmpty()) {
|
||||
builder.guid(guid)
|
||||
}
|
||||
if (guid.isNotEmpty()) builder.guid(guid)
|
||||
|
||||
if (action == Action.PLAY) {
|
||||
val started = `object`.optInt("started", -1)
|
||||
val position = `object`.optInt("position", -1)
|
||||
val total = `object`.optInt("total", -1)
|
||||
if (started >= 0 && position > 0 && total > 0) {
|
||||
builder
|
||||
.started(started)
|
||||
.position(position)
|
||||
.total(total)
|
||||
}
|
||||
if (started >= 0 && position > 0 && total > 0) builder.started(started).position(position).total(total)
|
||||
}
|
||||
return builder.build()
|
||||
}
|
||||
|
|
|
@ -3,9 +3,6 @@ package ac.mdiq.podcini.net.sync.model
|
|||
|
||||
class EpisodeActionChanges(val episodeActions: List<EpisodeAction>, val timestamp: Long) {
|
||||
override fun toString(): String {
|
||||
return ("EpisodeActionGetResponse{"
|
||||
+ "episodeActions=" + episodeActions
|
||||
+ ", timestamp=" + timestamp
|
||||
+ '}')
|
||||
return ("EpisodeActionGetResponse{episodeActions=$episodeActions, timestamp=$timestamp}")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,9 +10,7 @@ interface ISyncService {
|
|||
fun getSubscriptionChanges(lastSync: Long): SubscriptionChanges?
|
||||
|
||||
@Throws(SyncServiceException::class)
|
||||
fun uploadSubscriptionChanges(
|
||||
addedFeeds: List<String?>?, removedFeeds: List<String?>?
|
||||
): UploadChangesResponse?
|
||||
fun uploadSubscriptionChanges(addedFeeds: List<String?>?, removedFeeds: List<String?>?): UploadChangesResponse?
|
||||
|
||||
@Throws(SyncServiceException::class)
|
||||
fun getEpisodeActionChanges(lastSync: Long): EpisodeActionChanges?
|
||||
|
|
|
@ -5,8 +5,6 @@ class SubscriptionChanges(val added: List<String>,
|
|||
val timestamp: Long
|
||||
) {
|
||||
override fun toString(): String {
|
||||
return ("SubscriptionChange [added=" + added.toString()
|
||||
+ ", removed=" + removed.toString() + ", timestamp="
|
||||
+ timestamp + "]")
|
||||
return ("SubscriptionChange [added=$added, removed=$removed, timestamp=$timestamp]")
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
package ac.mdiq.podcini.net.sync.model
|
||||
|
||||
abstract class UploadChangesResponse(
|
||||
/**
|
||||
* timestamp/ID that can be used for requesting changes since this upload.
|
||||
*/
|
||||
@JvmField val timestamp: Long
|
||||
)
|
||||
abstract class UploadChangesResponse(@JvmField val timestamp: Long)
|
||||
|
|
|
@ -21,11 +21,8 @@ import java.net.URI
|
|||
import java.net.URL
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
class NextcloudLoginFlow(private val httpClient: OkHttpClient,
|
||||
private val rawHostUrl: String,
|
||||
private val context: Context,
|
||||
private val callback: AuthenticationCallback
|
||||
) {
|
||||
class NextcloudLoginFlow(private val httpClient: OkHttpClient, private val rawHostUrl: String, private val context: Context,
|
||||
private val callback: AuthenticationCallback) {
|
||||
private val hostname = HostnameParser(rawHostUrl)
|
||||
private var token: String? = null
|
||||
private var endpoint: String? = null
|
||||
|
@ -76,8 +73,7 @@ class NextcloudLoginFlow(private val httpClient: OkHttpClient,
|
|||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe({ result: JSONObject ->
|
||||
callback.onNextcloudAuthenticated(
|
||||
result.getString("server"), result.getString("loginName"), result.getString("appPassword"))
|
||||
callback.onNextcloudAuthenticated(result.getString("server"), result.getString("loginName"), result.getString("appPassword"))
|
||||
},
|
||||
{ error: Throwable ->
|
||||
this.token = null
|
||||
|
@ -87,18 +83,13 @@ class NextcloudLoginFlow(private val httpClient: OkHttpClient,
|
|||
}
|
||||
|
||||
fun cancel() {
|
||||
if (startDisposable != null) {
|
||||
startDisposable!!.dispose()
|
||||
}
|
||||
if (pollDisposable != null) {
|
||||
pollDisposable!!.dispose()
|
||||
}
|
||||
startDisposable?.dispose()
|
||||
pollDisposable?.dispose()
|
||||
}
|
||||
|
||||
@Throws(IOException::class, JSONException::class)
|
||||
private fun doRequest(url: URL, bodyContent: String): JSONObject {
|
||||
val requestBody = RequestBody.create(
|
||||
"application/x-www-form-urlencoded".toMediaType(), bodyContent)
|
||||
val requestBody = RequestBody.create("application/x-www-form-urlencoded".toMediaType(), bodyContent)
|
||||
val request: Request = Builder().url(url).method("POST", requestBody).build()
|
||||
val response = httpClient.newCall(request).execute()
|
||||
if (response.code != 200) {
|
||||
|
@ -118,9 +109,7 @@ class NextcloudLoginFlow(private val httpClient: OkHttpClient,
|
|||
companion object {
|
||||
private const val TAG = "NextcloudLoginFlow"
|
||||
|
||||
fun fromInstanceState(httpClient: OkHttpClient, context: Context,
|
||||
callback: AuthenticationCallback, instanceState: ArrayList<String>
|
||||
): NextcloudLoginFlow {
|
||||
fun fromInstanceState(httpClient: OkHttpClient, context: Context, callback: AuthenticationCallback, instanceState: ArrayList<String>): NextcloudLoginFlow {
|
||||
val flow = NextcloudLoginFlow(httpClient, instanceState[0], context, callback)
|
||||
flow.token = instanceState[1]
|
||||
flow.endpoint = instanceState[2]
|
||||
|
|
|
@ -14,13 +14,11 @@ import java.io.IOException
|
|||
import java.net.MalformedURLException
|
||||
import kotlin.math.min
|
||||
|
||||
class NextcloudSyncService(private val httpClient: OkHttpClient, baseHosturl: String?,
|
||||
private val username: String, private val password: String
|
||||
) : ISyncService {
|
||||
class NextcloudSyncService(private val httpClient: OkHttpClient, baseHosturl: String?, private val username: String, private val password: String)
|
||||
: ISyncService {
|
||||
private val hostname = HostnameParser(baseHosturl)
|
||||
|
||||
override fun login() {
|
||||
}
|
||||
override fun login() {}
|
||||
|
||||
@Throws(SyncServiceException::class)
|
||||
override fun getSubscriptionChanges(lastSync: Long): SubscriptionChanges {
|
||||
|
@ -85,8 +83,7 @@ class NextcloudSyncService(private val httpClient: OkHttpClient, baseHosturl: St
|
|||
override fun uploadEpisodeActions(queuedEpisodeActions: List<EpisodeAction?>?): UploadChangesResponse {
|
||||
var i = 0
|
||||
while (i < queuedEpisodeActions!!.size) {
|
||||
uploadEpisodeActionsPartial(queuedEpisodeActions,
|
||||
i, min(queuedEpisodeActions.size.toDouble(), (i + UPLOAD_BULK_SIZE).toDouble()).toInt())
|
||||
uploadEpisodeActionsPartial(queuedEpisodeActions, i, min(queuedEpisodeActions.size.toDouble(), (i + UPLOAD_BULK_SIZE).toDouble()).toInt())
|
||||
i += UPLOAD_BULK_SIZE
|
||||
}
|
||||
return NextcloudGpodderEpisodeActionPostResponse(System.currentTimeMillis() / 1000)
|
||||
|
@ -99,9 +96,7 @@ class NextcloudSyncService(private val httpClient: OkHttpClient, baseHosturl: St
|
|||
for (i in from until to) {
|
||||
val episodeAction = queuedEpisodeActions!![i]
|
||||
val obj = episodeAction!!.writeToJsonObject()
|
||||
if (obj != null) {
|
||||
list.put(obj)
|
||||
}
|
||||
if (obj != null) list.put(obj)
|
||||
}
|
||||
val url: HttpUrl.Builder = makeUrl("/index.php/apps/gpoddersync/episode_action/create")
|
||||
val requestBody = RequestBody.create("application/json".toMediaType(), list.toString())
|
||||
|
@ -121,9 +116,8 @@ class NextcloudSyncService(private val httpClient: OkHttpClient, baseHosturl: St
|
|||
.method(method, body)
|
||||
.build()
|
||||
val response = httpClient.newCall(request).execute()
|
||||
if (response.code != 200) {
|
||||
throw IOException("Response code: " + response.code)
|
||||
}
|
||||
if (response.code != 200) throw IOException("Response code: " + response.code)
|
||||
|
||||
return response.body!!.string()
|
||||
}
|
||||
|
||||
|
@ -131,14 +125,13 @@ class NextcloudSyncService(private val httpClient: OkHttpClient, baseHosturl: St
|
|||
val builder = HttpUrl.Builder()
|
||||
if (hostname.scheme != null) builder.scheme(hostname.scheme!!)
|
||||
if (hostname.host != null) builder.host(hostname.host!!)
|
||||
return builder.port(hostname.port)
|
||||
.addPathSegments(hostname.subfolder + path)
|
||||
return builder.port(hostname.port).addPathSegments(hostname.subfolder + path)
|
||||
}
|
||||
|
||||
override fun logout() {
|
||||
}
|
||||
override fun logout() {}
|
||||
|
||||
private class NextcloudGpodderEpisodeActionPostResponse(epochSecond: Long) : UploadChangesResponse(epochSecond)
|
||||
|
||||
companion object {
|
||||
private const val UPLOAD_BULK_SIZE = 30
|
||||
}
|
||||
|
|
|
@ -19,9 +19,7 @@ object SynchronizationQueueSink {
|
|||
}
|
||||
|
||||
fun syncNowIfNotSyncedRecently() {
|
||||
if (System.currentTimeMillis() - SynchronizationSettings.lastSyncAttempt > 1000 * 60 * 10) {
|
||||
syncNow()
|
||||
}
|
||||
if (System.currentTimeMillis() - SynchronizationSettings.lastSyncAttempt > 1000 * 60 * 10) syncNow()
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
|
@ -30,9 +28,8 @@ object SynchronizationQueueSink {
|
|||
}
|
||||
|
||||
fun enqueueFeedAddedIfSynchronizationIsActive(context: Context, downloadUrl: String) {
|
||||
if (!SynchronizationSettings.isProviderConnected) {
|
||||
return
|
||||
}
|
||||
if (!SynchronizationSettings.isProviderConnected) return
|
||||
|
||||
LockingAsyncExecutor.executeLockedAsync {
|
||||
SynchronizationQueueStorage(context).enqueueFeedAdded(downloadUrl)
|
||||
syncNow()
|
||||
|
@ -40,9 +37,8 @@ object SynchronizationQueueSink {
|
|||
}
|
||||
|
||||
fun enqueueFeedRemovedIfSynchronizationIsActive(context: Context, downloadUrl: String) {
|
||||
if (!SynchronizationSettings.isProviderConnected) {
|
||||
return
|
||||
}
|
||||
if (!SynchronizationSettings.isProviderConnected) return
|
||||
|
||||
LockingAsyncExecutor.executeLockedAsync {
|
||||
SynchronizationQueueStorage(context).enqueueFeedRemoved(downloadUrl)
|
||||
syncNow()
|
||||
|
@ -50,27 +46,19 @@ object SynchronizationQueueSink {
|
|||
}
|
||||
|
||||
fun enqueueEpisodeActionIfSynchronizationIsActive(context: Context, action: EpisodeAction) {
|
||||
if (!SynchronizationSettings.isProviderConnected) {
|
||||
return
|
||||
}
|
||||
if (!SynchronizationSettings.isProviderConnected) return
|
||||
|
||||
LockingAsyncExecutor.executeLockedAsync {
|
||||
SynchronizationQueueStorage(context).enqueueEpisodeAction(action)
|
||||
syncNow()
|
||||
}
|
||||
}
|
||||
|
||||
fun enqueueEpisodePlayedIfSynchronizationIsActive(context: Context, media: FeedMedia,
|
||||
completed: Boolean
|
||||
) {
|
||||
if (!SynchronizationSettings.isProviderConnected) {
|
||||
return
|
||||
}
|
||||
if (media.item?.feed == null || media.item!!.feed!!.isLocalFeed) {
|
||||
return
|
||||
}
|
||||
if (media.startPosition < 0 || (!completed && media.startPosition >= media.getPosition())) {
|
||||
return
|
||||
}
|
||||
fun enqueueEpisodePlayedIfSynchronizationIsActive(context: Context, media: FeedMedia, completed: Boolean) {
|
||||
if (!SynchronizationSettings.isProviderConnected) return
|
||||
if (media.item?.feed == null || media.item!!.feed!!.isLocalFeed) return
|
||||
if (media.startPosition < 0 || (!completed && media.startPosition >= media.getPosition())) return
|
||||
|
||||
val action = EpisodeAction.Builder(media.item!!, EpisodeAction.PLAY)
|
||||
.currentTimestamp()
|
||||
.started(media.startPosition / 1000)
|
||||
|
|
|
@ -14,8 +14,7 @@ class SynchronizationQueueStorage(context: Context) {
|
|||
get() {
|
||||
val actions = ArrayList<EpisodeAction>()
|
||||
try {
|
||||
val json = sharedPreferences
|
||||
.getString(QUEUED_EPISODE_ACTIONS, "[]")
|
||||
val json = sharedPreferences.getString(QUEUED_EPISODE_ACTIONS, "[]")
|
||||
val queue = JSONArray(json)
|
||||
for (i in 0 until queue.length()) {
|
||||
val act = EpisodeAction.readFromJsonObject(queue.getJSONObject(i))?: continue
|
||||
|
@ -31,8 +30,7 @@ class SynchronizationQueueStorage(context: Context) {
|
|||
get() {
|
||||
val removedFeedUrls = ArrayList<String>()
|
||||
try {
|
||||
val json = sharedPreferences
|
||||
.getString(QUEUED_FEEDS_REMOVED, "[]")
|
||||
val json = sharedPreferences.getString(QUEUED_FEEDS_REMOVED, "[]")
|
||||
val queue = JSONArray(json)
|
||||
for (i in 0 until queue.length()) {
|
||||
removedFeedUrls.add(queue.getString(i))
|
||||
|
@ -47,8 +45,7 @@ class SynchronizationQueueStorage(context: Context) {
|
|||
get() {
|
||||
val addedFeedUrls = ArrayList<String>()
|
||||
try {
|
||||
val json = sharedPreferences
|
||||
.getString(QUEUED_FEEDS_ADDED, "[]")
|
||||
val json = sharedPreferences.getString(QUEUED_FEEDS_ADDED, "[]")
|
||||
val queue = JSONArray(json)
|
||||
for (i in 0 until queue.length()) {
|
||||
addedFeedUrls.add(queue.getString(i))
|
||||
|
@ -60,8 +57,7 @@ class SynchronizationQueueStorage(context: Context) {
|
|||
}
|
||||
|
||||
fun clearEpisodeActionQueue() {
|
||||
sharedPreferences.edit()
|
||||
.putString(QUEUED_EPISODE_ACTIONS, "[]").apply()
|
||||
sharedPreferences.edit().putString(QUEUED_EPISODE_ACTIONS, "[]").apply()
|
||||
}
|
||||
|
||||
fun clearFeedQueues() {
|
||||
|
@ -115,9 +111,7 @@ class SynchronizationQueueStorage(context: Context) {
|
|||
private fun indexOf(string: String, array: JSONArray): Int {
|
||||
try {
|
||||
for (i in 0 until array.length()) {
|
||||
if (array.getString(i) == string) {
|
||||
return i
|
||||
}
|
||||
if (array.getString(i) == string) return i
|
||||
}
|
||||
} catch (jsonException: JSONException) {
|
||||
jsonException.printStackTrace()
|
||||
|
@ -131,9 +125,7 @@ class SynchronizationQueueStorage(context: Context) {
|
|||
try {
|
||||
val queue = JSONArray(json)
|
||||
queue.put(action.writeToJsonObject())
|
||||
sharedPreferences.edit().putString(
|
||||
QUEUED_EPISODE_ACTIONS, queue.toString()
|
||||
).apply()
|
||||
sharedPreferences.edit().putString(QUEUED_EPISODE_ACTIONS, queue.toString()).apply()
|
||||
} catch (jsonException: JSONException) {
|
||||
jsonException.printStackTrace()
|
||||
}
|
||||
|
|
|
@ -25,9 +25,9 @@ object PlayableUtils {
|
|||
val item = playable.item
|
||||
if (item != null && item.isNew) DBWriter.markItemPlayed(FeedItem.UNPLAYED, item.id)
|
||||
|
||||
if (playable.startPosition >= 0 && playable.getPosition() > playable.startPosition) {
|
||||
if (playable.startPosition >= 0 && playable.getPosition() > playable.startPosition)
|
||||
playable.playedDuration = (playable.playedDurationWhenStarted + playable.getPosition() - playable.startPosition)
|
||||
}
|
||||
|
||||
DBWriter.setFeedMediaPlaybackInformation(playable)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -41,6 +41,7 @@ abstract class PlaybackController(private val activity: FragmentActivity) {
|
|||
private var released = false
|
||||
private var initialized = false
|
||||
private var eventsRegistered = false
|
||||
|
||||
private var loadedFeedMedia: Long = -1
|
||||
|
||||
/**
|
||||
|
@ -109,7 +110,7 @@ abstract class PlaybackController(private val activity: FragmentActivity) {
|
|||
}
|
||||
|
||||
fun isPlaybackServiceReady() : Boolean {
|
||||
return playbackService != null
|
||||
return playbackService != null && playbackService!!.isServiceReady()
|
||||
}
|
||||
|
||||
private fun unbind() {
|
||||
|
@ -311,9 +312,7 @@ abstract class PlaybackController(private val activity: FragmentActivity) {
|
|||
|
||||
fun extendSleepTimer(extendTime: Long) {
|
||||
val timeLeft = sleepTimerTimeLeft
|
||||
if (playbackService != null && timeLeft != Playable.INVALID_TIME.toLong()) {
|
||||
setSleepTimer(timeLeft + extendTime)
|
||||
}
|
||||
if (playbackService != null && timeLeft != Playable.INVALID_TIME.toLong()) setSleepTimer(timeLeft + extendTime)
|
||||
}
|
||||
|
||||
fun setSleepTimer(time: Long) {
|
||||
|
@ -324,11 +323,13 @@ abstract class PlaybackController(private val activity: FragmentActivity) {
|
|||
if (playbackService != null) {
|
||||
playbackService!!.seekTo(time)
|
||||
} else {
|
||||
val media = getMedia()
|
||||
// val media = getMedia()
|
||||
if (media == null) return
|
||||
PlaybackServiceStarter(activity, media!!).start()
|
||||
if (media is FeedMedia) {
|
||||
media.setPosition(time)
|
||||
DBWriter.setFeedItem(media.item)
|
||||
EventBus.getDefault().post(PlaybackPositionEvent(time, media.getDuration()))
|
||||
media!!.setPosition(time)
|
||||
DBWriter.setFeedItem((media as FeedMedia).item)
|
||||
EventBus.getDefault().post(PlaybackPositionEvent(time, media!!.getDuration()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -291,8 +291,7 @@ abstract class PlaybackServiceMediaPlayer protected constructor(protected val co
|
|||
protected fun acquireWifiLockIfNecessary() {
|
||||
if (shouldLockWifi()) {
|
||||
if (wifiLock == null) {
|
||||
wifiLock = (context.applicationContext.getSystemService(Context.WIFI_SERVICE) as WifiManager)
|
||||
.createWifiLock(WifiManager.WIFI_MODE_FULL, TAG)
|
||||
wifiLock = (context.applicationContext.getSystemService(Context.WIFI_SERVICE) as WifiManager).createWifiLock(WifiManager.WIFI_MODE_FULL, TAG)
|
||||
wifiLock?.setReferenceCounted(false)
|
||||
}
|
||||
wifiLock?.acquire()
|
||||
|
@ -301,9 +300,7 @@ abstract class PlaybackServiceMediaPlayer protected constructor(protected val co
|
|||
|
||||
@Synchronized
|
||||
protected fun releaseWifiLockIfNecessary() {
|
||||
if (wifiLock != null && wifiLock!!.isHeld) {
|
||||
wifiLock!!.release()
|
||||
}
|
||||
if (wifiLock != null && wifiLock!!.isHeld) wifiLock!!.release()
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -361,7 +358,7 @@ abstract class PlaybackServiceMediaPlayer protected constructor(protected val co
|
|||
|
||||
fun onMediaChanged(reloadUI: Boolean)
|
||||
|
||||
fun onPostPlayback(media: Playable, ended: Boolean, skipped: Boolean, playingNext: Boolean)
|
||||
fun onPostPlayback(media: Playable?, ended: Boolean, skipped: Boolean, playingNext: Boolean)
|
||||
|
||||
fun onPlaybackStart(playable: Playable, position: Int)
|
||||
|
||||
|
|
|
@ -40,7 +40,6 @@ import java.util.concurrent.TimeUnit
|
|||
|
||||
@UnstableApi
|
||||
class ExoPlayerWrapper internal constructor(private val context: Context) {
|
||||
// TODO: need to experiment this, 5 seconds for now
|
||||
private val bufferUpdateInterval = 5L
|
||||
|
||||
private val bufferingUpdateDisposable: Disposable
|
||||
|
|
|
@ -46,13 +46,13 @@ class LocalPSMP(context: Context, callback: PSMPCallback) : PlaybackServiceMedia
|
|||
private var statusBeforeSeeking: PlayerStatus? = null
|
||||
|
||||
@Volatile
|
||||
private var mediaPlayer: ExoPlayerWrapper? = null
|
||||
private var playerWrapper: ExoPlayerWrapper? = null
|
||||
|
||||
@Volatile
|
||||
private var media: Playable? = null
|
||||
private var playable: Playable? = null
|
||||
|
||||
@Volatile
|
||||
private var stream = false
|
||||
private var isStreaming = false
|
||||
|
||||
@Volatile
|
||||
private var mediaType: MediaType
|
||||
|
@ -118,21 +118,21 @@ class LocalPSMP(context: Context, callback: PSMPCallback) : PlaybackServiceMedia
|
|||
* @see .playMediaObject
|
||||
*/
|
||||
private fun playMediaObject(playable: Playable, forceReset: Boolean, stream: Boolean, startWhenPrepared: Boolean, prepareImmediately: Boolean) {
|
||||
if (media != null) {
|
||||
if (!forceReset && media!!.getIdentifier() == playable.getIdentifier() && playerStatus == PlayerStatus.PLAYING) {
|
||||
if (this.playable != null) {
|
||||
if (!forceReset && this.playable!!.getIdentifier() == playable.getIdentifier() && playerStatus == PlayerStatus.PLAYING) {
|
||||
// episode is already playing -> ignore method call
|
||||
Log.d(TAG, "Method call to playMediaObject was ignored: media file already playing.")
|
||||
return
|
||||
} else {
|
||||
// stop playback of this episode
|
||||
if (playerStatus == PlayerStatus.PAUSED || playerStatus == PlayerStatus.PLAYING || playerStatus == PlayerStatus.PREPARED)
|
||||
mediaPlayer?.stop()
|
||||
playerWrapper?.stop()
|
||||
|
||||
// set temporarily to pause in order to update list with current position
|
||||
if (playerStatus == PlayerStatus.PLAYING) callback.onPlaybackPause(media, getPosition())
|
||||
if (playerStatus == PlayerStatus.PLAYING) callback.onPlaybackPause(this.playable, getPosition())
|
||||
|
||||
if (media!!.getIdentifier() != playable.getIdentifier()) {
|
||||
val oldMedia: Playable = media!!
|
||||
if (this.playable!!.getIdentifier() != playable.getIdentifier()) {
|
||||
val oldMedia: Playable = this.playable!!
|
||||
callback.onPostPlayback(oldMedia, ended = false, skipped = false, true)
|
||||
}
|
||||
|
||||
|
@ -140,39 +140,40 @@ class LocalPSMP(context: Context, callback: PSMPCallback) : PlaybackServiceMedia
|
|||
}
|
||||
}
|
||||
|
||||
media = playable
|
||||
this.stream = stream
|
||||
mediaType = media!!.getMediaType()
|
||||
this.playable = playable
|
||||
this.isStreaming = stream
|
||||
mediaType = this.playable!!.getMediaType()
|
||||
videoSize = null
|
||||
createMediaPlayer()
|
||||
this@LocalPSMP.startWhenPrepared.set(startWhenPrepared)
|
||||
setPlayerStatus(PlayerStatus.INITIALIZING, media)
|
||||
setPlayerStatus(PlayerStatus.INITIALIZING, this.playable)
|
||||
try {
|
||||
callback.ensureMediaInfoLoaded(media!!)
|
||||
callback.ensureMediaInfoLoaded(this.playable!!)
|
||||
callback.onMediaChanged(false)
|
||||
setPlaybackParams(PlaybackSpeedUtils.getCurrentPlaybackSpeed(media), UserPreferences.isSkipSilence)
|
||||
setPlaybackParams(PlaybackSpeedUtils.getCurrentPlaybackSpeed(this.playable), UserPreferences.isSkipSilence)
|
||||
when {
|
||||
stream -> {
|
||||
val streamurl = media!!.getStreamUrl()
|
||||
val streamurl = this.playable!!.getStreamUrl()
|
||||
if (streamurl != null) {
|
||||
if (playable is FeedMedia && playable.item?.feed?.preferences != null) {
|
||||
val preferences = playable.item!!.feed!!.preferences!!
|
||||
mediaPlayer?.setDataSource(streamurl, preferences.username, preferences.password)
|
||||
} else mediaPlayer?.setDataSource(streamurl)
|
||||
playerWrapper?.setDataSource(streamurl, preferences.username, preferences.password)
|
||||
} else playerWrapper?.setDataSource(streamurl)
|
||||
}
|
||||
}
|
||||
else -> {
|
||||
val localMediaurl = media!!.getLocalMediaUrl()
|
||||
if (localMediaurl != null && File(localMediaurl).canRead()) mediaPlayer?.setDataSource(localMediaurl)
|
||||
val localMediaurl = this.playable!!.getLocalMediaUrl()
|
||||
if (localMediaurl != null && File(localMediaurl).canRead()) playerWrapper?.setDataSource(localMediaurl)
|
||||
else throw IOException("Unable to read local file $localMediaurl")
|
||||
}
|
||||
}
|
||||
val uiModeManager = context.getSystemService(Context.UI_MODE_SERVICE) as UiModeManager
|
||||
if (uiModeManager.currentModeType != Configuration.UI_MODE_TYPE_CAR) setPlayerStatus(PlayerStatus.INITIALIZED, media)
|
||||
if (uiModeManager.currentModeType != Configuration.UI_MODE_TYPE_CAR) setPlayerStatus(PlayerStatus.INITIALIZED,
|
||||
this.playable)
|
||||
|
||||
if (prepareImmediately) {
|
||||
setPlayerStatus(PlayerStatus.PREPARING, media)
|
||||
mediaPlayer?.prepare()
|
||||
setPlayerStatus(PlayerStatus.PREPARING, this.playable)
|
||||
playerWrapper?.prepare()
|
||||
onPrepared(startWhenPrepared)
|
||||
}
|
||||
} catch (e: IOException) {
|
||||
|
@ -201,23 +202,19 @@ class LocalPSMP(context: Context, callback: PSMPCallback) : PlaybackServiceMedia
|
|||
Log.d(TAG, "Audiofocus successfully requested")
|
||||
Log.d(TAG, "Resuming/Starting playback")
|
||||
acquireWifiLockIfNecessary()
|
||||
setPlaybackParams(PlaybackSpeedUtils.getCurrentPlaybackSpeed(media), UserPreferences.isSkipSilence)
|
||||
setPlaybackParams(PlaybackSpeedUtils.getCurrentPlaybackSpeed(playable), UserPreferences.isSkipSilence)
|
||||
setVolume(1.0f, 1.0f)
|
||||
|
||||
if (media != null && playerStatus == PlayerStatus.PREPARED && media!!.getPosition() > 0) {
|
||||
val newPosition = RewindAfterPauseUtils.calculatePositionWithRewind(media!!.getPosition(), media!!.getLastPlayedTime())
|
||||
if (playable != null && playerStatus == PlayerStatus.PREPARED && playable!!.getPosition() > 0) {
|
||||
val newPosition = RewindAfterPauseUtils.calculatePositionWithRewind(playable!!.getPosition(), playable!!.getLastPlayedTime())
|
||||
seekTo(newPosition)
|
||||
}
|
||||
mediaPlayer?.start()
|
||||
playerWrapper?.start()
|
||||
|
||||
setPlayerStatus(PlayerStatus.PLAYING, media)
|
||||
setPlayerStatus(PlayerStatus.PLAYING, playable)
|
||||
pausedBecauseOfTransientAudiofocusLoss = false
|
||||
} else {
|
||||
Log.e(TAG, "Failed to request audio focus")
|
||||
}
|
||||
} else {
|
||||
Log.d(TAG, "Call to resume() was ignored because current state of PSMP object is $playerStatus")
|
||||
}
|
||||
} else Log.e(TAG, "Failed to request audio focus")
|
||||
} else Log.d(TAG, "Call to resume() was ignored because current state of PSMP object is $playerStatus")
|
||||
}
|
||||
|
||||
|
||||
|
@ -236,14 +233,14 @@ class LocalPSMP(context: Context, callback: PSMPCallback) : PlaybackServiceMedia
|
|||
releaseWifiLockIfNecessary()
|
||||
if (playerStatus == PlayerStatus.PLAYING) {
|
||||
Log.d(TAG, "Pausing playback.")
|
||||
mediaPlayer?.pause()
|
||||
setPlayerStatus(PlayerStatus.PAUSED, media, getPosition())
|
||||
playerWrapper?.pause()
|
||||
setPlayerStatus(PlayerStatus.PAUSED, playable, getPosition())
|
||||
|
||||
if (abandonFocus) {
|
||||
abandonAudioFocus()
|
||||
pausedBecauseOfTransientAudiofocusLoss = false
|
||||
}
|
||||
if (stream && reinit) reinit()
|
||||
if (isStreaming && reinit) reinit()
|
||||
} else {
|
||||
Log.d(TAG, "Ignoring call to pause: Player is in $playerStatus state")
|
||||
}
|
||||
|
@ -263,8 +260,8 @@ class LocalPSMP(context: Context, callback: PSMPCallback) : PlaybackServiceMedia
|
|||
override fun prepare() {
|
||||
if (playerStatus == PlayerStatus.INITIALIZED) {
|
||||
Log.d(TAG, "Preparing media player")
|
||||
setPlayerStatus(PlayerStatus.PREPARING, media)
|
||||
mediaPlayer?.prepare()
|
||||
setPlayerStatus(PlayerStatus.PREPARING, playable)
|
||||
playerWrapper?.prepare()
|
||||
onPrepared(startWhenPrepared.get())
|
||||
}
|
||||
}
|
||||
|
@ -276,18 +273,18 @@ class LocalPSMP(context: Context, callback: PSMPCallback) : PlaybackServiceMedia
|
|||
check(playerStatus == PlayerStatus.PREPARING) { "Player is not in PREPARING state" }
|
||||
Log.d(TAG, "Resource prepared")
|
||||
|
||||
if (mediaPlayer != null && mediaType == MediaType.VIDEO) videoSize = Pair(mediaPlayer!!.videoWidth, mediaPlayer!!.videoHeight)
|
||||
if (playerWrapper != null && mediaType == MediaType.VIDEO) videoSize = Pair(playerWrapper!!.videoWidth, playerWrapper!!.videoHeight)
|
||||
|
||||
if (media != null) {
|
||||
if (playable != null) {
|
||||
// TODO this call has no effect!
|
||||
if (media!!.getPosition() > 0) seekTo(media!!.getPosition())
|
||||
if (playable!!.getPosition() > 0) seekTo(playable!!.getPosition())
|
||||
|
||||
if (media!!.getDuration() <= 0) {
|
||||
if (playable!!.getDuration() <= 0) {
|
||||
Log.d(TAG, "Setting duration of media")
|
||||
if (mediaPlayer != null) media!!.setDuration(mediaPlayer!!.duration)
|
||||
if (playerWrapper != null) playable!!.setDuration(playerWrapper!!.duration)
|
||||
}
|
||||
}
|
||||
setPlayerStatus(PlayerStatus.PREPARED, media)
|
||||
setPlayerStatus(PlayerStatus.PREPARED, playable)
|
||||
|
||||
if (startWhenPrepared) resume()
|
||||
}
|
||||
|
@ -302,8 +299,8 @@ class LocalPSMP(context: Context, callback: PSMPCallback) : PlaybackServiceMedia
|
|||
Log.d(TAG, "reinit()")
|
||||
releaseWifiLockIfNecessary()
|
||||
when {
|
||||
media != null -> playMediaObject(media!!, true, stream, startWhenPrepared.get(), false)
|
||||
mediaPlayer != null -> mediaPlayer!!.reset()
|
||||
playable != null -> playMediaObject(playable!!, true, isStreaming, startWhenPrepared.get(), false)
|
||||
playerWrapper != null -> playerWrapper!!.reset()
|
||||
else -> Log.d(TAG, "Call to reinit was ignored: media and mediaPlayer were null")
|
||||
}
|
||||
}
|
||||
|
@ -336,9 +333,9 @@ class LocalPSMP(context: Context, callback: PSMPCallback) : PlaybackServiceMedia
|
|||
}
|
||||
seekLatch = CountDownLatch(1)
|
||||
statusBeforeSeeking = playerStatus
|
||||
setPlayerStatus(PlayerStatus.SEEKING, media, getPosition())
|
||||
mediaPlayer?.seekTo(t)
|
||||
if (statusBeforeSeeking == PlayerStatus.PREPARED) media?.setPosition(t)
|
||||
setPlayerStatus(PlayerStatus.SEEKING, playable, getPosition())
|
||||
playerWrapper?.seekTo(t)
|
||||
if (statusBeforeSeeking == PlayerStatus.PREPARED) playable?.setPosition(t)
|
||||
try {
|
||||
seekLatch!!.await(3, TimeUnit.SECONDS)
|
||||
} catch (e: InterruptedException) {
|
||||
|
@ -346,7 +343,7 @@ class LocalPSMP(context: Context, callback: PSMPCallback) : PlaybackServiceMedia
|
|||
}
|
||||
}
|
||||
PlayerStatus.INITIALIZED -> {
|
||||
media?.setPosition(t)
|
||||
playable?.setPosition(t)
|
||||
startWhenPrepared.set(false)
|
||||
prepare()
|
||||
}
|
||||
|
@ -361,11 +358,8 @@ class LocalPSMP(context: Context, callback: PSMPCallback) : PlaybackServiceMedia
|
|||
*/
|
||||
override fun seekDelta(d: Int) {
|
||||
val currentPosition = getPosition()
|
||||
if (currentPosition != Playable.INVALID_TIME) {
|
||||
seekTo(currentPosition + d)
|
||||
} else {
|
||||
Log.e(TAG, "getPosition() returned INVALID_TIME in seekDelta")
|
||||
}
|
||||
if (currentPosition != Playable.INVALID_TIME) seekTo(currentPosition + d)
|
||||
else Log.e(TAG, "getPosition() returned INVALID_TIME in seekDelta")
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -374,10 +368,9 @@ class LocalPSMP(context: Context, callback: PSMPCallback) : PlaybackServiceMedia
|
|||
override fun getDuration(): Int {
|
||||
var retVal = Playable.INVALID_TIME
|
||||
if (playerStatus == PlayerStatus.PLAYING || playerStatus == PlayerStatus.PAUSED || playerStatus == PlayerStatus.PREPARED) {
|
||||
if (mediaPlayer != null) retVal = mediaPlayer!!.duration
|
||||
if (playerWrapper != null) retVal = playerWrapper!!.duration
|
||||
}
|
||||
if (retVal <= 0 && media != null && media!!.getDuration() > 0) retVal = media!!.getDuration()
|
||||
|
||||
if (retVal <= 0 && playable != null && playable!!.getDuration() > 0) retVal = playable!!.getDuration()
|
||||
return retVal
|
||||
}
|
||||
|
||||
|
@ -387,10 +380,9 @@ class LocalPSMP(context: Context, callback: PSMPCallback) : PlaybackServiceMedia
|
|||
override fun getPosition(): Int {
|
||||
var retVal = Playable.INVALID_TIME
|
||||
if (playerStatus.isAtLeast(PlayerStatus.PREPARED)) {
|
||||
if (mediaPlayer != null) retVal = mediaPlayer!!.currentPosition
|
||||
if (playerWrapper != null) retVal = playerWrapper!!.currentPosition
|
||||
}
|
||||
if (retVal <= 0 && media != null && media!!.getPosition() >= 0) retVal = media!!.getPosition()
|
||||
|
||||
if (retVal <= 0 && playable != null && playable!!.getPosition() >= 0) retVal = playable!!.getPosition()
|
||||
return retVal
|
||||
}
|
||||
|
||||
|
@ -409,7 +401,7 @@ class LocalPSMP(context: Context, callback: PSMPCallback) : PlaybackServiceMedia
|
|||
override fun setPlaybackParams(speed: Float, skipSilence: Boolean) {
|
||||
Log.d(TAG, "Playback speed was set to $speed")
|
||||
EventBus.getDefault().post(SpeedChangedEvent(speed))
|
||||
mediaPlayer?.setPlaybackParams(speed, skipSilence)
|
||||
playerWrapper?.setPlaybackParams(speed, skipSilence)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -418,7 +410,7 @@ class LocalPSMP(context: Context, callback: PSMPCallback) : PlaybackServiceMedia
|
|||
override fun getPlaybackSpeed(): Float {
|
||||
var retVal = 1f
|
||||
if (playerStatus == PlayerStatus.PLAYING || playerStatus == PlayerStatus.PAUSED || playerStatus == PlayerStatus.INITIALIZED || playerStatus == PlayerStatus.PREPARED) {
|
||||
if (mediaPlayer != null) retVal = mediaPlayer!!.currentSpeedMultiplier
|
||||
if (playerWrapper != null) retVal = playerWrapper!!.currentSpeedMultiplier
|
||||
}
|
||||
return retVal
|
||||
}
|
||||
|
@ -440,7 +432,7 @@ class LocalPSMP(context: Context, callback: PSMPCallback) : PlaybackServiceMedia
|
|||
volumeRight *= adaptionFactor
|
||||
}
|
||||
}
|
||||
mediaPlayer?.setVolume(volumeLeft, volumeRight)
|
||||
playerWrapper?.setVolume(volumeLeft, volumeRight)
|
||||
Log.d(TAG, "Media player volume was set to $volumeLeft $volumeRight")
|
||||
}
|
||||
|
||||
|
@ -449,22 +441,22 @@ class LocalPSMP(context: Context, callback: PSMPCallback) : PlaybackServiceMedia
|
|||
}
|
||||
|
||||
override fun isStreaming(): Boolean {
|
||||
return stream
|
||||
return isStreaming
|
||||
}
|
||||
|
||||
/**
|
||||
* Releases internally used resources. This method should only be called when the object is not used anymore.
|
||||
*/
|
||||
override fun shutdown() {
|
||||
if (mediaPlayer != null) {
|
||||
if (playerWrapper != null) {
|
||||
try {
|
||||
clearMediaPlayerListeners()
|
||||
if (mediaPlayer!!.isPlaying) mediaPlayer!!.stop()
|
||||
if (playerWrapper!!.isPlaying) playerWrapper!!.stop()
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
mediaPlayer!!.release()
|
||||
mediaPlayer = null
|
||||
playerWrapper!!.release()
|
||||
playerWrapper = null
|
||||
playerStatus = PlayerStatus.STOPPED
|
||||
}
|
||||
isShutDown = true
|
||||
|
@ -473,13 +465,13 @@ class LocalPSMP(context: Context, callback: PSMPCallback) : PlaybackServiceMedia
|
|||
}
|
||||
|
||||
override fun setVideoSurface(surface: SurfaceHolder?) {
|
||||
mediaPlayer?.setDisplay(surface)
|
||||
playerWrapper?.setDisplay(surface)
|
||||
}
|
||||
|
||||
override fun resetVideoSurface() {
|
||||
if (mediaType == MediaType.VIDEO) {
|
||||
Log.d(TAG, "Resetting video surface")
|
||||
mediaPlayer?.setDisplay(null)
|
||||
playerWrapper?.setDisplay(null)
|
||||
reinit()
|
||||
} else {
|
||||
Log.e(TAG, "Resetting video surface for media of Audio type")
|
||||
|
@ -494,9 +486,8 @@ class LocalPSMP(context: Context, callback: PSMPCallback) : PlaybackServiceMedia
|
|||
* invalid values.
|
||||
*/
|
||||
override fun getVideoSize(): Pair<Int, Int>? {
|
||||
if (mediaPlayer != null && playerStatus != PlayerStatus.ERROR && mediaType == MediaType.VIDEO)
|
||||
videoSize = Pair(mediaPlayer!!.videoWidth, mediaPlayer!!.videoHeight)
|
||||
|
||||
if (playerWrapper != null && playerStatus != PlayerStatus.ERROR && mediaType == MediaType.VIDEO)
|
||||
videoSize = Pair(playerWrapper!!.videoWidth, playerWrapper!!.videoHeight)
|
||||
return videoSize
|
||||
}
|
||||
|
||||
|
@ -507,37 +498,37 @@ class LocalPSMP(context: Context, callback: PSMPCallback) : PlaybackServiceMedia
|
|||
* @return the current media. May be null
|
||||
*/
|
||||
override fun getPlayable(): Playable? {
|
||||
return media
|
||||
return playable
|
||||
}
|
||||
|
||||
override fun setPlayable(playable: Playable?) {
|
||||
media = playable
|
||||
this.playable = playable
|
||||
}
|
||||
|
||||
override fun getAudioTracks(): List<String> {
|
||||
return mediaPlayer?.audioTracks?: listOf()
|
||||
return playerWrapper?.audioTracks?: listOf()
|
||||
}
|
||||
|
||||
override fun setAudioTrack(track: Int) {
|
||||
if (mediaPlayer != null) mediaPlayer!!.setAudioTrack(track)
|
||||
if (playerWrapper != null) playerWrapper!!.setAudioTrack(track)
|
||||
}
|
||||
|
||||
override fun getSelectedAudioTrack(): Int {
|
||||
return mediaPlayer?.selectedAudioTrack?:0
|
||||
return playerWrapper?.selectedAudioTrack?:0
|
||||
}
|
||||
|
||||
override fun createMediaPlayer() {
|
||||
mediaPlayer?.release()
|
||||
playerWrapper?.release()
|
||||
|
||||
if (media == null) {
|
||||
mediaPlayer = null
|
||||
if (playable == null) {
|
||||
playerWrapper = null
|
||||
playerStatus = PlayerStatus.STOPPED
|
||||
return
|
||||
}
|
||||
|
||||
mediaPlayer = ExoPlayerWrapper(context)
|
||||
mediaPlayer!!.setAudioStreamType(AudioManager.STREAM_MUSIC)
|
||||
setMediaPlayerListeners(mediaPlayer)
|
||||
playerWrapper = ExoPlayerWrapper(context)
|
||||
playerWrapper!!.setAudioStreamType(AudioManager.STREAM_MUSIC)
|
||||
setMediaPlayerListeners(playerWrapper)
|
||||
}
|
||||
|
||||
private val audioFocusChangeListener = OnAudioFocusChangeListener { focusChange ->
|
||||
|
@ -552,7 +543,7 @@ class LocalPSMP(context: Context, callback: PSMPCallback) : PlaybackServiceMedia
|
|||
focusChange == AudioManager.AUDIOFOCUS_LOSS -> {
|
||||
Log.d(TAG, "Lost audio focus")
|
||||
pause(true, reinit = false)
|
||||
callback.shouldStop()
|
||||
// callback.shouldStop()
|
||||
}
|
||||
focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK && !UserPreferences.shouldPauseForFocusLoss() -> {
|
||||
if (playerStatus == PlayerStatus.PLAYING) {
|
||||
|
@ -564,20 +555,19 @@ class LocalPSMP(context: Context, callback: PSMPCallback) : PlaybackServiceMedia
|
|||
focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT || focusChange == AudioManager.AUDIOFOCUS_LOSS_TRANSIENT_CAN_DUCK -> {
|
||||
if (playerStatus == PlayerStatus.PLAYING) {
|
||||
Log.d(TAG, "Lost audio focus temporarily. Pausing...")
|
||||
mediaPlayer?.pause() // Pause without telling the PlaybackService
|
||||
playerWrapper?.pause() // Pause without telling the PlaybackService
|
||||
pausedBecauseOfTransientAudiofocusLoss = true
|
||||
|
||||
audioFocusCanceller.removeCallbacksAndMessages(null)
|
||||
audioFocusCanceller.postDelayed({
|
||||
// Still did not get back the audio focus. Now actually pause.
|
||||
if (pausedBecauseOfTransientAudiofocusLoss) pause(abandonFocus = true, reinit = false)
|
||||
}, 30000)
|
||||
audioFocusCanceller.postDelayed({ if (pausedBecauseOfTransientAudiofocusLoss) pause(abandonFocus = true, reinit = false) },
|
||||
30000)
|
||||
}
|
||||
}
|
||||
focusChange == AudioManager.AUDIOFOCUS_GAIN -> {
|
||||
Log.d(TAG, "Gained audio focus")
|
||||
audioFocusCanceller.removeCallbacksAndMessages(null)
|
||||
if (pausedBecauseOfTransientAudiofocusLoss) mediaPlayer?.start() // we paused => play now
|
||||
if (pausedBecauseOfTransientAudiofocusLoss) playerWrapper?.start() // we paused => play now
|
||||
else setVolume(1.0f, 1.0f) // we ducked => raise audio level back
|
||||
|
||||
pausedBecauseOfTransientAudiofocusLoss = false
|
||||
|
@ -606,13 +596,13 @@ class LocalPSMP(context: Context, callback: PSMPCallback) : PlaybackServiceMedia
|
|||
|
||||
// we're relying on the position stored in the Playable object for post-playback processing
|
||||
val position = getPosition()
|
||||
if (position >= 0) media?.setPosition(position)
|
||||
if (position >= 0) playable?.setPosition(position)
|
||||
|
||||
mediaPlayer?.reset()
|
||||
playerWrapper?.reset()
|
||||
|
||||
abandonAudioFocus()
|
||||
|
||||
val currentMedia = media
|
||||
val currentMedia = playable
|
||||
var nextMedia: Playable? = null
|
||||
|
||||
if (shouldContinue) {
|
||||
|
@ -624,7 +614,7 @@ class LocalPSMP(context: Context, callback: PSMPCallback) : PlaybackServiceMedia
|
|||
callback.onPlaybackEnded(nextMedia.getMediaType(), false)
|
||||
// setting media to null signals to playMediaObject() that
|
||||
// we're taking care of post-playback processing
|
||||
media = null
|
||||
playable = null
|
||||
playMediaObject(nextMedia, false, !nextMedia.localFileAvailable(), isPlaying, isPlaying)
|
||||
}
|
||||
}
|
||||
|
@ -635,12 +625,9 @@ class LocalPSMP(context: Context, callback: PSMPCallback) : PlaybackServiceMedia
|
|||
stop()
|
||||
}
|
||||
val hasNext = nextMedia != null
|
||||
|
||||
callback.onPostPlayback(currentMedia!!, hasEnded, wasSkipped, hasNext)
|
||||
}
|
||||
isPlaying -> {
|
||||
callback.onPlaybackPause(currentMedia, currentMedia!!.getPosition())
|
||||
callback.onPostPlayback(currentMedia, hasEnded, wasSkipped, hasNext)
|
||||
}
|
||||
isPlaying -> callback.onPlaybackPause(currentMedia, currentMedia!!.getPosition())
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -652,20 +639,16 @@ class LocalPSMP(context: Context, callback: PSMPCallback) : PlaybackServiceMedia
|
|||
*/
|
||||
private fun stop() {
|
||||
releaseWifiLockIfNecessary()
|
||||
|
||||
if (playerStatus == PlayerStatus.INDETERMINATE) {
|
||||
setPlayerStatus(PlayerStatus.STOPPED, null)
|
||||
} else {
|
||||
Log.d(TAG, "Ignored call to stop: Current player state is: $playerStatus")
|
||||
}
|
||||
if (playerStatus == PlayerStatus.INDETERMINATE) setPlayerStatus(PlayerStatus.STOPPED, null)
|
||||
else Log.d(TAG, "Ignored call to stop: Current player state is: $playerStatus")
|
||||
}
|
||||
|
||||
override fun shouldLockWifi(): Boolean {
|
||||
return stream
|
||||
return isStreaming
|
||||
}
|
||||
|
||||
private fun setMediaPlayerListeners(mp: ExoPlayerWrapper?) {
|
||||
if (mp == null || media == null) return
|
||||
if (mp == null || playable == null) return
|
||||
|
||||
mp.setOnCompletionListener(Runnable { endPlayback(hasEnded = true, wasSkipped = false, shouldContinue = true, toStoppedState = true) })
|
||||
mp.setOnSeekCompleteListener(Runnable { this.genericSeekCompleteListener() })
|
||||
|
@ -682,11 +665,11 @@ class LocalPSMP(context: Context, callback: PSMPCallback) : PlaybackServiceMedia
|
|||
}
|
||||
|
||||
private fun clearMediaPlayerListeners() {
|
||||
if (mediaPlayer == null) return
|
||||
mediaPlayer!!.setOnCompletionListener {}
|
||||
mediaPlayer!!.setOnSeekCompleteListener {}
|
||||
mediaPlayer!!.setOnBufferingUpdateListener { }
|
||||
mediaPlayer!!.setOnErrorListener { }
|
||||
if (playerWrapper == null) return
|
||||
playerWrapper!!.setOnCompletionListener {}
|
||||
playerWrapper!!.setOnSeekCompleteListener {}
|
||||
playerWrapper!!.setOnBufferingUpdateListener { }
|
||||
playerWrapper!!.setOnErrorListener { }
|
||||
}
|
||||
|
||||
private fun genericSeekCompleteListener() {
|
||||
|
@ -694,9 +677,9 @@ class LocalPSMP(context: Context, callback: PSMPCallback) : PlaybackServiceMedia
|
|||
seekLatch?.countDown()
|
||||
|
||||
if (playerStatus == PlayerStatus.PLAYING) {
|
||||
if (media != null) callback.onPlaybackStart(media!!, getPosition())
|
||||
if (playable != null) callback.onPlaybackStart(playable!!, getPosition())
|
||||
}
|
||||
if (playerStatus == PlayerStatus.SEEKING && statusBeforeSeeking != null) setPlayerStatus(statusBeforeSeeking!!, media, getPosition())
|
||||
if (playerStatus == PlayerStatus.SEEKING && statusBeforeSeeking != null) setPlayerStatus(statusBeforeSeeking!!, playable, getPosition())
|
||||
}
|
||||
|
||||
override fun isCasting(): Boolean {
|
||||
|
|
|
@ -124,13 +124,9 @@ class PlaybackService : MediaSessionService() {
|
|||
private val notificationCustomButtons = NotificationCustomButton.entries.map { command -> command.commandButton }
|
||||
|
||||
private lateinit var taskManager: PlaybackServiceTaskManager
|
||||
// private lateinit var stateManager: PlaybackServiceStateManager
|
||||
// private lateinit var notificationBuilder: PlaybackServiceNotificationBuilder
|
||||
private lateinit var castStateListener: CastStateListener
|
||||
|
||||
private var autoSkippedFeedMediaId: String? = null
|
||||
// private var clickCount = 0
|
||||
// private val clickHandler = Handler(Looper.getMainLooper())
|
||||
|
||||
private var isSpeedForward = false
|
||||
private var normalSpeed = 1.0f
|
||||
|
@ -160,11 +156,6 @@ class PlaybackService : MediaSessionService() {
|
|||
Log.d(TAG, "Service created.")
|
||||
isRunning = true
|
||||
|
||||
// this.startForeground()
|
||||
|
||||
// stateManager = PlaybackServiceStateManager(this)
|
||||
// notificationBuilder = PlaybackServiceNotificationBuilder(this)
|
||||
|
||||
if (Build.VERSION.SDK_INT >= VERSION_CODES.TIRAMISU) {
|
||||
registerReceiver(autoStateUpdated, IntentFilter("com.google.android.gms.car.media.STATUS"), RECEIVER_NOT_EXPORTED)
|
||||
registerReceiver(shutdownReceiver, IntentFilter(PlaybackServiceInterface.ACTION_SHUTDOWN_PLAYBACK_SERVICE), RECEIVER_NOT_EXPORTED)
|
||||
|
@ -236,26 +227,6 @@ class PlaybackService : MediaSessionService() {
|
|||
super.onDestroy()
|
||||
Log.d(TAG, "Service is about to be destroyed")
|
||||
|
||||
// if (notificationBuilder.playerStatus == PlayerStatus.PLAYING || notificationBuilder.playerStatus == PlayerStatus.FALLBACK) {
|
||||
// notificationBuilder.playerStatus = PlayerStatus.STOPPED
|
||||
// val notificationManager = NotificationManagerCompat.from(this)
|
||||
// if (Build.VERSION.SDK_INT >= 33 && ActivityCompat.checkSelfPermission(this,
|
||||
// Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) {
|
||||
// // TODO: Consider calling
|
||||
// // ActivityCompat#requestPermissions
|
||||
//// requestPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS)
|
||||
// // here to request the missing permissions, and then overriding
|
||||
// // public void onRequestPermissionsResult(int requestCode, String[] permissions,
|
||||
// // int[] grantResults)
|
||||
// // to handle the case where the user grants the permission. See the documentation
|
||||
// // for ActivityCompat#requestPermissions for more details.
|
||||
// Log.e(TAG, "onDestroy: require POST_NOTIFICATIONS permission")
|
||||
// Toast.makeText(applicationContext, R.string.notification_permission_text, Toast.LENGTH_LONG).show()
|
||||
// return
|
||||
// }
|
||||
// notificationManager.notify(R.id.notification_playing, notificationBuilder.build())
|
||||
// }
|
||||
// stateManager.stopForeground(!isPersistNotify)
|
||||
isRunning = false
|
||||
currentMediaType = MediaType.UNKNOWN
|
||||
castStateListener.destroy()
|
||||
|
@ -279,6 +250,10 @@ class PlaybackService : MediaSessionService() {
|
|||
EventBus.getDefault().unregister(this)
|
||||
}
|
||||
|
||||
fun isServiceReady(): Boolean {
|
||||
return mediaSession?.player?.playbackState != STATE_IDLE
|
||||
}
|
||||
|
||||
private inner class MyCallback : MediaSession.Callback {
|
||||
override fun onConnect(session: MediaSession, controller: MediaSession.ControllerInfo): MediaSession.ConnectionResult {
|
||||
Log.d(TAG, "in onConnect")
|
||||
|
@ -305,13 +280,6 @@ class PlaybackService : MediaSessionService() {
|
|||
// .setAvailableSessionCommands(sessionCommands.build())
|
||||
// .build()
|
||||
|
||||
val connectionResult = super.onConnect(session, controller)
|
||||
val defaultPlayerCommands = connectionResult.availablePlayerCommands
|
||||
Log.d(TAG, defaultPlayerCommands.toString())
|
||||
// for (command in defaultPlayerCommands.toString()) {
|
||||
// Log.d(TAG, command.toString())
|
||||
// }
|
||||
|
||||
// val availableSessionCommands = connectionResult.availableSessionCommands.buildUpon()
|
||||
|
||||
/* Registering custom player command buttons for player notification. */
|
||||
|
@ -509,23 +477,17 @@ class PlaybackService : MediaSessionService() {
|
|||
super.onStartCommand(intent, flags, startId)
|
||||
Log.d(TAG, "OnStartCommand called")
|
||||
|
||||
// stateManager.startForeground(R.id.notification_playing, notificationBuilder.build())
|
||||
// val notificationManager = NotificationManagerCompat.from(this)
|
||||
// notificationManager.cancel(R.id.notification_streaming_confirmation)
|
||||
|
||||
val keycode = intent?.getIntExtra(MediaButtonReceiver.EXTRA_KEYCODE, -1) ?: -1
|
||||
val customAction = intent?.getStringExtra(MediaButtonReceiver.EXTRA_CUSTOM_ACTION)
|
||||
val hardwareButton = intent?.getBooleanExtra(MediaButtonReceiver.EXTRA_HARDWAREBUTTON, false) ?: false
|
||||
val playable = intent?.getParcelableExtra<Playable>(PlaybackServiceInterface.EXTRA_PLAYABLE)
|
||||
if (keycode == -1 && playable == null && customAction == null) {
|
||||
Log.e(TAG, "PlaybackService was started with no arguments")
|
||||
// stateManager.stopService()
|
||||
return START_NOT_STICKY
|
||||
}
|
||||
|
||||
if ((flags and START_FLAG_REDELIVERY) != 0) {
|
||||
Log.d(TAG, "onStartCommand is a redelivered intent, calling stopForeground now.")
|
||||
// stateManager.stopForeground(true)
|
||||
} else {
|
||||
when {
|
||||
keycode != -1 -> {
|
||||
|
@ -537,14 +499,9 @@ class PlaybackService : MediaSessionService() {
|
|||
Log.d(TAG, "Received media button event")
|
||||
notificationButton = true
|
||||
}
|
||||
val handled = handleKeycode(keycode, notificationButton)
|
||||
// if (!handled && !stateManager.hasReceivedValidStartCommand()) {
|
||||
// stateManager.stopService()
|
||||
// return START_NOT_STICKY
|
||||
// }
|
||||
// val handled = handleKeycode(keycode, notificationButton)
|
||||
}
|
||||
playable != null -> {
|
||||
// stateManager.validStartCommandWasReceived()
|
||||
val allowStreamThisTime = intent.getBooleanExtra(PlaybackServiceInterface.EXTRA_ALLOW_STREAM_THIS_TIME, false)
|
||||
val allowStreamAlways = intent.getBooleanExtra(PlaybackServiceInterface.EXTRA_ALLOW_STREAM_ALWAYS, false)
|
||||
sendNotificationBroadcast(PlaybackServiceInterface.NOTIFICATION_TYPE_RELOAD, 0)
|
||||
|
@ -552,7 +509,6 @@ class PlaybackService : MediaSessionService() {
|
|||
Observable.fromCallable {
|
||||
if (playable is FeedMedia) return@fromCallable DBReader.getFeedMedia(playable.id)
|
||||
else return@fromCallable playable
|
||||
|
||||
}
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
|
@ -561,7 +517,6 @@ class PlaybackService : MediaSessionService() {
|
|||
{ error: Throwable ->
|
||||
Log.d(TAG, "Playable was not found. Stopping service.")
|
||||
error.printStackTrace()
|
||||
// stateManager.stopService()
|
||||
})
|
||||
return START_NOT_STICKY
|
||||
}
|
||||
|
@ -601,56 +556,37 @@ class PlaybackService : MediaSessionService() {
|
|||
return
|
||||
}
|
||||
|
||||
val intentAllowThisTime = Intent(originalIntent)
|
||||
intentAllowThisTime.setAction(PlaybackServiceInterface.EXTRA_ALLOW_STREAM_THIS_TIME)
|
||||
intentAllowThisTime.putExtra(PlaybackServiceInterface.EXTRA_ALLOW_STREAM_THIS_TIME, true)
|
||||
val pendingIntentAllowThisTime = if (Build.VERSION.SDK_INT >= VERSION_CODES.O) {
|
||||
PendingIntent.getForegroundService(this, R.id.pending_intent_allow_stream_this_time, intentAllowThisTime,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
|
||||
} else {
|
||||
PendingIntent.getService(this, R.id.pending_intent_allow_stream_this_time, intentAllowThisTime,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
|
||||
}
|
||||
// val intentAllowThisTime = Intent(originalIntent)
|
||||
// intentAllowThisTime.setAction(PlaybackServiceInterface.EXTRA_ALLOW_STREAM_THIS_TIME)
|
||||
// intentAllowThisTime.putExtra(PlaybackServiceInterface.EXTRA_ALLOW_STREAM_THIS_TIME, true)
|
||||
// val pendingIntentAllowThisTime = if (Build.VERSION.SDK_INT >= VERSION_CODES.O)
|
||||
// PendingIntent.getForegroundService(this, R.id.pending_intent_allow_stream_this_time, intentAllowThisTime,
|
||||
// PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
|
||||
// else PendingIntent.getService(this, R.id.pending_intent_allow_stream_this_time, intentAllowThisTime,
|
||||
// PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
|
||||
|
||||
val intentAlwaysAllow = Intent(intentAllowThisTime)
|
||||
intentAlwaysAllow.setAction(PlaybackServiceInterface.EXTRA_ALLOW_STREAM_ALWAYS)
|
||||
intentAlwaysAllow.putExtra(PlaybackServiceInterface.EXTRA_ALLOW_STREAM_ALWAYS, true)
|
||||
val pendingIntentAlwaysAllow = if (Build.VERSION.SDK_INT >= VERSION_CODES.O) {
|
||||
PendingIntent.getForegroundService(this, R.id.pending_intent_allow_stream_always, intentAlwaysAllow,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
|
||||
} else {
|
||||
PendingIntent.getService(this, R.id.pending_intent_allow_stream_always, intentAlwaysAllow,
|
||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
|
||||
}
|
||||
// val intentAlwaysAllow = Intent(intentAllowThisTime)
|
||||
// intentAlwaysAllow.setAction(PlaybackServiceInterface.EXTRA_ALLOW_STREAM_ALWAYS)
|
||||
// intentAlwaysAllow.putExtra(PlaybackServiceInterface.EXTRA_ALLOW_STREAM_ALWAYS, true)
|
||||
// val pendingIntentAlwaysAllow = if (Build.VERSION.SDK_INT >= VERSION_CODES.O)
|
||||
// PendingIntent.getForegroundService(this, R.id.pending_intent_allow_stream_always, intentAlwaysAllow,
|
||||
// PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
|
||||
// else PendingIntent.getService(this, R.id.pending_intent_allow_stream_always, intentAlwaysAllow,
|
||||
// PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
|
||||
|
||||
val builder = NotificationCompat.Builder(this,
|
||||
NotificationUtils.CHANNEL_ID_USER_ACTION)
|
||||
.setSmallIcon(R.drawable.ic_notification_stream)
|
||||
.setContentTitle(getString(R.string.confirm_mobile_streaming_notification_title))
|
||||
.setContentText(getString(R.string.confirm_mobile_streaming_notification_message))
|
||||
.setStyle(NotificationCompat.BigTextStyle()
|
||||
.bigText(getString(R.string.confirm_mobile_streaming_notification_message)))
|
||||
.setPriority(NotificationCompat.PRIORITY_DEFAULT)
|
||||
.setContentIntent(pendingIntentAllowThisTime)
|
||||
.addAction(R.drawable.ic_notification_stream, getString(R.string.confirm_mobile_streaming_button_once), pendingIntentAllowThisTime)
|
||||
.addAction(R.drawable.ic_notification_stream, getString(R.string.confirm_mobile_streaming_button_always), pendingIntentAlwaysAllow)
|
||||
.setAutoCancel(true)
|
||||
// val notificationManager = NotificationManagerCompat.from(this)
|
||||
if (Build.VERSION.SDK_INT >= 33 && ActivityCompat.checkSelfPermission(this,
|
||||
Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) {
|
||||
// TODO: Consider calling
|
||||
// ActivityCompat#requestPermissions
|
||||
// requestPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS)
|
||||
// here to request the missing permissions, and then overriding
|
||||
// public void onRequestPermissionsResult(int requestCode, String[] permissions,
|
||||
// int[] grantResults)
|
||||
// to handle the case where the user grants the permission. See the documentation
|
||||
// for ActivityCompat#requestPermissions for more details.
|
||||
Log.e(TAG, "displayStreamingNotAllowedNotification: require POST_NOTIFICATIONS permission")
|
||||
Toast.makeText(applicationContext, R.string.notification_permission_text, Toast.LENGTH_LONG).show()
|
||||
return
|
||||
}
|
||||
// notificationManager.notify(R.id.notification_streaming_confirmation, builder.build())
|
||||
|
||||
// val builder = NotificationCompat.Builder(this,
|
||||
// NotificationUtils.CHANNEL_ID_USER_ACTION)
|
||||
// .setSmallIcon(R.drawable.ic_notification_stream)
|
||||
// .setContentTitle(getString(R.string.confirm_mobile_streaming_notification_title))
|
||||
// .setContentText(getString(R.string.confirm_mobile_streaming_notification_message))
|
||||
// .setStyle(NotificationCompat.BigTextStyle()
|
||||
// .bigText(getString(R.string.confirm_mobile_streaming_notification_message)))
|
||||
// .setPriority(NotificationCompat.PRIORITY_DEFAULT)
|
||||
// .setContentIntent(pendingIntentAllowThisTime)
|
||||
// .addAction(R.drawable.ic_notification_stream, getString(R.string.confirm_mobile_streaming_button_once), pendingIntentAllowThisTime)
|
||||
// .addAction(R.drawable.ic_notification_stream, getString(R.string.confirm_mobile_streaming_button_always), pendingIntentAlwaysAllow)
|
||||
// .setAutoCancel(true)
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -734,10 +670,7 @@ class PlaybackService : MediaSessionService() {
|
|||
return false
|
||||
}
|
||||
KeyEvent.KEYCODE_MEDIA_STOP -> {
|
||||
if (this.status == PlayerStatus.FALLBACK || status == PlayerStatus.PLAYING) {
|
||||
mediaPlayer?.pause(true, true)
|
||||
}
|
||||
// stateManager.stopForeground(true) // gets rid of persistent notification
|
||||
if (this.status == PlayerStatus.FALLBACK || status == PlayerStatus.PLAYING) mediaPlayer?.pause(true, true)
|
||||
return true
|
||||
}
|
||||
else -> {
|
||||
|
@ -763,7 +696,6 @@ class PlaybackService : MediaSessionService() {
|
|||
{ error: Throwable ->
|
||||
Log.d(TAG, "Playable was not loaded from preferences. Stopping service.")
|
||||
error.printStackTrace()
|
||||
// stateManager.stopService()
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -773,9 +705,8 @@ class PlaybackService : MediaSessionService() {
|
|||
val localFeed = URLUtil.isContentUrl(playable.getStreamUrl())
|
||||
val stream = !playable.localFileAvailable() || localFeed
|
||||
if (stream && !localFeed && !isStreamingAllowed && !allowStreamThisTime) {
|
||||
displayStreamingNotAllowedNotification(PlaybackServiceStarter(this, playable).intent)
|
||||
// displayStreamingNotAllowedNotification(PlaybackServiceStarter(this, playable).intent)
|
||||
writeNoMediaPlaying()
|
||||
// stateManager.stopService()
|
||||
return
|
||||
}
|
||||
|
||||
|
@ -784,8 +715,6 @@ class PlaybackService : MediaSessionService() {
|
|||
}
|
||||
|
||||
mediaPlayer?.playMediaObject(playable, stream, true, true)
|
||||
// stateManager.validStartCommandWasReceived()
|
||||
// stateManager.startForeground(R.id.notification_playing, notificationBuilder.build())
|
||||
recreateMediaSessionIfNeeded()
|
||||
updateNotificationAndMediaSession(playable)
|
||||
addPlayableToQueue(playable)
|
||||
|
@ -804,7 +733,6 @@ class PlaybackService : MediaSessionService() {
|
|||
mediaPlayer?.pause(true, false)
|
||||
mediaPlayer?.resetVideoSurface()
|
||||
updateNotificationAndMediaSession(playable)
|
||||
// stateManager.stopForeground(!isPersistNotify)
|
||||
}
|
||||
|
||||
private val taskManagerCallback: PSTMCallback = object : PSTMCallback {
|
||||
|
@ -840,9 +768,6 @@ class PlaybackService : MediaSessionService() {
|
|||
}
|
||||
PlayerStatus.PAUSED -> {
|
||||
updateNotificationAndMediaSession(newInfo.playable)
|
||||
if (!isCasting) {
|
||||
// stateManager.stopForeground(!isPersistNotify)
|
||||
}
|
||||
cancelPositionObserver()
|
||||
if (mediaPlayer != null) writePlayerStatus(mediaPlayer!!.playerStatus)
|
||||
}
|
||||
|
@ -853,8 +778,6 @@ class PlaybackService : MediaSessionService() {
|
|||
recreateMediaSessionIfNeeded()
|
||||
updateNotificationAndMediaSession(newInfo.playable)
|
||||
setupPositionObserver()
|
||||
// stateManager.validStartCommandWasReceived()
|
||||
// stateManager.startForeground(R.id.notification_playing, notificationBuilder.build())
|
||||
// set sleep timer if auto-enabled
|
||||
var autoEnableByTime = true
|
||||
val fromSetting = autoEnableFrom()
|
||||
|
@ -872,10 +795,7 @@ class PlaybackService : MediaSessionService() {
|
|||
}
|
||||
loadQueueForMediaSession()
|
||||
}
|
||||
PlayerStatus.ERROR -> {
|
||||
writeNoMediaPlaying()
|
||||
// stateManager.stopService()
|
||||
}
|
||||
PlayerStatus.ERROR -> writeNoMediaPlaying()
|
||||
else -> {}
|
||||
}
|
||||
}
|
||||
|
@ -889,10 +809,8 @@ class PlaybackService : MediaSessionService() {
|
|||
taskManager.requestWidgetUpdate()
|
||||
}
|
||||
|
||||
|
||||
override fun shouldStop() {
|
||||
// stateManager.stopForeground(!isPersistNotify)
|
||||
}
|
||||
// TODO: not used
|
||||
override fun shouldStop() {}
|
||||
|
||||
override fun onMediaChanged(reloadUI: Boolean) {
|
||||
Log.d(TAG, "reloadUI callback reached")
|
||||
|
@ -900,7 +818,7 @@ class PlaybackService : MediaSessionService() {
|
|||
updateNotificationAndMediaSession(this@PlaybackService.playable)
|
||||
}
|
||||
|
||||
override fun onPostPlayback(media: Playable, ended: Boolean, skipped: Boolean, playingNext: Boolean) {
|
||||
override fun onPostPlayback(media: Playable?, ended: Boolean, skipped: Boolean, playingNext: Boolean) {
|
||||
this@PlaybackService.onPostPlayback(media, ended, skipped, playingNext)
|
||||
}
|
||||
|
||||
|
@ -947,7 +865,6 @@ class PlaybackService : MediaSessionService() {
|
|||
fun playerError(event: PlayerErrorEvent?) {
|
||||
if (mediaPlayer?.playerStatus == PlayerStatus.PLAYING || mediaPlayer?.playerStatus == PlayerStatus.FALLBACK)
|
||||
mediaPlayer!!.pause(true, false)
|
||||
// stateManager.stopService()
|
||||
}
|
||||
|
||||
@Subscribe(threadMode = ThreadMode.MAIN)
|
||||
|
@ -1012,9 +929,8 @@ class PlaybackService : MediaSessionService() {
|
|||
}
|
||||
|
||||
if (!nextItem.media!!.localFileAvailable() && !isStreamingAllowed && isFollowQueue && nextItem.feed != null && !nextItem.feed!!.isLocalFeed) {
|
||||
displayStreamingNotAllowedNotification(PlaybackServiceStarter(this, nextItem.media!!).intent)
|
||||
// displayStreamingNotAllowedNotification(PlaybackServiceStarter(this, nextItem.media!!).intent)
|
||||
writeNoMediaPlaying()
|
||||
// stateManager.stopService()
|
||||
return null
|
||||
}
|
||||
return nextItem.media
|
||||
|
@ -1029,10 +945,6 @@ class PlaybackService : MediaSessionService() {
|
|||
if (stopPlaying) {
|
||||
taskManager.cancelPositionSaver()
|
||||
cancelPositionObserver()
|
||||
// if (!isCasting) {
|
||||
// stateManager.stopForeground(true)
|
||||
// stateManager.stopService()
|
||||
// }
|
||||
}
|
||||
if (mediaType == null) {
|
||||
sendNotificationBroadcast(PlaybackServiceInterface.NOTIFICATION_TYPE_PLAYBACK_END, 0)
|
||||
|
@ -1228,59 +1140,32 @@ class PlaybackService : MediaSessionService() {
|
|||
}
|
||||
|
||||
private fun updateNotificationAndMediaSession(p: Playable?) {
|
||||
setupNotification(p)
|
||||
// setupNotification(p)
|
||||
updateMediaSessionMetadata(p)
|
||||
}
|
||||
|
||||
private fun updateMediaSessionMetadata(p: Playable?) {
|
||||
if (p == null || mediaSession == null) return
|
||||
|
||||
val builder = MediaMetadataCompat.Builder()
|
||||
builder.putString(MediaMetadataCompat.METADATA_KEY_ARTIST, p.getFeedTitle())
|
||||
builder.putString(MediaMetadataCompat.METADATA_KEY_TITLE, p.getEpisodeTitle())
|
||||
builder.putString(MediaMetadataCompat.METADATA_KEY_ALBUM, p.getFeedTitle())
|
||||
builder.putLong(MediaMetadataCompat.METADATA_KEY_DURATION, p.getDuration().toLong())
|
||||
builder.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_TITLE, p.getEpisodeTitle())
|
||||
builder.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_SUBTITLE, p.getFeedTitle())
|
||||
// TODO: what's this?
|
||||
// val builder = MediaMetadataCompat.Builder()
|
||||
// builder.putString(MediaMetadataCompat.METADATA_KEY_ARTIST, p.getFeedTitle())
|
||||
// builder.putString(MediaMetadataCompat.METADATA_KEY_TITLE, p.getEpisodeTitle())
|
||||
// builder.putString(MediaMetadataCompat.METADATA_KEY_ALBUM, p.getFeedTitle())
|
||||
// builder.putLong(MediaMetadataCompat.METADATA_KEY_DURATION, p.getDuration().toLong())
|
||||
// builder.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_TITLE, p.getEpisodeTitle())
|
||||
// builder.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_SUBTITLE, p.getFeedTitle())
|
||||
|
||||
|
||||
// if (notificationBuilder.isIconCached) {
|
||||
// builder.putBitmap(MediaMetadataCompat.METADATA_KEY_ART, notificationBuilder.cachedIcon)
|
||||
// } else {
|
||||
// var iconUri = p.getImageLocation()
|
||||
// // Don't use embedded cover etc, which Android can't load
|
||||
// if (p is FeedMedia) {
|
||||
// val m = p
|
||||
// if (m.item != null) {
|
||||
// val item = m.item!!
|
||||
// when {
|
||||
// item.imageUrl != null -> {
|
||||
// iconUri = item.imageUrl
|
||||
// }
|
||||
// item.feed != null -> {
|
||||
// iconUri = item.feed!!.imageUrl
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// if (!iconUri.isNullOrEmpty()) {
|
||||
// builder.putString(MediaMetadataCompat.METADATA_KEY_DISPLAY_ICON_URI, iconUri)
|
||||
// }
|
||||
// }
|
||||
|
||||
// if (stateManager.hasReceivedValidStartCommand()) {
|
||||
// TODO: what's this?
|
||||
// mediaSession!!.setSessionActivity(PendingIntent.getActivity(this, R.id.pending_intent_player_activity,
|
||||
// getPlayerActivityIntent(this), PendingIntent.FLAG_UPDATE_CURRENT
|
||||
// or (if (Build.VERSION.SDK_INT >= 31) PendingIntent.FLAG_MUTABLE else 0)))
|
||||
mediaSession!!.setSessionActivity(PendingIntent.getActivity(this, R.id.pending_intent_player_activity,
|
||||
getPlayerActivityIntent(this), FLAG_IMMUTABLE))
|
||||
// getPlayerActivityIntent(this), FLAG_IMMUTABLE))
|
||||
|
||||
// try {
|
||||
// mediaSession!!.setMetadata(builder.build())
|
||||
// } catch (e: OutOfMemoryError) {
|
||||
// Log.e(TAG, "Setting media session metadata", e)
|
||||
// builder.putBitmap(MediaMetadataCompat.METADATA_KEY_ART, null)
|
||||
// mediaSession!!.setMetadata(builder.build())
|
||||
// }
|
||||
// }
|
||||
}
|
||||
|
||||
|
@ -1292,6 +1177,7 @@ class PlaybackService : MediaSessionService() {
|
|||
/**
|
||||
* Prepares notification and starts the service in the foreground.
|
||||
*/
|
||||
// TODO: not needed?
|
||||
@Synchronized
|
||||
private fun setupNotification(playable: Playable?) {
|
||||
Log.d(TAG, "setupNotification")
|
||||
|
@ -1299,46 +1185,8 @@ class PlaybackService : MediaSessionService() {
|
|||
|
||||
if (playable == null || mediaPlayer == null) {
|
||||
Log.d(TAG, "setupNotification: playable=$playable mediaPlayer=$mediaPlayer")
|
||||
// if (!stateManager.hasReceivedValidStartCommand()) {
|
||||
// stateManager.stopService()
|
||||
// }
|
||||
return
|
||||
}
|
||||
|
||||
val playerStatus = mediaPlayer!!.playerStatus
|
||||
// notificationBuilder.setPlayable(playable)
|
||||
// if (mediaSession != null) notificationBuilder.setMediaSessionToken(mediaSession!!.getSessionCompatToken())
|
||||
// notificationBuilder.playerStatus = playerStatus
|
||||
// notificationBuilder.updatePosition(currentPosition, currentPlaybackSpeed)
|
||||
|
||||
// val notificationManager = NotificationManagerCompat.from(this)
|
||||
if (Build.VERSION.SDK_INT >= 33 && ActivityCompat.checkSelfPermission(this,
|
||||
Manifest.permission.POST_NOTIFICATIONS) != PackageManager.PERMISSION_GRANTED) {
|
||||
// TODO: Consider calling
|
||||
// ActivityCompat#requestPermissions
|
||||
// requestPermissionLauncher.launch(Manifest.permission.POST_NOTIFICATIONS)
|
||||
// here to request the missing permissions, and then overriding
|
||||
// public void onRequestPermissionsResult(int requestCode, String[] permissions,
|
||||
// int[] grantResults)
|
||||
// to handle the case where the user grants the permission. See the documentation
|
||||
// for ActivityCompat#requestPermissions for more details.
|
||||
Log.e(TAG, "setupNotification: require POST_NOTIFICATIONS permission")
|
||||
Toast.makeText(applicationContext, R.string.notification_permission_text, Toast.LENGTH_LONG).show()
|
||||
return
|
||||
}
|
||||
// notificationManager.notify(R.id.notification_playing, notificationBuilder.build())
|
||||
|
||||
// if (!notificationBuilder.isIconCached) {
|
||||
// playableIconLoaderThread = Thread {
|
||||
// Log.d(TAG, "Loading notification icon")
|
||||
// notificationBuilder.loadIcon()
|
||||
// if (!Thread.currentThread().isInterrupted) {
|
||||
// notificationManager.notify(R.id.notification_playing, notificationBuilder.build())
|
||||
// updateMediaSessionMetadata(playable)
|
||||
// }
|
||||
// }
|
||||
// playableIconLoaderThread?.start()
|
||||
// }
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -1485,10 +1333,7 @@ class PlaybackService : MediaSessionService() {
|
|||
}
|
||||
if (transientPause) {
|
||||
transientPause = false
|
||||
if (Build.VERSION.SDK_INT >= 31) {
|
||||
// stateManager.stopService()
|
||||
return
|
||||
}
|
||||
if (Build.VERSION.SDK_INT >= 31) return
|
||||
when {
|
||||
!bluetooth && isUnpauseOnHeadsetReconnect -> mediaPlayer?.resume()
|
||||
bluetooth && isUnpauseOnBluetoothReconnect -> {
|
||||
|
@ -1503,10 +1348,8 @@ class PlaybackService : MediaSessionService() {
|
|||
|
||||
private val shutdownReceiver: BroadcastReceiver = object : BroadcastReceiver() {
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
if (TextUtils.equals(intent.action, PlaybackServiceInterface.ACTION_SHUTDOWN_PLAYBACK_SERVICE)) {
|
||||
if (TextUtils.equals(intent.action, PlaybackServiceInterface.ACTION_SHUTDOWN_PLAYBACK_SERVICE))
|
||||
EventBus.getDefault().post(PlaybackServiceEvent(PlaybackServiceEvent.Action.SERVICE_SHUT_DOWN))
|
||||
// stateManager.stopService()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -1520,7 +1363,6 @@ class PlaybackService : MediaSessionService() {
|
|||
@Subscribe(threadMode = ThreadMode.MAIN)
|
||||
@Suppress("unused")
|
||||
fun speedPresetChanged(event: SpeedPresetChangedEvent) {
|
||||
// TODO: speed
|
||||
val item = (playable as? FeedMedia)?.item ?: currentitem
|
||||
// if (playable is FeedMedia) {
|
||||
if (item?.feed?.id == event.feedId) {
|
||||
|
@ -1715,12 +1557,6 @@ class PlaybackService : MediaSessionService() {
|
|||
.subscribe {
|
||||
Log.d(TAG, "setupPositionObserver currentPosition: $currentPosition, currentPlaybackSpeed: $currentPlaybackSpeed")
|
||||
EventBus.getDefault().post(PlaybackPositionEvent(currentPosition, duration))
|
||||
// TODO: why set SDK_INT < 29
|
||||
if (Build.VERSION.SDK_INT < 29) {
|
||||
// notificationBuilder.updatePosition(currentPosition, currentPlaybackSpeed)
|
||||
// val notificationManager = getSystemService(NOTIFICATION_SERVICE) as? NotificationManager
|
||||
// notificationManager?.notify(R.id.notification_playing, notificationBuilder.build())
|
||||
}
|
||||
skipEndingIfNecessary()
|
||||
}
|
||||
}
|
||||
|
@ -1737,15 +1573,9 @@ class PlaybackService : MediaSessionService() {
|
|||
}
|
||||
}
|
||||
|
||||
// private val sessionCallback: MediaSession.Callback = object : MediaSession.Callback {
|
||||
// private val TAG = "MediaSessionCompat"
|
||||
//// TODO: not used now with media3
|
||||
// }
|
||||
|
||||
companion object {
|
||||
private const val TAG = "PlaybackService"
|
||||
|
||||
// TODO: need to experiment this value
|
||||
private const val POSITION_EVENT_INTERVAL = 5L
|
||||
|
||||
const val ACTION_PLAYER_STATUS_CHANGED: String = "action.ac.mdiq.podcini.service.playerStatusChanged"
|
||||
|
|
|
@ -1,222 +0,0 @@
|
|||
package ac.mdiq.podcini.playback.service
|
||||
|
||||
import ac.mdiq.podcini.R
|
||||
import android.app.Notification
|
||||
import android.app.PendingIntent
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.graphics.Bitmap
|
||||
import android.graphics.Canvas
|
||||
import android.graphics.drawable.BitmapDrawable
|
||||
import android.graphics.drawable.VectorDrawable
|
||||
import android.os.Build
|
||||
import android.support.v4.media.session.MediaSessionCompat
|
||||
import android.util.Log
|
||||
import android.view.KeyEvent
|
||||
import androidx.appcompat.content.res.AppCompatResources
|
||||
import androidx.core.app.NotificationCompat
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import com.bumptech.glide.Glide
|
||||
import com.bumptech.glide.request.RequestOptions
|
||||
import ac.mdiq.podcini.feed.util.ImageResourceUtils
|
||||
import ac.mdiq.podcini.receiver.MediaButtonReceiver
|
||||
import ac.mdiq.podcini.util.TimeSpeedConverter
|
||||
import ac.mdiq.podcini.ui.utils.NotificationUtils
|
||||
import ac.mdiq.podcini.storage.model.playback.Playable
|
||||
import ac.mdiq.podcini.playback.base.PlayerStatus
|
||||
import ac.mdiq.podcini.preferences.UserPreferences
|
||||
import ac.mdiq.podcini.util.Converter
|
||||
import org.apache.commons.lang3.ArrayUtils
|
||||
|
||||
// TODO: not needed with media3
|
||||
@UnstableApi
|
||||
class PlaybackServiceNotificationBuilder(private val context: Context) {
|
||||
private var playable: Playable? = null
|
||||
private var mediaSessionToken: MediaSessionCompat.Token? = null
|
||||
@JvmField
|
||||
var playerStatus: PlayerStatus? = null
|
||||
var cachedIcon: Bitmap? = null
|
||||
private set
|
||||
private var position: String? = null
|
||||
|
||||
fun setPlayable(playable: Playable) {
|
||||
if (playable !== this.playable) clearCache()
|
||||
|
||||
this.playable = playable
|
||||
}
|
||||
|
||||
private fun clearCache() {
|
||||
this.cachedIcon = null
|
||||
this.position = null
|
||||
}
|
||||
|
||||
fun updatePosition(position: Int, speed: Float) {
|
||||
val converter = TimeSpeedConverter(speed)
|
||||
this.position = Converter.getDurationStringLong(converter.convert(position))
|
||||
}
|
||||
|
||||
val isIconCached: Boolean
|
||||
get() = cachedIcon != null
|
||||
|
||||
fun loadIcon() {
|
||||
val iconSize = (128 * context.resources.displayMetrics.density).toInt()
|
||||
val options = RequestOptions().centerCrop()
|
||||
try {
|
||||
val imgLoc = playable?.getImageLocation()
|
||||
val imgLoc1 = ImageResourceUtils.getFallbackImageLocation(playable!!)
|
||||
Log.d(TAG, "loadIcon imgLoc $imgLoc $imgLoc1")
|
||||
cachedIcon = Glide.with(context)
|
||||
.asBitmap()
|
||||
.load(imgLoc)
|
||||
.error(Glide.with(context)
|
||||
.asBitmap()
|
||||
.load(imgLoc1)
|
||||
.apply(options)
|
||||
.submit(iconSize, iconSize)
|
||||
.get())
|
||||
.apply(options)
|
||||
.submit(iconSize, iconSize)
|
||||
.get()
|
||||
} catch (ignore: InterruptedException) {
|
||||
Log.e(TAG, "Media icon loader was interrupted")
|
||||
} catch (tr: Throwable) {
|
||||
Log.e(TAG, "Error loading the media icon for the notification", tr)
|
||||
}
|
||||
}
|
||||
|
||||
private val defaultIcon: Bitmap?
|
||||
get() {
|
||||
if (Companion.defaultIcon == null) Companion.defaultIcon = getBitmap(context, R.mipmap.ic_launcher)
|
||||
return Companion.defaultIcon
|
||||
}
|
||||
|
||||
fun build(): Notification {
|
||||
val notification = NotificationCompat.Builder(context, NotificationUtils.CHANNEL_ID_PLAYING)
|
||||
|
||||
if (playable != null) {
|
||||
notification.setContentTitle(playable!!.getFeedTitle())
|
||||
notification.setContentText(playable!!.getEpisodeTitle())
|
||||
addActions(notification, mediaSessionToken, playerStatus)
|
||||
|
||||
if (cachedIcon != null) notification.setLargeIcon(cachedIcon)
|
||||
else notification.setLargeIcon(this.defaultIcon)
|
||||
|
||||
if (Build.VERSION.SDK_INT < 29) notification.setSubText(position)
|
||||
} else {
|
||||
notification.setContentTitle(context.getString(R.string.app_name))
|
||||
notification.setContentText("Loading. If this does not go away, play any episode and contact us.")
|
||||
}
|
||||
|
||||
notification.setContentIntent(playerActivityPendingIntent)
|
||||
notification.setWhen(0)
|
||||
notification.setSmallIcon(R.drawable.ic_notification)
|
||||
notification.setOngoing(false)
|
||||
notification.setOnlyAlertOnce(true)
|
||||
notification.setShowWhen(false)
|
||||
notification.setPriority(UserPreferences.notifyPriority)
|
||||
notification.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
|
||||
notification.setColor(NotificationCompat.COLOR_DEFAULT)
|
||||
return notification.build()
|
||||
}
|
||||
|
||||
private val playerActivityPendingIntent: PendingIntent
|
||||
get() = PendingIntent.getActivity(context, R.id.pending_intent_player_activity, PlaybackService.getPlayerActivityIntent(context),
|
||||
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
|
||||
|
||||
private fun addActions(notification: NotificationCompat.Builder, mediaSessionToken: MediaSessionCompat.Token?, playerStatus: PlayerStatus?) {
|
||||
val compactActionList = ArrayList<Int>()
|
||||
|
||||
var numActions = 0 // we start and 0 and then increment by 1 for each call to addAction
|
||||
|
||||
val rewindButtonPendingIntent = getPendingIntentForMediaAction(KeyEvent.KEYCODE_MEDIA_REWIND, numActions)
|
||||
notification.addAction(R.drawable.ic_notification_fast_rewind, context.getString(R.string.rewind_label), rewindButtonPendingIntent)
|
||||
compactActionList.add(numActions)
|
||||
numActions++
|
||||
|
||||
if (playerStatus == PlayerStatus.PLAYING) {
|
||||
val pauseButtonPendingIntent = getPendingIntentForMediaAction(KeyEvent.KEYCODE_MEDIA_PAUSE, numActions)
|
||||
//pause action
|
||||
notification.addAction(R.drawable.ic_notification_pause, context.getString(R.string.pause_label), pauseButtonPendingIntent)
|
||||
} else {
|
||||
val playButtonPendingIntent = getPendingIntentForMediaAction(KeyEvent.KEYCODE_MEDIA_PLAY, numActions)
|
||||
//play action
|
||||
notification.addAction(R.drawable.ic_notification_play, context.getString(R.string.play_label), playButtonPendingIntent)
|
||||
}
|
||||
compactActionList.add(numActions++)
|
||||
|
||||
// ff follows play, then we have skip (if it's present)
|
||||
val ffButtonPendingIntent = getPendingIntentForMediaAction(KeyEvent.KEYCODE_MEDIA_FAST_FORWARD, numActions)
|
||||
notification.addAction(R.drawable.ic_notification_fast_forward, context.getString(R.string.fast_forward_label), ffButtonPendingIntent)
|
||||
compactActionList.add(numActions)
|
||||
numActions++
|
||||
|
||||
if (UserPreferences.showNextChapterOnFullNotification() && playable?.getChapters() != null) {
|
||||
val nextChapterPendingIntent = getPendingIntentForCustomMediaAction(PlaybackService.CUSTOM_ACTION_NEXT_CHAPTER, numActions)
|
||||
notification.addAction(R.drawable.ic_notification_next_chapter, context.getString(R.string.next_chapter), nextChapterPendingIntent)
|
||||
numActions++
|
||||
}
|
||||
|
||||
if (UserPreferences.showSkipOnFullNotification()) {
|
||||
val skipButtonPendingIntent = getPendingIntentForMediaAction(KeyEvent.KEYCODE_MEDIA_NEXT, numActions)
|
||||
notification.addAction(R.drawable.ic_notification_skip, context.getString(R.string.skip_episode_label), skipButtonPendingIntent)
|
||||
numActions++
|
||||
}
|
||||
|
||||
val stopButtonPendingIntent = getPendingIntentForMediaAction(KeyEvent.KEYCODE_MEDIA_STOP, numActions)
|
||||
notification
|
||||
.setStyle(androidx.media.app.NotificationCompat.MediaStyle()
|
||||
.setMediaSession(mediaSessionToken)
|
||||
.setShowActionsInCompactView(*ArrayUtils.toPrimitive(compactActionList.toTypedArray<Int>()))
|
||||
.setShowCancelButton(true)
|
||||
.setCancelButtonIntent(stopButtonPendingIntent))
|
||||
}
|
||||
|
||||
private fun getPendingIntentForMediaAction(keycodeValue: Int, requestCode: Int): PendingIntent {
|
||||
val intent = Intent(context, PlaybackService::class.java)
|
||||
intent.setAction("MediaCode$keycodeValue")
|
||||
intent.putExtra(MediaButtonReceiver.EXTRA_KEYCODE, keycodeValue)
|
||||
|
||||
return if (Build.VERSION.SDK_INT >= 26) {
|
||||
PendingIntent.getForegroundService(context, requestCode, intent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
|
||||
} else {
|
||||
PendingIntent.getService(context, requestCode, intent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
|
||||
}
|
||||
}
|
||||
|
||||
private fun getPendingIntentForCustomMediaAction(action: String, requestCode: Int): PendingIntent {
|
||||
val intent = Intent(context, PlaybackService::class.java)
|
||||
intent.setAction("MediaAction$action")
|
||||
intent.putExtra(MediaButtonReceiver.EXTRA_CUSTOM_ACTION, action)
|
||||
|
||||
return if (Build.VERSION.SDK_INT >= 26) {
|
||||
PendingIntent.getForegroundService(context, requestCode, intent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
|
||||
} else {
|
||||
PendingIntent.getService(context, requestCode, intent, PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
|
||||
}
|
||||
}
|
||||
|
||||
fun setMediaSessionToken(mediaSessionToken: MediaSessionCompat.Token?) {
|
||||
this.mediaSessionToken = mediaSessionToken
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TAG = "PlaybackSrvNotification"
|
||||
private var defaultIcon: Bitmap? = null
|
||||
|
||||
private fun getBitmap(vectorDrawable: VectorDrawable): Bitmap {
|
||||
val bitmap = Bitmap.createBitmap(vectorDrawable.intrinsicWidth, vectorDrawable.intrinsicHeight, Bitmap.Config.ARGB_8888)
|
||||
val canvas = Canvas(bitmap)
|
||||
vectorDrawable.setBounds(0, 0, canvas.width, canvas.height)
|
||||
vectorDrawable.draw(canvas)
|
||||
return bitmap
|
||||
}
|
||||
|
||||
private fun getBitmap(context: Context, drawableId: Int): Bitmap? {
|
||||
return when (val drawable = AppCompatResources.getDrawable(context, drawableId)) {
|
||||
is BitmapDrawable -> drawable.bitmap
|
||||
is VectorDrawable -> getBitmap(drawable)
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,58 +0,0 @@
|
|||
package ac.mdiq.podcini.playback.service
|
||||
|
||||
import android.app.Notification
|
||||
import android.content.pm.ServiceInfo.FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK
|
||||
import android.os.Build
|
||||
import android.util.Log
|
||||
import androidx.core.app.ServiceCompat
|
||||
import kotlin.concurrent.Volatile
|
||||
|
||||
// TODO: not needed with media3
|
||||
internal class PlaybackServiceStateManager(private val playbackService: PlaybackService) {
|
||||
@Volatile
|
||||
private var isInForeground = false
|
||||
|
||||
@Volatile
|
||||
private var hasReceivedValidStartCommand = false
|
||||
|
||||
fun startForeground(notificationId: Int, notification: Notification) {
|
||||
Log.d(TAG, "startForeground")
|
||||
if (Build.VERSION.SDK_INT >= 29) {
|
||||
playbackService.startForeground(notificationId, notification, FOREGROUND_SERVICE_TYPE_MEDIA_PLAYBACK)
|
||||
} else {
|
||||
playbackService.startForeground(notificationId, notification)
|
||||
}
|
||||
isInForeground = true
|
||||
}
|
||||
|
||||
fun stopService() {
|
||||
Log.d(TAG, "stopService")
|
||||
stopForeground(true)
|
||||
playbackService.stopSelf()
|
||||
hasReceivedValidStartCommand = false
|
||||
}
|
||||
|
||||
fun stopForeground(removeNotification: Boolean) {
|
||||
Log.d(TAG, "stopForeground")
|
||||
if (isInForeground) {
|
||||
if (removeNotification) {
|
||||
ServiceCompat.stopForeground(playbackService, ServiceCompat.STOP_FOREGROUND_REMOVE)
|
||||
} else {
|
||||
ServiceCompat.stopForeground(playbackService, ServiceCompat.STOP_FOREGROUND_DETACH)
|
||||
}
|
||||
}
|
||||
isInForeground = false
|
||||
}
|
||||
|
||||
fun hasReceivedValidStartCommand(): Boolean {
|
||||
return hasReceivedValidStartCommand
|
||||
}
|
||||
|
||||
fun validStartCommandWasReceived() {
|
||||
this.hasReceivedValidStartCommand = true
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val TAG = "PlaybackSrvState"
|
||||
}
|
||||
}
|
|
@ -68,9 +68,7 @@ class PlaybackServiceTaskManager(private val context: Context, private val callb
|
|||
positionSaverFuture = schedExecutor.scheduleWithFixedDelay(positionSaver, POSITION_SAVER_WAITING_INTERVAL.toLong(),
|
||||
POSITION_SAVER_WAITING_INTERVAL.toLong(), TimeUnit.MILLISECONDS)
|
||||
Log.d(TAG, "Started PositionSaver")
|
||||
} else {
|
||||
Log.d(TAG, "Call to startPositionSaver was ignored.")
|
||||
}
|
||||
} else Log.d(TAG, "Call to startPositionSaver was ignored.")
|
||||
}
|
||||
|
||||
@get:Synchronized
|
||||
|
@ -102,9 +100,7 @@ class PlaybackServiceTaskManager(private val context: Context, private val callb
|
|||
widgetUpdaterFuture = schedExecutor.scheduleWithFixedDelay(widgetUpdater, WIDGET_UPDATER_NOTIFICATION_INTERVAL.toLong(),
|
||||
WIDGET_UPDATER_NOTIFICATION_INTERVAL.toLong(), TimeUnit.MILLISECONDS)
|
||||
Log.d(TAG, "Started WidgetUpdater")
|
||||
} else {
|
||||
Log.d(TAG, "Call to startWidgetUpdater was ignored.")
|
||||
}
|
||||
} else Log.d(TAG, "Call to startWidgetUpdater was ignored.")
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -243,9 +239,7 @@ class PlaybackServiceTaskManager(private val context: Context, private val callb
|
|||
// Run on ui thread even if called from schedExecutor
|
||||
val handler = Handler(Looper.getMainLooper())
|
||||
return Runnable { handler.post(runnable) }
|
||||
} else {
|
||||
return runnable
|
||||
}
|
||||
} else return runnable
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -287,10 +281,9 @@ class PlaybackServiceTaskManager(private val context: Context, private val callb
|
|||
}
|
||||
if (timeLeft <= 0) {
|
||||
Log.d(TAG, "Sleep timer expired")
|
||||
if (shakeListener != null) {
|
||||
shakeListener!!.pause()
|
||||
shakeListener?.pause()
|
||||
shakeListener = null
|
||||
}
|
||||
|
||||
hasVibrated = false
|
||||
}
|
||||
}
|
||||
|
@ -303,11 +296,9 @@ class PlaybackServiceTaskManager(private val context: Context, private val callb
|
|||
fun restart() {
|
||||
EventBus.getDefault().post(SleepTimerUpdatedEvent.cancelled())
|
||||
setSleepTimer(waitingTime)
|
||||
if (shakeListener != null) {
|
||||
shakeListener!!.pause()
|
||||
shakeListener?.pause()
|
||||
shakeListener = null
|
||||
}
|
||||
}
|
||||
|
||||
fun cancel() {
|
||||
sleepTimerFuture!!.cancel(true)
|
||||
|
|
|
@ -6,8 +6,7 @@ import ac.mdiq.podcini.playback.base.PlaybackServiceMediaPlayer
|
|||
import ac.mdiq.podcini.playback.base.PlayerStatus
|
||||
|
||||
internal class PlaybackVolumeUpdater {
|
||||
fun updateVolumeIfNecessary(mediaPlayer: PlaybackServiceMediaPlayer, feedId: Long,
|
||||
volumeAdaptionSetting: VolumeAdaptionSetting) {
|
||||
fun updateVolumeIfNecessary(mediaPlayer: PlaybackServiceMediaPlayer, feedId: Long, volumeAdaptionSetting: VolumeAdaptionSetting) {
|
||||
val playable = mediaPlayer.getPlayable()
|
||||
|
||||
if (playable is FeedMedia) updateFeedMediaVolumeIfNecessary(mediaPlayer, feedId, volumeAdaptionSetting, playable)
|
||||
|
|
|
@ -43,9 +43,8 @@ class QuickSettingsTileService : TileService() {
|
|||
|
||||
fun updateTile() {
|
||||
val qsTile = qsTile
|
||||
if (qsTile == null) {
|
||||
Log.d(TAG, "Ignored call to update QS tile: getQsTile() returned null.")
|
||||
} else {
|
||||
if (qsTile == null) Log.d(TAG, "Ignored call to update QS tile: getQsTile() returned null.")
|
||||
else {
|
||||
val isPlaying = (PlaybackService.isRunning && PlaybackPreferences.currentPlayerStatus == PlaybackPreferences.PLAYER_STATUS_PLAYING)
|
||||
qsTile.state = if (isPlaying) Tile.STATE_ACTIVE else Tile.STATE_INACTIVE
|
||||
qsTile.updateTile()
|
||||
|
|
|
@ -31,11 +31,9 @@ internal class ShakeListener(private val mContext: Context, private val mSleepTi
|
|||
}
|
||||
|
||||
fun pause() {
|
||||
if (mSensorMgr != null) {
|
||||
mSensorMgr!!.unregisterListener(this)
|
||||
mSensorMgr?.unregisterListener(this)
|
||||
mSensorMgr = null
|
||||
}
|
||||
}
|
||||
|
||||
override fun onSensorChanged(event: SensorEvent) {
|
||||
val gX = event.values[0] / SensorManager.GRAVITY_EARTH
|
||||
|
@ -49,8 +47,7 @@ internal class ShakeListener(private val mContext: Context, private val mSleepTi
|
|||
}
|
||||
}
|
||||
|
||||
override fun onAccuracyChanged(sensor: Sensor, accuracy: Int) {
|
||||
}
|
||||
override fun onAccuracyChanged(sensor: Sensor, accuracy: Int) {}
|
||||
|
||||
companion object {
|
||||
private val TAG: String = ShakeListener::class.java.simpleName
|
||||
|
|
|
@ -20,17 +20,13 @@ class MaterialListPreference : ListPreference {
|
|||
val values = entryValues
|
||||
var selected = -1
|
||||
for (i in values.indices) {
|
||||
if (values[i].toString() == value) {
|
||||
selected = i
|
||||
}
|
||||
if (values[i].toString() == value) selected = i
|
||||
}
|
||||
builder.setSingleChoiceItems(entries, selected) { dialog: DialogInterface, which: Int ->
|
||||
dialog.dismiss()
|
||||
if (which >= 0 && entryValues != null) {
|
||||
val value = entryValues[which].toString()
|
||||
if (callChangeListener(value)) {
|
||||
setValue(value)
|
||||
}
|
||||
if (callChangeListener(value)) setValue(value)
|
||||
}
|
||||
}
|
||||
builder.show()
|
||||
|
|
|
@ -22,14 +22,11 @@ class MaterialMultiSelectListPreference : MultiSelectListPreference {
|
|||
for (i in values.indices) {
|
||||
selected[i] = getValues().contains(values[i].toString())
|
||||
}
|
||||
builder.setMultiChoiceItems(entries, selected
|
||||
) { dialog: DialogInterface?, which: Int, isChecked: Boolean -> selected[which] = isChecked }
|
||||
builder.setMultiChoiceItems(entries, selected) { dialog: DialogInterface?, which: Int, isChecked: Boolean -> selected[which] = isChecked }
|
||||
builder.setPositiveButton("OK") { dialog: DialogInterface?, which: Int ->
|
||||
val selectedValues: MutableSet<String> = HashSet()
|
||||
for (i in values.indices) {
|
||||
if (selected[i]) {
|
||||
selectedValues.add(entryValues[i].toString())
|
||||
}
|
||||
if (selected[i]) selectedValues.add(entryValues[i].toString())
|
||||
}
|
||||
setValues(selectedValues)
|
||||
}
|
||||
|
|
|
@ -22,9 +22,7 @@ import org.greenrobot.eventbus.EventBus
|
|||
*/
|
||||
class PlaybackPreferences private constructor() : OnSharedPreferenceChangeListener {
|
||||
override fun onSharedPreferenceChanged(sharedPreferences: SharedPreferences, key: String?) {
|
||||
if (PREF_CURRENT_PLAYER_STATUS == key) {
|
||||
EventBus.getDefault().post(PlayerStatusEvent())
|
||||
}
|
||||
if (PREF_CURRENT_PLAYER_STATUS == key) EventBus.getDefault().post(PlayerStatusEvent())
|
||||
}
|
||||
|
||||
companion object {
|
||||
|
@ -222,9 +220,7 @@ class PlaybackPreferences private constructor() : OnSharedPreferenceChangeListen
|
|||
private fun createFeedMediaInstance(pref: SharedPreferences): Playable? {
|
||||
var result: Playable? = null
|
||||
val mediaId = pref.getLong(FeedMedia.PREF_MEDIA_ID, -1)
|
||||
if (mediaId != -1L) {
|
||||
result = DBReader.getFeedMedia(mediaId)
|
||||
}
|
||||
if (mediaId != -1L) result = DBReader.getFeedMedia(mediaId)
|
||||
return result
|
||||
}
|
||||
}
|
||||
|
|
|
@ -48,10 +48,9 @@ object PreferenceUpgrader {
|
|||
}
|
||||
|
||||
private fun upgrade(oldVersion: Int, context: Context) {
|
||||
if (oldVersion == -1) {
|
||||
//New installation
|
||||
return
|
||||
}
|
||||
if (oldVersion == -1) return
|
||||
|
||||
if (oldVersion < 1070196) {
|
||||
// migrate episode cleanup value (unit changed from days to hours)
|
||||
val oldValueInDays = episodeCleanupValue
|
||||
|
@ -60,14 +59,10 @@ object PreferenceUpgrader {
|
|||
} // else 0 or special negative values, no change needed
|
||||
}
|
||||
if (oldVersion < 1070197) {
|
||||
if (prefs.getBoolean("prefMobileUpdate", false)) {
|
||||
prefs.edit().putString("prefMobileUpdateAllowed", "everything").apply()
|
||||
}
|
||||
if (prefs.getBoolean("prefMobileUpdate", false)) prefs.edit().putString("prefMobileUpdateAllowed", "everything").apply()
|
||||
}
|
||||
if (oldVersion < 1070300) {
|
||||
if (prefs.getBoolean("prefEnableAutoDownloadOnMobile", false)) {
|
||||
isAllowMobileAutoDownload = true
|
||||
}
|
||||
if (prefs.getBoolean("prefEnableAutoDownloadOnMobile", false)) isAllowMobileAutoDownload = true
|
||||
when (prefs.getString("prefMobileUpdateAllowed", "images")) {
|
||||
"everything" -> {
|
||||
isAllowMobileFeedRefresh = true
|
||||
|
@ -80,9 +75,7 @@ object PreferenceUpgrader {
|
|||
}
|
||||
if (oldVersion < 1070400) {
|
||||
val theme = theme
|
||||
if (theme == UserPreferences.ThemePreference.LIGHT) {
|
||||
prefs.edit().putString(UserPreferences.PREF_THEME, "system").apply()
|
||||
}
|
||||
if (theme == UserPreferences.ThemePreference.LIGHT) prefs.edit().putString(UserPreferences.PREF_THEME, "system").apply()
|
||||
|
||||
isQueueLocked = false
|
||||
isStreamOverDownload = false
|
||||
|
@ -96,49 +89,38 @@ object PreferenceUpgrader {
|
|||
}
|
||||
if (oldVersion < 2010300) {
|
||||
// Migrate hardware button preferences
|
||||
if (prefs.getBoolean("prefHardwareForwardButtonSkips", false)) {
|
||||
prefs.edit().putString(UserPreferences.PREF_HARDWARE_FORWARD_BUTTON,
|
||||
KeyEvent.KEYCODE_MEDIA_NEXT.toString()).apply()
|
||||
}
|
||||
if (prefs.getBoolean("prefHardwarePreviousButtonRestarts", false)) {
|
||||
prefs.edit().putString(UserPreferences.PREF_HARDWARE_PREVIOUS_BUTTON,
|
||||
KeyEvent.KEYCODE_MEDIA_PREVIOUS.toString()).apply()
|
||||
}
|
||||
if (prefs.getBoolean("prefHardwareForwardButtonSkips", false))
|
||||
prefs.edit().putString(UserPreferences.PREF_HARDWARE_FORWARD_BUTTON, KeyEvent.KEYCODE_MEDIA_NEXT.toString()).apply()
|
||||
|
||||
if (prefs.getBoolean("prefHardwarePreviousButtonRestarts", false))
|
||||
prefs.edit().putString(UserPreferences.PREF_HARDWARE_PREVIOUS_BUTTON, KeyEvent.KEYCODE_MEDIA_PREVIOUS.toString()).apply()
|
||||
}
|
||||
if (oldVersion < 2040000) {
|
||||
val swipePrefs = context.getSharedPreferences(SwipeActions.PREF_NAME, Context.MODE_PRIVATE)
|
||||
swipePrefs.edit().putString(SwipeActions.KEY_PREFIX_SWIPEACTIONS + QueueFragment.TAG,
|
||||
SwipeAction.REMOVE_FROM_QUEUE + "," + SwipeAction.REMOVE_FROM_QUEUE).apply()
|
||||
}
|
||||
if (oldVersion < 2050000) {
|
||||
prefs.edit().putBoolean(UserPreferences.PREF_PAUSE_PLAYBACK_FOR_FOCUS_LOSS, true).apply()
|
||||
}
|
||||
if (oldVersion < 2050000) prefs.edit().putBoolean(UserPreferences.PREF_PAUSE_PLAYBACK_FOR_FOCUS_LOSS, true).apply()
|
||||
|
||||
if (oldVersion < 2080000) {
|
||||
// Migrate drawer feed counter setting to reflect removal of
|
||||
// "unplayed and in inbox" (0), by changing it to "unplayed" (2)
|
||||
val feedCounterSetting = prefs.getString(UserPreferences.PREF_DRAWER_FEED_COUNTER, "2")
|
||||
if (feedCounterSetting == "0") {
|
||||
prefs.edit().putString(UserPreferences.PREF_DRAWER_FEED_COUNTER, "2").apply()
|
||||
}
|
||||
if (feedCounterSetting == "0") prefs.edit().putString(UserPreferences.PREF_DRAWER_FEED_COUNTER, "2").apply()
|
||||
|
||||
val sleepTimerPreferences =
|
||||
context.getSharedPreferences(SleepTimerPreferences.PREF_NAME, Context.MODE_PRIVATE)
|
||||
val sleepTimerPreferences = context.getSharedPreferences(SleepTimerPreferences.PREF_NAME, Context.MODE_PRIVATE)
|
||||
val timeUnits = arrayOf(TimeUnit.SECONDS, TimeUnit.MINUTES, TimeUnit.HOURS)
|
||||
val value = lastTimerValue()!!.toLong()
|
||||
val unit = timeUnits[sleepTimerPreferences.getInt("LastTimeUnit", 1)]
|
||||
setLastTimer(unit.toMinutes(value).toString())
|
||||
|
||||
if (prefs.getString(UserPreferences.PREF_EPISODE_CACHE_SIZE, "20")
|
||||
== context.getString(R.string.pref_episode_cache_unlimited)) {
|
||||
prefs.edit().putString(UserPreferences.PREF_EPISODE_CACHE_SIZE,
|
||||
"" + UserPreferences.EPISODE_CACHE_SIZE_UNLIMITED).apply()
|
||||
}
|
||||
if (prefs.getString(UserPreferences.PREF_EPISODE_CACHE_SIZE, "20") == context.getString(R.string.pref_episode_cache_unlimited))
|
||||
prefs.edit().putString(UserPreferences.PREF_EPISODE_CACHE_SIZE, "" + UserPreferences.EPISODE_CACHE_SIZE_UNLIMITED).apply()
|
||||
}
|
||||
if (oldVersion < 3000007) {
|
||||
if (prefs.getString("prefBackButtonBehavior", "") == "drawer") {
|
||||
if (prefs.getString("prefBackButtonBehavior", "") == "drawer")
|
||||
prefs.edit().putBoolean(UserPreferences.PREF_BACK_OPENS_DRAWER, true).apply()
|
||||
}
|
||||
}
|
||||
if (oldVersion < 3010000) {
|
||||
if (prefs.getString(UserPreferences.PREF_THEME, "system") == "2") {
|
||||
prefs.edit()
|
||||
|
@ -147,27 +129,20 @@ object PreferenceUpgrader {
|
|||
.apply()
|
||||
}
|
||||
isAllowMobileSync = true
|
||||
if (prefs.getString(UserPreferences.PREF_UPDATE_INTERVAL, ":")!!
|
||||
.contains(":")) { // Unset or "time of day"
|
||||
// Unset or "time of day"
|
||||
if (prefs.getString(UserPreferences.PREF_UPDATE_INTERVAL, ":")!!.contains(":"))
|
||||
prefs.edit().putString(UserPreferences.PREF_UPDATE_INTERVAL, "12").apply()
|
||||
|
||||
}
|
||||
}
|
||||
if (oldVersion < 3020000) {
|
||||
NotificationManagerCompat.from(context).deleteNotificationChannel("auto_download")
|
||||
}
|
||||
if (oldVersion < 3020000) NotificationManagerCompat.from(context).deleteNotificationChannel("auto_download")
|
||||
|
||||
if (oldVersion < 3030000) {
|
||||
val allEpisodesPreferences =
|
||||
context.getSharedPreferences(AllEpisodesFragment.PREF_NAME, Context.MODE_PRIVATE)
|
||||
val allEpisodesPreferences = context.getSharedPreferences(AllEpisodesFragment.PREF_NAME, Context.MODE_PRIVATE)
|
||||
val oldEpisodeSort = allEpisodesPreferences.getString(UserPreferences.PREF_SORT_ALL_EPISODES, "")
|
||||
if (!StringUtils.isAllEmpty(oldEpisodeSort)) {
|
||||
prefs.edit().putString(UserPreferences.PREF_SORT_ALL_EPISODES, oldEpisodeSort).apply()
|
||||
}
|
||||
if (!StringUtils.isAllEmpty(oldEpisodeSort)) prefs.edit().putString(UserPreferences.PREF_SORT_ALL_EPISODES, oldEpisodeSort).apply()
|
||||
|
||||
val oldEpisodeFilter = allEpisodesPreferences.getString("filter", "")
|
||||
if (!StringUtils.isAllEmpty(oldEpisodeFilter)) {
|
||||
prefs.edit().putString(UserPreferences.PREF_FILTER_ALL_EPISODES, oldEpisodeFilter).apply()
|
||||
}
|
||||
if (!StringUtils.isAllEmpty(oldEpisodeFilter)) prefs.edit().putString(UserPreferences.PREF_FILTER_ALL_EPISODES, oldEpisodeFilter).apply()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -103,14 +103,10 @@ object SleepTimerPreferences {
|
|||
@JvmStatic
|
||||
fun isInTimeRange(from: Int, to: Int, current: Int): Boolean {
|
||||
// Range covers one day
|
||||
if (from < to) {
|
||||
return from <= current && current < to
|
||||
}
|
||||
if (from < to) return from <= current && current < to
|
||||
|
||||
// Range covers two days
|
||||
if (from <= current) {
|
||||
return true
|
||||
}
|
||||
if (from <= current) return true
|
||||
|
||||
return current < to
|
||||
}
|
||||
|
|
|
@ -34,9 +34,7 @@ class ThemePreference : Preference {
|
|||
card.setCardBackgroundColor(if (theme == activeTheme) surfaceColorActive else surfaceColor)
|
||||
card.setOnClickListener {
|
||||
UserPreferences.theme = theme
|
||||
if (onPreferenceChangeListener != null) {
|
||||
onPreferenceChangeListener!!.onPreferenceChange(this, UserPreferences.theme)
|
||||
}
|
||||
onPreferenceChangeListener?.onPreferenceChange(this, UserPreferences.theme)
|
||||
updateUi()
|
||||
}
|
||||
}
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue