4.9.3 commit

This commit is contained in:
Xilin Jia 2024-04-25 11:47:28 +01:00
parent ae82900ae0
commit 9476d7d681
301 changed files with 2687 additions and 5976 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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
}
// report success if never reported before
if (downloadResults.isEmpty()) return true
downloadResults.sortWith { downloadStatus1: DownloadResult, downloadStatus2: DownloadResult ->
downloadStatus1.getCompletionDate().compareTo(downloadStatus2.getCompletionDate())

View File

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

View File

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

View File

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

View File

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

View File

@ -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.")

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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 <= ' ' }
}
}

View File

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

View File

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

View File

@ -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
}
// less than 16kb is suspicious, check manually
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()
}
}
}

View File

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

View File

@ -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 <= ' ' }
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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]

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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()
}
}

View File

@ -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
}
// they didn't tell us the size, but we don't want to keep querying on it
if (size <= 0) media.setCheckedOnSizeButUnknown()
else media.size = size
emitter.onSuccess(size)
DBWriter.setFeedMedia(media)
})

View File

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

View File

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

View File

@ -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()
}
}

View File

@ -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()
}
// This also happens when the worker was preempted, not just when the user cancelled it
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 (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)))
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)))
}
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))

View File

@ -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,23 +161,18 @@ 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)
}
}
}
}

View File

@ -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()
}
}

View File

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

View File

@ -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")
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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")
}
}

View File

@ -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 -> {
scheme = "https" // assume https
}
scheme == null && port == 80 -> scheme = "http"
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
}
}

View File

@ -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)
}
// Do not spam users with notification and retry before notifying
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)
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -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) {
/**
* URLs that should be updated. The key of the map is the original URL, the value of the map
* is the sanitized URL.
*/
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)
}

View File

@ -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]")
}
}

View File

@ -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) {
/**
* URLs that should be updated. The key of the map is the original URL, the value of the map
* is the sanitized URL.
*/
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 {

View File

@ -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()
}

View File

@ -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}")
}
}

View File

@ -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?

View File

@ -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]")
}
}

View File

@ -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
)
/**
* timestamp/ID that can be used for requesting changes since this upload.
*/
abstract class UploadChangesResponse(@JvmField val timestamp: Long)

View File

@ -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]

View File

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

View File

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

View File

@ -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()
}

View File

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

View File

@ -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()))
}
}
}

View File

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

View File

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

View File

@ -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)
// Still did not get back the audio focus. Now actually pause.
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 {

View File

@ -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())
// TODO: what's this?
// mediaSession!!.setSessionActivity(PendingIntent.getActivity(this, R.id.pending_intent_player_activity,
// getPlayerActivityIntent(this), FLAG_IMMUTABLE))
// 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()) {
// 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))
// 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())
// }
// 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"

View File

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

View File

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

View File

@ -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 = null
}
shakeListener?.pause()
shakeListener = null
hasVibrated = false
}
}
@ -303,10 +296,8 @@ class PlaybackServiceTaskManager(private val context: Context, private val callb
fun restart() {
EventBus.getDefault().post(SleepTimerUpdatedEvent.cancelled())
setSleepTimer(waitingTime)
if (shakeListener != null) {
shakeListener!!.pause()
shakeListener = null
}
shakeListener?.pause()
shakeListener = null
}
fun cancel() {

View File

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

View File

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

View File

@ -31,10 +31,8 @@ internal class ShakeListener(private val mContext: Context, private val mSleepTi
}
fun pause() {
if (mSensorMgr != null) {
mSensorMgr!!.unregisterListener(this)
mSensorMgr = null
}
mSensorMgr?.unregisterListener(this)
mSensorMgr = null
}
override fun onSensorChanged(event: SensorEvent) {
@ -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

View File

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

View File

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

View File

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

View File

@ -48,10 +48,9 @@ object PreferenceUpgrader {
}
private fun upgrade(oldVersion: Int, context: Context) {
if (oldVersion == -1) {
//New installation
return
}
//New installation
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,48 +89,37 @@ 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") {
@ -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()
}
}
}

View File

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

View File

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