6.15.1 commit

This commit is contained in:
Xilin Jia 2024-12-01 15:46:59 +01:00
parent c2a6c71e18
commit f2f4b8dcee
23 changed files with 223 additions and 1786 deletions

View File

@ -26,8 +26,8 @@ android {
vectorDrawables.useSupportLibrary false
vectorDrawables.generatedDensities = []
versionCode 3020308
versionName "6.15.0"
versionCode 3020309
versionName "6.15.1"
applicationId "ac.mdiq.podcini.R"
def commit = ""

View File

@ -92,18 +92,6 @@
<meta-data android:name="android.service.quicksettings.TOGGLEABLE_TILE" android:value="true" />
</service>
<activity
android:name=".ui.activity.PlaybackSpeedDialogActivity"
android:noHistory="true"
android:exported="false"
android:excludeFromRecents="true"
android:theme="@style/Theme.Podcini.Light.Translucent">
<intent-filter>
<action android:name="ac.mdiq.podcini.intents.PLAYBACK_SPEED" />
<category android:name="android.intent.category.DEFAULT"/>
</intent-filter>
</activity>
<meta-data android:name="android.webkit.WebView.MetricsOptOut"
android:value="true"/>
<meta-data android:name="com.google.android.gms.car.notification.SmallIcon"
@ -168,28 +156,6 @@
</intent-filter>
</activity>
<activity
android:name=".ui.activity.WidgetConfigActivity"
android:label="@string/widget_settings"
android:exported="true">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_CONFIGUR"/>
</intent-filter>
</activity>
<receiver
android:name=".receiver.PlayerWidget"
android:exported="true">
<intent-filter>
<action android:name="android.appwidget.action.APPWIDGET_UPDATE"/>
<action android:name="ac.mdiq.podcini.FORCE_WIDGET_UPDATE"/>
<action android:name="ac.mdiq.podcini.STOP_WIDGET_UPDATE"/>
</intent-filter>
<meta-data
android:name="android.appwidget.provider"
android:resource="@xml/player_widget_info"/>
</receiver>
<activity
android:name=".ui.activity.BugReportActivity"
android:label="@string/bug_report_title">

View File

@ -27,7 +27,6 @@ import kotlinx.coroutines.launch
* Communicates with the playback service. GUI classes should use this class to
* control playback instead of communicating with the PlaybackService directly.
*/
abstract class ServiceStatusHandler(private val activity: FragmentActivity) {
private var mediaInfoLoaded = false
@ -150,8 +149,7 @@ abstract class ServiceStatusHandler(private val activity: FragmentActivity) {
when (MediaPlayerBase.status) {
PlayerStatus.PLAYING -> updatePlayButton(false)
PlayerStatus.PREPARING -> updatePlayButton(!PlaybackService.isStartWhenPrepared)
PlayerStatus.FALLBACK, PlayerStatus.PAUSED, PlayerStatus.PREPARED, PlayerStatus.STOPPED, PlayerStatus.INITIALIZED ->
updatePlayButton(true)
PlayerStatus.FALLBACK, PlayerStatus.PAUSED, PlayerStatus.PREPARED, PlayerStatus.STOPPED, PlayerStatus.INITIALIZED -> updatePlayButton(true)
else -> {}
}
}

View File

@ -46,9 +46,6 @@ import java.util.concurrent.CountDownLatch
import java.util.concurrent.TimeUnit
import kotlin.Throws
/**
* Manages the MediaPlayer object of the PlaybackService.
*/
class LocalMediaPlayer(context: Context, callback: MediaPlayerCallback) : MediaPlayerBase(context, callback) {
@Volatile
@ -84,14 +81,10 @@ class LocalMediaPlayer(context: Context, callback: MediaPlayerCallback) : MediaP
}
private val videoWidth: Int
get() {
return exoPlayer?.videoFormat?.width ?: 0
}
get() = exoPlayer?.videoFormat?.width ?: 0
private val videoHeight: Int
get() {
return exoPlayer?.videoFormat?.height ?: 0
}
get() = exoPlayer?.videoFormat?.height ?: 0
init {
if (httpDataSourceFactory == null) {
@ -137,15 +130,6 @@ class LocalMediaPlayer(context: Context, callback: MediaPlayerCallback) : MediaP
bufferingUpdateListener = null
}
// private fun setAudioStreamType(i: Int) {
// val a = exoPlayer!!.audioAttributes
// val b = AudioAttributes.Builder()
// b.setContentType(i)
// b.setFlags(a.flags)
// b.setUsage(a.usage)
// exoPlayer?.setAudioAttributes(b.build(), true)
// }
/**
* Starts or prepares playback of the specified Playable object. If another Playable object is already being played, the currently playing
* episode will be stopped and replaced with the new Playable object. If the Playable object is already being played, the method will not do anything.

View File

@ -30,11 +30,8 @@ import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.setValue
import androidx.media3.common.MediaItem
import androidx.media3.common.MediaMetadata
import androidx.media3.common.MimeTypes
import androidx.media3.datasource.DataSource
import androidx.media3.datasource.DataSpec
import androidx.media3.datasource.DefaultDataSource
import androidx.media3.datasource.DefaultHttpDataSource
import androidx.media3.datasource.okhttp.OkHttpDataSource
import androidx.media3.exoplayer.source.DefaultMediaSourceFactory
import androidx.media3.exoplayer.source.MediaSource
@ -44,6 +41,7 @@ import androidx.media3.extractor.DefaultExtractorsFactory
import androidx.media3.extractor.mp3.Mp3Extractor
import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicBoolean
import kotlin.Throws
import kotlin.math.max
/*

View File

@ -56,8 +56,6 @@ import ac.mdiq.podcini.storage.utils.ChapterUtils
import ac.mdiq.podcini.ui.activity.starter.MainActivityStarter
import ac.mdiq.podcini.ui.activity.starter.VideoPlayerActivityStarter
import ac.mdiq.podcini.ui.utils.NotificationUtils
import ac.mdiq.podcini.ui.widget.WidgetUpdater
import ac.mdiq.podcini.ui.widget.WidgetUpdater.WidgetState
import ac.mdiq.podcini.util.EventFlow
import ac.mdiq.podcini.util.FlowEvent
import ac.mdiq.podcini.util.FlowEvent.PlayEvent.Action
@ -229,9 +227,6 @@ class PlaybackService : MediaLibraryService() {
prevPosition = curPosition
}
}
override fun requestWidgetState(): WidgetState {
return WidgetState(curMedia, status, curPosition, curDuration, curSpeed)
}
override fun onChapterLoaded(media: Playable?) {
sendNotificationBroadcast(NOTIFICATION_TYPE_RELOAD, 0)
}
@ -313,7 +308,6 @@ class PlaybackService : MediaLibraryService() {
sendLocalBroadcast(applicationContext, ACTION_PLAYER_STATUS_CHANGED)
bluetoothNotifyChange(newInfo, AVRCP_ACTION_PLAYER_STATUS_CHANGED)
bluetoothNotifyChange(newInfo, AVRCP_ACTION_META_CHANGED)
taskManager.requestWidgetUpdate()
}
override fun onMediaChanged(reloadUI: Boolean) {
@ -398,7 +392,6 @@ class PlaybackService : MediaLibraryService() {
override fun onPlaybackStart(playable: Playable, position: Int) {
val delayInterval = positionUpdateInterval(playable.getDuration())
Logd(TAG, "onPlaybackStart position: $position delayInterval: $delayInterval")
taskManager.startWidgetUpdater(delayInterval)
// if (position != Playable.INVALID_TIME) playable.setPosition(position + (delayInterval/2).toInt())
if (position != Playable.INVALID_TIME) playable.setPosition(position)
else skipIntro(playable)
@ -410,7 +403,6 @@ class PlaybackService : MediaLibraryService() {
Logd(TAG, "onPlaybackPause $position")
taskManager.cancelPositionSaver()
persistCurrentPosition(position == Playable.INVALID_TIME || playable == null, playable, position)
taskManager.cancelWidgetUpdater()
if (playable != null) {
if (playable is EpisodeMedia) SynchronizationQueueSink.enqueueEpisodePlayedIfSyncActive(applicationContext, playable, false)
playable.onPlaybackPause(applicationContext)
@ -1303,7 +1295,7 @@ class PlaybackService : MediaLibraryService() {
/**
* Manages the background tasks of PlaybackSerivce, i.e.
* the sleep timer, the position saver, the widget updater and the queue loader.
* the sleep timer, the position saver, and the queue loader.
*
* The PlaybackServiceTaskManager(PSTM) uses a callback object (PSTMCallback)
* to notify the PlaybackService about updates from the running tasks.
@ -1316,7 +1308,6 @@ class PlaybackService : MediaLibraryService() {
}
private var positionSaverFuture: ScheduledFuture<*>? = null
private var widgetUpdaterFuture: ScheduledFuture<*>? = null
private var sleepTimerFuture: ScheduledFuture<*>? = null
private var sleepTimer: SleepTimer? = null
@ -1331,13 +1322,6 @@ class PlaybackService : MediaLibraryService() {
val sleepTimerTimeLeft: Long
get() = if (isSleepTimerActive) sleepTimer!!.getWaitingTime() else 0
/**
* Returns true if the widget updater is currently running.
*/
@get:Synchronized
val isWidgetUpdaterActive: Boolean
get() = widgetUpdaterFuture != null && !widgetUpdaterFuture!!.isCancelled && !widgetUpdaterFuture!!.isDone
@get:Synchronized
val isPositionSaverActive: Boolean
get() = positionSaverFuture != null && !positionSaverFuture!!.isCancelled && !positionSaverFuture!!.isDone
@ -1360,29 +1344,6 @@ class PlaybackService : MediaLibraryService() {
}
}
@Synchronized
fun startWidgetUpdater(delayInterval: Long) {
if (!isWidgetUpdaterActive && !schedExecutor.isShutdown) {
var widgetUpdater = Runnable { this.requestWidgetUpdate() }
widgetUpdater = useMainThreadIfNecessary(widgetUpdater)
// val delayInterval = positionUpdateInterval(duration)
// widgetUpdaterFuture = schedExecutor.scheduleWithFixedDelay(
// widgetUpdater, WIDGET_UPDATER_NOTIFICATION_INTERVAL.toLong(), WIDGET_UPDATER_NOTIFICATION_INTERVAL.toLong(), TimeUnit.MILLISECONDS)
widgetUpdaterFuture = schedExecutor.scheduleWithFixedDelay(widgetUpdater, delayInterval, delayInterval, TimeUnit.MILLISECONDS)
Logd(TAG, "Started WidgetUpdater")
}
}
/**
* Retrieves information about the widget state in the calling thread and then displays it in a background thread.
*/
@Synchronized
fun requestWidgetUpdate() {
val state = callback.requestWidgetState()
if (!schedExecutor.isShutdown) schedExecutor.execute { WidgetUpdater.updateWidget(context, state) }
else Logd(TAG, "Call to requestWidgetUpdate was ignored.")
}
/**
* Starts a new sleep timer with the given waiting time. If another sleep timer is already active, it will be
* cancelled first.
@ -1415,17 +1376,6 @@ class PlaybackService : MediaLibraryService() {
}
}
/**
* Cancels the widget updater. If the widget updater is not running, nothing will happen.
*/
@Synchronized
fun cancelWidgetUpdater() {
if (isWidgetUpdaterActive) {
widgetUpdaterFuture!!.cancel(false)
Logd(TAG, "Cancelled WidgetUpdater")
}
}
/**
* Starts a new thread that loads the chapter marks from a playable object. If another chapter loader is already active,
* it will be cancelled first.
@ -1452,7 +1402,6 @@ class PlaybackService : MediaLibraryService() {
@Synchronized
fun cancelAllTasks() {
cancelPositionSaver()
cancelWidgetUpdater()
disableSleepTimer()
// chapterLoaderFuture?.dispose()
// chapterLoaderFuture = null
@ -1539,7 +1488,6 @@ class PlaybackService : MediaLibraryService() {
interface PSTMCallback {
fun positionSaverTick()
fun requestWidgetState(): WidgetState
fun onChapterLoaded(media: Playable?)
}

View File

@ -44,7 +44,7 @@ class ExportWorker private constructor(private val exportWriter: ExportWriter, p
writer = OutputStreamWriter(FileOutputStream(output), Charset.forName("UTF-8"))
val feeds_ = feeds ?: getFeedList()
Logd(TAG, "feeds_: ${feeds_.size}")
exportWriter.writeDocument(feeds_, writer, context)
exportWriter.writeDocument(feeds_, writer!!, context)
output // return the output file
} catch (e: IOException) {
Log.e(TAG, "Error during file export", e)
@ -73,7 +73,7 @@ class DocumentFileExportWorker(private val exportWriter: ExportWriter, private v
writer = OutputStreamWriter(outputStream, Charset.forName("UTF-8"))
val feeds_ = feeds ?: getFeedList()
Logd("DocumentFileExportWorker", "feeds_: ${feeds_.size}")
exportWriter.writeDocument(feeds_, writer, context)
exportWriter.writeDocument(feeds_, writer!!, context)
output
} catch (e: IOException) { throw e
} finally {

View File

@ -7,7 +7,7 @@ import java.io.Writer
interface ExportWriter {
@Throws(IllegalArgumentException::class, IllegalStateException::class, IOException::class)
fun writeDocument(feeds: List<Feed>, writer: Writer?, context: Context)
fun writeDocument(feeds: List<Feed>, writer: Writer, context: Context)
fun fileExtension(): String?
}

View File

@ -64,7 +64,7 @@ class OpmlTransporter {
* Takes a list of feeds and a writer and writes those into an OPML document.
*/
@Throws(IllegalArgumentException::class, IllegalStateException::class, IOException::class)
override fun writeDocument(feeds: List<Feed>, writer: Writer?, context: Context) {
override fun writeDocument(feeds: List<Feed>, writer: Writer, context: Context) {
val xs = Xml.newSerializer()
xs.setFeature(OpmlSymbols.XML_FEATURE_INDENT_OUTPUT, true)
xs.setOutput(writer)

View File

@ -1,99 +0,0 @@
package ac.mdiq.podcini.receiver
import ac.mdiq.podcini.ui.widget.WidgetUpdaterWorker
import ac.mdiq.podcini.util.Logd
import android.appwidget.AppWidgetManager
import android.appwidget.AppWidgetProvider
import android.content.ComponentName
import android.content.Context
import android.content.SharedPreferences
import androidx.work.ExistingWorkPolicy
import androidx.work.OneTimeWorkRequest
import androidx.work.WorkManager
import java.util.concurrent.TimeUnit
class PlayerWidget : AppWidgetProvider() {
override fun onEnabled(context: Context) {
super.onEnabled(context)
getSharedPrefs(context)
Logd(TAG, "Widget enabled")
setEnabled(true)
WidgetUpdaterWorker.enqueueWork(context)
scheduleWorkaround(context)
}
override fun onUpdate(context: Context, appWidgetManager: AppWidgetManager, appWidgetIds: IntArray) {
Logd(TAG, "onUpdate() called with: context = [$context], appWidgetManager = [$appWidgetManager], appWidgetIds = [${appWidgetIds.contentToString()}]")
getSharedPrefs(context)
WidgetUpdaterWorker.enqueueWork(context)
if (!prefs!!.getBoolean(Prefs.WorkaroundEnabled.name, false)) {
scheduleWorkaround(context)
prefs!!.edit().putBoolean(Prefs.WorkaroundEnabled.name, true).apply()
}
}
override fun onDisabled(context: Context) {
super.onDisabled(context)
Logd(TAG, "Widget disabled")
setEnabled(false)
}
override fun onDeleted(context: Context, appWidgetIds: IntArray) {
Logd(TAG, "OnDeleted")
for (appWidgetId in appWidgetIds) {
prefs!!.edit().remove(Prefs.widget_color.name + appWidgetId).apply()
prefs!!.edit().remove(Prefs.widget_playback_speed.name + appWidgetId).apply()
prefs!!.edit().remove(Prefs.widget_rewind.name + appWidgetId).apply()
prefs!!.edit().remove(Prefs.widget_fast_forward.name + appWidgetId).apply()
prefs!!.edit().remove(Prefs.widget_skip.name + appWidgetId).apply()
}
val manager = AppWidgetManager.getInstance(context)
val widgetIds = manager.getAppWidgetIds(ComponentName(context, PlayerWidget::class.java))
if (widgetIds.isEmpty()) {
prefs!!.edit().putBoolean(Prefs.WorkaroundEnabled.name, false).apply()
WorkManager.getInstance(context).cancelUniqueWork(Prefs.WidgetUpdaterWorkaround.name)
}
super.onDeleted(context, appWidgetIds)
}
private fun setEnabled(enabled: Boolean) {
prefs!!.edit().putBoolean(Prefs.WidgetEnabled.name, enabled).apply()
}
enum class Prefs {
widget_color,
widget_playback_speed,
widget_skip,
widget_fast_forward,
widget_rewind,
WidgetUpdaterWorkaround,
WorkaroundEnabled,
WidgetEnabled
}
companion object {
private val TAG: String = PlayerWidget::class.simpleName ?: "Anonymous"
private const val PREFS_NAME: String = "PlayerWidgetPrefs"
const val DEFAULT_COLOR: Int = -0xd9d3cf
var prefs: SharedPreferences? = null
fun getSharedPrefs(context: Context) {
if (prefs == null) prefs = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
}
private fun scheduleWorkaround(context: Context) {
// Enqueueing work enables a BOOT_COMPLETED receiver, which in turn makes Android refresh widgets.
// This creates an endless loop with a flickering widget.
// Workaround: When there is a widget, schedule a dummy task in the far future, so that the receiver stays.
val workRequest: OneTimeWorkRequest = OneTimeWorkRequest.Builder(WidgetUpdaterWorker::class.java)
.setInitialDelay((100 * 356).toLong(), TimeUnit.DAYS)
.build()
WorkManager.getInstance(context).enqueueUniqueWork(Prefs.WidgetUpdaterWorkaround.name, ExistingWorkPolicy.REPLACE, workRequest)
}
@JvmStatic
fun isEnabled(): Boolean {
return prefs!!.getBoolean(Prefs.WidgetEnabled.name, false)
}
}
}

View File

@ -18,7 +18,6 @@ import ac.mdiq.podcini.preferences.UserPreferences.backButtonOpensDrawer
import ac.mdiq.podcini.preferences.UserPreferences.defaultPage
import ac.mdiq.podcini.preferences.UserPreferences.hiddenDrawerItems
import ac.mdiq.podcini.receiver.MediaButtonReceiver.Companion.createIntent
import ac.mdiq.podcini.receiver.PlayerWidget
import ac.mdiq.podcini.storage.database.Feeds.buildTags
import ac.mdiq.podcini.storage.database.Feeds.monitorFeeds
import ac.mdiq.podcini.storage.database.RealmDB.runOnIOScope
@ -187,7 +186,6 @@ class MainActivity : CastEnabledActivity() {
SwipeActions.getSharedPrefs(this@MainActivity)
buildTags()
monitorFeeds()
PlayerWidget.getSharedPrefs(this@MainActivity)
}
if (savedInstanceState != null) ensureGeneratedViewIdGreaterThan(savedInstanceState.getInt(Extras.generated_view_id.name, 0))

View File

@ -1,32 +0,0 @@
package ac.mdiq.podcini.ui.activity
import ac.mdiq.podcini.preferences.ThemeSwitcher.getTranslucentTheme
import ac.mdiq.podcini.ui.compose.PlaybackSpeedFullDialog
import android.os.Bundle
import android.view.ViewGroup
import androidx.appcompat.app.AppCompatActivity
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.platform.ComposeView
// This is for widget
class PlaybackSpeedDialogActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
setTheme(getTranslucentTheme(this))
super.onCreate(savedInstanceState)
val composeView = ComposeView(this).apply {
setContent {
var showSpeedDialog by remember { mutableStateOf(true) }
if (showSpeedDialog) PlaybackSpeedFullDialog(settingCode = booleanArrayOf(true, true, true), indexDefault = 0, maxSpeed = 3f,
onDismiss = {
showSpeedDialog = false
(parent as? ViewGroup)?.removeView(this)
finish()
})
}
}
(window.decorView as? ViewGroup)?.addView(composeView)
}
}

View File

@ -1,144 +0,0 @@
package ac.mdiq.podcini.ui.activity
import ac.mdiq.podcini.R
import ac.mdiq.podcini.databinding.ActivityWidgetConfigBinding
import ac.mdiq.podcini.databinding.PlayerWidgetBinding
import ac.mdiq.podcini.preferences.ThemeSwitcher.getTheme
import ac.mdiq.podcini.receiver.PlayerWidget
import ac.mdiq.podcini.receiver.PlayerWidget.Companion.prefs
import ac.mdiq.podcini.ui.widget.WidgetUpdaterWorker
import ac.mdiq.podcini.util.Logd
import android.appwidget.AppWidgetManager
import android.content.Intent
import android.graphics.Color
import android.os.Bundle
import android.view.View
import android.widget.CheckBox
import android.widget.SeekBar
import android.widget.SeekBar.OnSeekBarChangeListener
import android.widget.TextView
import androidx.appcompat.app.AppCompatActivity
import kotlin.math.roundToInt
class WidgetConfigActivity : AppCompatActivity() {
private var _binding: ActivityWidgetConfigBinding? = null
private val binding get() = _binding!!
private var _wpBinding: PlayerWidgetBinding? = null
private val wpBinding get() = _wpBinding!!
private var appWidgetId = AppWidgetManager.INVALID_APPWIDGET_ID
private lateinit var widgetPreview: View
private lateinit var opacitySeekBar: SeekBar
private lateinit var opacityTextView: TextView
private lateinit var ckPlaybackSpeed: CheckBox
private lateinit var ckRewind: CheckBox
private lateinit var ckFastForward: CheckBox
private lateinit var ckSkip: CheckBox
override fun onCreate(savedInstanceState: Bundle?) {
setTheme(getTheme(this))
super.onCreate(savedInstanceState)
_binding = ActivityWidgetConfigBinding.inflate(layoutInflater)
setContentView(binding.root)
val configIntent = intent
val extras = configIntent.extras
if (extras != null) appWidgetId = extras.getInt(AppWidgetManager.EXTRA_APPWIDGET_ID, AppWidgetManager.INVALID_APPWIDGET_ID)
val resultValue = Intent()
resultValue.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId)
setResult(RESULT_CANCELED, resultValue)
if (appWidgetId == AppWidgetManager.INVALID_APPWIDGET_ID) finish()
opacityTextView = binding.widgetOpacityTextView
opacitySeekBar = binding.widgetOpacitySeekBar
widgetPreview = binding.widgetConfigPreview.playerWidget
_wpBinding = PlayerWidgetBinding.bind(widgetPreview)
binding.butConfirm.setOnClickListener{ confirmCreateWidget() }
opacitySeekBar.setOnSeekBarChangeListener(object : OnSeekBarChangeListener {
override fun onProgressChanged(seekBar: SeekBar, i: Int, b: Boolean) {
opacityTextView.text = seekBar.progress.toString() + "%"
val color = getColorWithAlpha(PlayerWidget.DEFAULT_COLOR, opacitySeekBar.progress)
widgetPreview.setBackgroundColor(color)
}
override fun onStartTrackingTouch(seekBar: SeekBar) {}
override fun onStopTrackingTouch(seekBar: SeekBar) {}
})
wpBinding.txtNoPlaying.visibility = View.GONE
val title = wpBinding.txtvTitle
title.visibility = View.VISIBLE
title.setText(R.string.app_name)
val progress = wpBinding.txtvProgress
progress.visibility = View.VISIBLE
progress.setText(R.string.position_default_label)
ckPlaybackSpeed = binding.ckPlaybackSpeed
ckPlaybackSpeed.setOnClickListener { displayPreviewPanel() }
ckRewind = binding.ckRewind
ckRewind.setOnClickListener { displayPreviewPanel() }
ckFastForward = binding.ckFastForward
ckFastForward.setOnClickListener { displayPreviewPanel() }
ckSkip = binding.ckSkip
ckSkip.setOnClickListener { displayPreviewPanel() }
setInitialState()
}
override fun onDestroy() {
_binding = null
_wpBinding = null
super.onDestroy()
}
private fun setInitialState() {
PlayerWidget.getSharedPrefs(this)
// val prefs = getSharedPreferences(PlayerWidget.PREFS_NAME, MODE_PRIVATE)
ckPlaybackSpeed.isChecked = prefs!!.getBoolean(PlayerWidget.Prefs.widget_playback_speed.name + appWidgetId, true)
ckRewind.isChecked = prefs!!.getBoolean(PlayerWidget.Prefs.widget_rewind.name + appWidgetId, true)
ckFastForward.isChecked = prefs!!.getBoolean(PlayerWidget.Prefs.widget_fast_forward.name + appWidgetId, true)
ckSkip.isChecked = prefs!!.getBoolean(PlayerWidget.Prefs.widget_skip.name + appWidgetId, true)
val color = prefs!!.getInt(PlayerWidget.Prefs.widget_color.name + appWidgetId, PlayerWidget.DEFAULT_COLOR)
val opacity = Color.alpha(color) * 100 / 0xFF
opacitySeekBar.setProgress(opacity, false)
displayPreviewPanel()
}
private fun displayPreviewPanel() {
val showExtendedPreview = ckPlaybackSpeed.isChecked || ckRewind.isChecked || ckFastForward.isChecked || ckSkip.isChecked
wpBinding.extendedButtonsContainer.visibility = if (showExtendedPreview) View.VISIBLE else View.GONE
wpBinding.butPlay.visibility = if (showExtendedPreview) View.GONE else View.VISIBLE
wpBinding.butPlaybackSpeed.visibility = if (ckPlaybackSpeed.isChecked) View.VISIBLE else View.GONE
wpBinding.butFastForward.visibility = if (ckFastForward.isChecked) View.VISIBLE else View.GONE
wpBinding.butSkip.visibility = if (ckSkip.isChecked) View.VISIBLE else View.GONE
wpBinding.butRew.visibility = if (ckRewind.isChecked) View.VISIBLE else View.GONE
}
private fun confirmCreateWidget() {
val backgroundColor = getColorWithAlpha(PlayerWidget.DEFAULT_COLOR, opacitySeekBar.progress)
Logd("WidgetConfigActivity", "confirmCreateWidget appWidgetId $appWidgetId")
// val prefs = getSharedPreferences(PlayerWidget.PREFS_NAME, MODE_PRIVATE)
val editor = prefs!!.edit()
editor.putInt(PlayerWidget.Prefs.widget_color.name + appWidgetId, backgroundColor)
editor.putBoolean(PlayerWidget.Prefs.widget_playback_speed.name + appWidgetId, ckPlaybackSpeed.isChecked)
editor.putBoolean(PlayerWidget.Prefs.widget_skip.name + appWidgetId, ckSkip.isChecked)
editor.putBoolean(PlayerWidget.Prefs.widget_rewind.name + appWidgetId, ckRewind.isChecked)
editor.putBoolean(PlayerWidget.Prefs.widget_fast_forward.name + appWidgetId, ckFastForward.isChecked)
editor.apply()
val resultValue = Intent()
resultValue.putExtra(AppWidgetManager.EXTRA_APPWIDGET_ID, appWidgetId)
setResult(RESULT_OK, resultValue)
finish()
WidgetUpdaterWorker.enqueueWork(this)
}
private fun getColorWithAlpha(color: Int, opacity: Int): Int {
return (0xFF * (0.01 * opacity)).roundToInt() * 0x1000000 + (color and 0xffffff)
}
}

View File

@ -1,7 +1,5 @@
package ac.mdiq.podcini.ui.compose
import ac.mdiq.podcini.R
import ac.mdiq.podcini.preferences.UserPreferences
import ac.mdiq.podcini.preferences.UserPreferences.appPrefs
import androidx.compose.foundation.BorderStroke
import androidx.compose.foundation.background
@ -12,11 +10,12 @@ import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.BasicTextField
import androidx.compose.material3.*
import androidx.compose.runtime.*
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.res.vectorResource
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.input.TextFieldValue
@ -195,16 +194,28 @@ fun SimpleSwitchDialog(title: String, text: String, onDismissRequest: ()->Unit,
}
@Composable
fun TitleSummaryColumn(titleRes: Int, summaryRes: Int, callback: ()-> Unit) {
fun IconTitleSummaryActionRow(vecRes: Int, titleRes: Int, summaryRes: Int, callback: ()-> Unit) {
val textColor = MaterialTheme.colorScheme.onSurface
Column(modifier = Modifier.fillMaxWidth().padding(start = 16.dp, top = 10.dp).clickable(onClick = { callback() })) {
Text(stringResource(titleRes), color = textColor, style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.Bold)
Text(stringResource(summaryRes), color = textColor)
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth().padding(start = 10.dp, top = 10.dp)) {
Icon(imageVector = ImageVector.vectorResource(vecRes), contentDescription = "", tint = textColor, modifier = Modifier.size(40.dp).padding(end = 15.dp))
Column(modifier = Modifier.weight(1f).clickable(onClick = { callback() })) {
Text(stringResource(titleRes), color = textColor, style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.Bold)
Text(stringResource(summaryRes), color = textColor)
}
}
}
@Composable
fun TitleSummarySwitchRow(titleRes: Int, summaryRes: Int, prefName: String) {
fun TitleSummaryActionColumn(titleRes: Int, summaryRes: Int, callback: ()-> Unit) {
val textColor = MaterialTheme.colorScheme.onSurface
Column(modifier = Modifier.fillMaxWidth().padding(start = 16.dp, top = 10.dp).clickable(onClick = { callback() })) {
Text(stringResource(titleRes), color = textColor, style = MaterialTheme.typography.titleLarge, fontWeight = FontWeight.Bold)
if (summaryRes != 0) Text(stringResource(summaryRes), color = textColor)
}
}
@Composable
fun TitleSummarySwitchPrefRow(titleRes: Int, summaryRes: Int, prefName: String) {
val textColor = MaterialTheme.colorScheme.onSurface
Row(verticalAlignment = Alignment.CenterVertically, modifier = Modifier.fillMaxWidth().padding(start = 16.dp, top = 10.dp)) {
Column(modifier = Modifier.weight(1f)) {
@ -216,4 +227,5 @@ fun TitleSummarySwitchRow(titleRes: Int, summaryRes: Int, prefName: String) {
isChecked = it
appPrefs.edit().putBoolean(prefName, it).apply() })
}
}
}

View File

@ -223,9 +223,8 @@ class AudioPlayerFragment : Fragment() {
if (playbackService == null) PlaybackServiceStarter(requireContext(), curMedia!!).start()
}
val imgLoc_ = remember(currentMedia) { imgLoc }
AsyncImage(model = ImageRequest.Builder(context).data(imgLoc_)
AsyncImage(contentDescription = "imgvCover", model = ImageRequest.Builder(context).data(imgLoc_)
.memoryCachePolicy(CachePolicy.ENABLED).placeholder(R.mipmap.ic_launcher).error(R.mipmap.ic_launcher).build(),
contentDescription = "imgvCover",
modifier = Modifier.width(65.dp).height(65.dp).padding(start = 5.dp)
.clickable(onClick = {
Logd(TAG, "playerUiFragment icon was clicked")
@ -249,10 +248,7 @@ class AudioPlayerFragment : Fragment() {
Spacer(Modifier.weight(0.1f))
Column(horizontalAlignment = Alignment.CenterHorizontally) {
SpeedometerWithArc(speed = curPlaybackSpeed*100, maxSpeed = 300f, trackColor = textColor,
modifier = Modifier.width(43.dp).height(43.dp).clickable(onClick = {
showSpeedDialog = true
// VariableSpeedDialog.newInstance(booleanArrayOf(true, true, true), null)?.show(childFragmentManager, null)
}))
modifier = Modifier.width(43.dp).height(43.dp).clickable(onClick = { showSpeedDialog = true }))
// Icon(imageVector = ImageVector.vectorResource(R.drawable.ic_playback_speed), tint = textColor, contentDescription = "speed",
// modifier = Modifier.width(43.dp).height(43.dp).clickable(onClick = {
// VariableSpeedDialog.newInstance(booleanArrayOf(true, true, true), null)?.show(childFragmentManager, null)
@ -264,8 +260,7 @@ class AudioPlayerFragment : Fragment() {
var showSkipDialog by remember { mutableStateOf(false) }
var rewindSecs by remember { mutableStateOf(NumberFormat.getInstance().format(UserPreferences.rewindSecs.toLong())) }
if (showSkipDialog) SkipDialog(SkipDirection.SKIP_REWIND, onDismissRequest = { showSkipDialog = false }) { rewindSecs = it.toString() }
Icon(imageVector = ImageVector.vectorResource(R.drawable.ic_fast_rewind), tint = textColor,
contentDescription = "rewind",
Icon(imageVector = ImageVector.vectorResource(R.drawable.ic_fast_rewind), tint = textColor, contentDescription = "rewind",
modifier = Modifier.width(43.dp).height(43.dp).combinedClickable(
onClick = { playbackService?.mPlayer?.seekDelta(-UserPreferences.rewindSecs * 1000) },
onLongClick = { showSkipDialog = true }))
@ -273,31 +268,29 @@ class AudioPlayerFragment : Fragment() {
}
Spacer(Modifier.weight(0.1f))
Icon(imageVector = ImageVector.vectorResource(playButRes), tint = textColor, contentDescription = "play",
modifier = Modifier.width(64.dp).height(64.dp).combinedClickable(onClick = {
if (controller == null) return@combinedClickable
if (curMedia != null) {
val media = curMedia!!
setIsShowPlay(!isShowPlay)
if (media.getMediaType() == MediaType.VIDEO && status != PlayerStatus.PLAYING &&
(media is EpisodeMedia && media.episode?.feed?.preferences?.videoModePolicy != VideoMode.AUDIO_ONLY)) {
playPause()
requireContext().startActivity(getPlayerActivityIntent(requireContext(), curMedia!!.getMediaType()))
} else playPause()
}
}, onLongClick = {
// if (controller != null && status == PlayerStatus.PLAYING) {
if (status == PlayerStatus.PLAYING) {
val fallbackSpeed = UserPreferences.fallbackSpeed
if (fallbackSpeed > 0.1f) toggleFallbackSpeed(fallbackSpeed)
}
}))
modifier = Modifier.width(64.dp).height(64.dp).combinedClickable(
onClick = {
if (controller == null) return@combinedClickable
if (curMedia != null) {
val media = curMedia!!
setIsShowPlay(!isShowPlay)
if (media.getMediaType() == MediaType.VIDEO && status != PlayerStatus.PLAYING &&
(media is EpisodeMedia && media.episode?.feed?.preferences?.videoModePolicy != VideoMode.AUDIO_ONLY)) {
playPause()
requireContext().startActivity(getPlayerActivityIntent(requireContext(), curMedia!!.getMediaType()))
} else playPause()
} },
onLongClick = {
if (status == PlayerStatus.PLAYING) {
val fallbackSpeed = UserPreferences.fallbackSpeed
if (fallbackSpeed > 0.1f) toggleFallbackSpeed(fallbackSpeed)
} }))
Spacer(Modifier.weight(0.1f))
Column(horizontalAlignment = Alignment.CenterHorizontally) {
var showSkipDialog by remember { mutableStateOf(false) }
var fastForwardSecs by remember { mutableStateOf(NumberFormat.getInstance().format(UserPreferences.fastForwardSecs.toLong())) }
if (showSkipDialog) SkipDialog(SkipDirection.SKIP_FORWARD, onDismissRequest = {showSkipDialog = false }) { fastForwardSecs = it.toString()}
Icon(imageVector = ImageVector.vectorResource(R.drawable.ic_fast_forward), tint = textColor,
contentDescription = "forward",
Icon(imageVector = ImageVector.vectorResource(R.drawable.ic_fast_forward), tint = textColor, contentDescription = "forward",
modifier = Modifier.width(43.dp).height(43.dp).combinedClickable(
onClick = { playbackService?.mPlayer?.seekDelta(UserPreferences.fastForwardSecs * 1000) },
onLongClick = { showSkipDialog = true }))
@ -313,17 +306,14 @@ class AudioPlayerFragment : Fragment() {
} else playbackService?.mPlayer?.setPlaybackParams(playbackService!!.normalSpeed, isSkipSilence)
playbackService!!.isSpeedForward = !playbackService!!.isSpeedForward
}
Icon(imageVector = ImageVector.vectorResource(R.drawable.ic_skip_48dp), tint = textColor,
contentDescription = "rewind",
modifier = Modifier.width(43.dp).height(43.dp).combinedClickable(onClick = {
// if (controller != null && status == PlayerStatus.PLAYING) {
if (status == PlayerStatus.PLAYING) {
val speedForward = UserPreferences.speedforwardSpeed
if (speedForward > 0.1f) speedForward(speedForward)
}
}, onLongClick = {
activity?.sendBroadcast(MediaButtonReceiver.createIntent(requireContext(), KeyEvent.KEYCODE_MEDIA_NEXT))
}))
Icon(imageVector = ImageVector.vectorResource(R.drawable.ic_skip_48dp), tint = textColor, contentDescription = "rewind",
modifier = Modifier.width(43.dp).height(43.dp).combinedClickable(
onClick = {
if (status == PlayerStatus.PLAYING) {
val speedForward = UserPreferences.speedforwardSpeed
if (speedForward > 0.1f) speedForward(speedForward)
} },
onLongClick = { activity?.sendBroadcast(MediaButtonReceiver.createIntent(requireContext(), KeyEvent.KEYCODE_MEDIA_NEXT)) }))
if (UserPreferences.speedforwardSpeed > 0.1f) Text(NumberFormat.getInstance().format(UserPreferences.speedforwardSpeed), color = textColor, style = MaterialTheme.typography.bodySmall)
}
Spacer(Modifier.weight(0.1f))
@ -382,9 +372,6 @@ class AudioPlayerFragment : Fragment() {
Logd(TAG, "row clicked: $item $selectedOption")
if (item != selectedOption) {
onOptionSelected(item)
// currentItem = upsertBlk(currentItem!!) {
// it.media?.volumeAdaptionSetting = item
// }
if (currentMedia is EpisodeMedia) {
(currentMedia as? EpisodeMedia)?.volumeAdaptionSetting = item
currentMedia = currentItem!!.media
@ -454,20 +441,14 @@ class AudioPlayerFragment : Fragment() {
}
})
Icon(imageVector = ImageVector.vectorResource(R.drawable.ic_volume_adaption), tint = textColor, contentDescription = "Volume adaptation", modifier = Modifier.clickable {
if (currentItem != null) {
showVolumeDialog = true
}
if (currentItem != null) showVolumeDialog = true
})
Icon(imageVector = ImageVector.vectorResource(R.drawable.baseline_offline_share_24), tint = textColor, contentDescription = "Share Note", modifier = Modifier.clickable {
val notes = if (showHomeText) readerhtml else feedItem?.description
if (!notes.isNullOrEmpty()) {
val shareText = HtmlCompat.fromHtml(notes, HtmlCompat.FROM_HTML_MODE_COMPACT).toString()
val context = requireContext()
val intent = ShareCompat.IntentBuilder(context)
.setType("text/plain")
.setText(shareText)
.setChooserTitle(R.string.share_notes_label)
.createChooserIntent()
val intent = ShareCompat.IntentBuilder(context).setType("text/plain").setText(shareText).setChooserTitle(R.string.share_notes_label).createChooserIntent()
context.startActivity(intent)
}
})
@ -479,9 +460,7 @@ class AudioPlayerFragment : Fragment() {
@Composable
fun DetailUI(modifier: Modifier) {
var showChooseRatingDialog by remember { mutableStateOf(false) }
if (showChooseRatingDialog) ChooseRatingDialog(listOf(currentItem!!)) {
showChooseRatingDialog = false
}
if (showChooseRatingDialog) ChooseRatingDialog(listOf(currentItem!!)) { showChooseRatingDialog = false }
var showChaptersDialog by remember { mutableStateOf(false) }
if (showChaptersDialog) ChaptersDialog(media = currentMedia!!, onDismissRequest = {showChaptersDialog = false})
@ -491,9 +470,8 @@ class AudioPlayerFragment : Fragment() {
fun copyText(text: String): Boolean {
val clipboardManager: ClipboardManager? = ContextCompat.getSystemService(requireContext(), ClipboardManager::class.java)
clipboardManager?.setPrimaryClip(ClipData.newPlainText("Podcini", text))
if (Build.VERSION.SDK_INT <= 32) {
if (Build.VERSION.SDK_INT <= 32)
(requireActivity() as MainActivity).showSnackbarAbovePlayer(resources.getString(R.string.copied_to_clipboard), Snackbar.LENGTH_SHORT)
}
return true
}
Text(txtvPodcastTitle, textAlign = TextAlign.Center, color = textColor, style = MaterialTheme.typography.headlineSmall,
@ -547,10 +525,7 @@ class AudioPlayerFragment : Fragment() {
postDelayed({ }, 50)
}
}
}, update = { webView ->
// Logd(TAG, "AndroidView update: $cleanedNotes")
webView.loadDataWithBaseURL("https://127.0.0.1", cleanedNotes?:"No notes", "text/html", "utf-8", "about:blank")
})
}, update = { webView -> webView.loadDataWithBaseURL("https://127.0.0.1", cleanedNotes?:"No notes", "text/html", "utf-8", "about:blank") })
if (displayedChapterIndex >= 0) {
Row(modifier = Modifier.padding(start = 20.dp, end = 20.dp),
horizontalArrangement = Arrangement.Center, verticalAlignment = Alignment.CenterVertically) {
@ -567,8 +542,7 @@ class AudioPlayerFragment : Fragment() {
}
}
AsyncImage(model = imgLocLarge, contentDescription = "imgvCover", placeholder = painterResource(R.mipmap.ic_launcher), error = painterResource(R.mipmap.ic_launcher),
modifier = Modifier.fillMaxWidth().padding(start = 32.dp, end = 32.dp, top = 10.dp).clickable(onClick = {
}))
modifier = Modifier.fillMaxWidth().padding(start = 32.dp, end = 32.dp, top = 10.dp).clickable(onClick = {}))
}
}
@ -628,10 +602,8 @@ class AudioPlayerFragment : Fragment() {
onPositionUpdate(FlowEvent.PlaybackPositionEvent(media, media.getPosition(), media.getDuration()))
if (prevMedia?.getIdentifier() != media.getIdentifier()) imgLoc = ImageResourceUtils.getEpisodeListImageLocation(media)
if (isPlayingVideoLocally && (curMedia as? EpisodeMedia)?.episode?.feed?.preferences?.videoModePolicy != VideoMode.AUDIO_ONLY) {
// (activity as MainActivity).bottomSheet.setLocked(true)
(activity as MainActivity).bottomSheet.state = BottomSheetBehavior.STATE_COLLAPSED
}
// else (activity as MainActivity).bottomSheet.setLocked(false)
prevMedia = media
}
@ -694,36 +666,18 @@ class AudioPlayerFragment : Fragment() {
val article = readability4J.parse()
readerhtml = article.contentWithDocumentsCharsetOrUtf8
if (!readerhtml.isNullOrEmpty()) {
currentItem = upsertBlk(currentItem!!) {
it.setTranscriptIfLonger(readerhtml)
}
currentItem = upsertBlk(currentItem!!) { it.setTranscriptIfLonger(readerhtml) }
homeText = currentItem!!.transcript
// persistEpisode(currentItem)
}
}
if (!homeText.isNullOrEmpty()) {
// val shownotesCleaner = ShownotesCleaner(requireContext())
cleanedNotes = shownotesCleaner?.processShownotes(homeText!!, 0)
withContext(Dispatchers.Main) {
// shownoteView.loadDataWithBaseURL("https://127.0.0.1",
// cleanedNotes ?: "No notes",
// "text/html",
// "UTF-8",
// null)
}
// withContext(Dispatchers.Main) {}
} else withContext(Dispatchers.Main) { Toast.makeText(context, R.string.web_content_not_available, Toast.LENGTH_LONG).show() }
} else {
// val shownotesCleaner = ShownotesCleaner(requireContext())
cleanedNotes = shownotesCleaner?.processShownotes(currentItem?.description ?: "", currentMedia?.getDuration() ?: 0)
if (!cleanedNotes.isNullOrEmpty()) {
withContext(Dispatchers.Main) {
// shownoteView.loadDataWithBaseURL("https://127.0.0.1",
// cleanedNotes ?: "No notes",
// "text/html",
// "UTF-8",
// null)
}
} else withContext(Dispatchers.Main) { Toast.makeText(context, R.string.web_content_not_available, Toast.LENGTH_LONG).show() }
if (cleanedNotes.isNullOrEmpty())
withContext(Dispatchers.Main) { Toast.makeText(context, R.string.web_content_not_available, Toast.LENGTH_LONG).show() }
}
}
}
@ -782,8 +736,6 @@ class AudioPlayerFragment : Fragment() {
Logd(TAG, "Saving preferences")
val editor = prefs.edit() ?: return
if (curMedia != null) {
// Logd(TAG, "Saving scroll position: " + binding.itemDescriptionFragment.scrollY)
// editor.putInt(PREF_SCROLL_Y, binding.itemDescriptionFragment.scrollY)
editor.putString(PREF_PLAYABLE_ID, curMedia!!.getIdentifier().toString())
} else {
Logd(TAG, "savePreferences was called while media or webview was null")
@ -799,7 +751,6 @@ class AudioPlayerFragment : Fragment() {
// if (isCollapsed) {
isCollapsed = false
if (shownotesCleaner == null) shownotesCleaner = ShownotesCleaner(requireContext())
// showPlayer1 = false
// if (currentMedia != null) updateUi(currentMedia!!)
setIsShowPlay(isShowPlay)
updateDetails()
@ -809,7 +760,6 @@ class AudioPlayerFragment : Fragment() {
fun onCollaped() {
Logd(TAG, "onCollaped()")
isCollapsed = true
// showPlayer1 = true
// if (currentMedia != null) updateUi(currentMedia!!)
setIsShowPlay(isShowPlay)
}
@ -882,7 +832,6 @@ class AudioPlayerFragment : Fragment() {
private fun createHandler(): ServiceStatusHandler {
return object : ServiceStatusHandler(requireActivity()) {
override fun updatePlayButton(showPlay: Boolean) {
// isShowPlay = showPlay
setIsShowPlay(showPlay)
}
override fun loadMediaInfo() {
@ -890,18 +839,12 @@ class AudioPlayerFragment : Fragment() {
// if (!isCollapsed) updateDetails()
}
override fun onPlaybackEnd() {
// isShowPlay = true
setIsShowPlay(true)
(activity as MainActivity).setPlayerVisible(false)
}
}
}
// override fun onCreate(savedInstanceState: Bundle?) {
// super.onCreate(savedInstanceState)
// retainInstance = true
// }
override fun onResume() {
Logd(TAG, "onResume() isCollapsed: $isCollapsed")
super.onResume()
@ -1016,10 +959,7 @@ class AudioPlayerFragment : Fragment() {
}
private fun onRatingEvent(event: FlowEvent.RatingEvent) {
if (curEpisode?.id == event.episode.id) {
rating = event.rating
// EpisodeMenuHandler.onPrepareMenu(toolbar.menu, event.episode)
}
if (curEpisode?.id == event.episode.id) rating = event.rating
}
// fun scrollToTop() {

View File

@ -1,232 +0,0 @@
package ac.mdiq.podcini.ui.widget
import ac.mdiq.podcini.R
import ac.mdiq.podcini.playback.base.PlayerStatus
import ac.mdiq.podcini.preferences.UserPreferences.shouldShowRemainingTime
import ac.mdiq.podcini.receiver.MediaButtonReceiver.Companion.createPendingIntent
import ac.mdiq.podcini.receiver.PlayerWidget
import ac.mdiq.podcini.receiver.PlayerWidget.Companion.isEnabled
import ac.mdiq.podcini.receiver.PlayerWidget.Companion.prefs
import ac.mdiq.podcini.storage.model.MediaType
import ac.mdiq.podcini.storage.model.Playable
import ac.mdiq.podcini.storage.utils.DurationConverter.getDurationStringLong
import ac.mdiq.podcini.storage.utils.ImageResourceUtils.getFallbackImageLocation
import ac.mdiq.podcini.storage.utils.TimeSpeedConverter
import ac.mdiq.podcini.ui.activity.starter.MainActivityStarter
import ac.mdiq.podcini.ui.activity.starter.VideoPlayerActivityStarter
import ac.mdiq.podcini.util.Logd
import android.app.PendingIntent
import android.appwidget.AppWidgetManager
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.graphics.Bitmap
import android.graphics.drawable.BitmapDrawable
import android.util.Log
import android.view.KeyEvent
import android.view.View
import android.widget.RemoteViews
import coil.imageLoader
import coil.request.ErrorResult
import coil.request.ImageRequest
import coil.request.SuccessResult
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import kotlin.math.max
object WidgetUpdater {
private val TAG: String = WidgetUpdater::class.simpleName ?: "Anonymous"
/**
* Update the widgets with the given parameters. Must be called in a background thread.
*/
fun updateWidget(context: Context, widgetState: WidgetState?) {
if (!isEnabled() || widgetState == null) return
Logd(TAG, "in updateWidget")
val startMediaPlayer =
if (widgetState.media != null && widgetState.media.getMediaType() === MediaType.VIDEO) VideoPlayerActivityStarter(context).pendingIntent
else MainActivityStarter(context).withOpenPlayer().pendingIntent
val startPlaybackSpeedDialog = PlaybackSpeedActivityStarter(context).pendingIntent
val views = RemoteViews(context.packageName, R.layout.player_widget)
if (widgetState.media != null) {
var icon: Bitmap? = null
val iconSize = context.resources.getDimensionPixelSize(android.R.dimen.app_icon_size)
views.setOnClickPendingIntent(R.id.layout_left, startMediaPlayer)
views.setOnClickPendingIntent(R.id.imgvCover, startMediaPlayer)
views.setOnClickPendingIntent(R.id.butPlaybackSpeed, startPlaybackSpeedDialog)
try {
val imgLoc = widgetState.media.getImageLocation()
val imgLoc1 = getFallbackImageLocation(widgetState.media)
CoroutineScope(Dispatchers.IO).launch {
val request = ImageRequest.Builder(context)
.data(imgLoc)
.setHeader("User-Agent", "Mozilla/5.0")
.placeholder(R.color.light_gray)
.listener(object : ImageRequest.Listener {
override fun onError(request: ImageRequest, throwable: ErrorResult) {
CoroutineScope(Dispatchers.IO).launch {
val fallbackImageRequest = ImageRequest.Builder(context)
.data(imgLoc1)
.setHeader("User-Agent", "Mozilla/5.0")
.error(R.mipmap.ic_launcher)
.size(iconSize, iconSize)
.build()
val result = (context.imageLoader.execute(fallbackImageRequest) as SuccessResult).drawable
icon = (result as BitmapDrawable).bitmap
}
}
})
.size(iconSize, iconSize)
.build()
withContext(Dispatchers.Main) {
val result = (context.imageLoader.execute(request) as SuccessResult).drawable
icon = (result as BitmapDrawable).bitmap
try {
if (icon != null) views.setImageViewBitmap(R.id.imgvCover, icon)
else views.setImageViewResource(R.id.imgvCover, R.mipmap.ic_launcher)
} catch(e: Exception) {
Log.e(TAG, e.message?:"")
e.printStackTrace()
}
}
}
} catch (tr1: Throwable) {
Log.e(TAG, "Error loading the media icon for the widget", tr1)
}
views.setTextViewText(R.id.txtvTitle, widgetState.media.getEpisodeTitle())
views.setViewVisibility(R.id.txtvTitle, View.VISIBLE)
views.setViewVisibility(R.id.txtNoPlaying, View.GONE)
val progressString = getProgressString(widgetState.position, widgetState.duration, widgetState.playbackSpeed)
if (progressString != null) {
views.setViewVisibility(R.id.txtvProgress, View.VISIBLE)
views.setTextViewText(R.id.txtvProgress, progressString)
}
if (widgetState.status == PlayerStatus.PLAYING) {
views.setImageViewResource(R.id.butPlay, R.drawable.ic_widget_pause)
views.setContentDescription(R.id.butPlay, context.getString(R.string.pause_label))
views.setImageViewResource(R.id.butPlayExtended, R.drawable.ic_widget_pause)
views.setContentDescription(R.id.butPlayExtended, context.getString(R.string.pause_label))
} else {
views.setImageViewResource(R.id.butPlay, R.drawable.ic_widget_play)
views.setContentDescription(R.id.butPlay, context.getString(R.string.play_label))
views.setImageViewResource(R.id.butPlayExtended, R.drawable.ic_widget_play)
views.setContentDescription(R.id.butPlayExtended, context.getString(R.string.play_label))
}
views.setOnClickPendingIntent(R.id.butPlay, createPendingIntent(context, KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE))
views.setOnClickPendingIntent(R.id.butPlayExtended, createPendingIntent(context, KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE))
views.setOnClickPendingIntent(R.id.butRew, createPendingIntent(context, KeyEvent.KEYCODE_MEDIA_REWIND))
views.setOnClickPendingIntent(R.id.butFastForward, createPendingIntent(context, KeyEvent.KEYCODE_MEDIA_FAST_FORWARD))
views.setOnClickPendingIntent(R.id.butSkip, createPendingIntent(context, KeyEvent.KEYCODE_MEDIA_NEXT))
} else {
// start the app if they click anything
views.setOnClickPendingIntent(R.id.layout_left, startMediaPlayer)
views.setOnClickPendingIntent(R.id.butPlay, startMediaPlayer)
views.setOnClickPendingIntent(R.id.butPlayExtended, createPendingIntent(context, KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE))
views.setViewVisibility(R.id.txtvProgress, View.GONE)
views.setViewVisibility(R.id.txtvTitle, View.GONE)
views.setViewVisibility(R.id.txtNoPlaying, View.VISIBLE)
views.setImageViewResource(R.id.imgvCover, R.mipmap.ic_launcher)
views.setImageViewResource(R.id.butPlay, R.drawable.ic_widget_play)
views.setImageViewResource(R.id.butPlayExtended, R.drawable.ic_widget_play)
}
val playerWidget = ComponentName(context, PlayerWidget::class.java)
val manager = AppWidgetManager.getInstance(context)
val widgetIds = manager.getAppWidgetIds(playerWidget)
for (id in widgetIds) {
Logd(TAG, "updating widget $id")
val options = manager.getAppWidgetOptions(id)
// val prefs = context.getSharedPreferences(PlayerWidget.PREFS_NAME, Context.MODE_PRIVATE)
val minWidth = options.getInt(AppWidgetManager.OPTION_APPWIDGET_MIN_WIDTH)
val columns = getCellsForSize(minWidth)
if (columns < 3) views.setViewVisibility(R.id.layout_center, View.INVISIBLE)
else views.setViewVisibility(R.id.layout_center, View.VISIBLE)
val showPlaybackSpeed = prefs!!.getBoolean(PlayerWidget.Prefs.widget_playback_speed.name + id, true)
val showRewind = prefs!!.getBoolean(PlayerWidget.Prefs.widget_rewind.name + id, true)
val showFastForward = prefs!!.getBoolean(PlayerWidget.Prefs.widget_fast_forward.name + id, true)
val showSkip = prefs!!.getBoolean(PlayerWidget.Prefs.widget_skip.name + id, true)
if (showPlaybackSpeed || showRewind || showSkip || showFastForward) {
views.setInt(R.id.extendedButtonsContainer, "setVisibility", View.VISIBLE)
views.setInt(R.id.butPlay, "setVisibility", View.GONE)
views.setInt(R.id.butPlaybackSpeed, "setVisibility", if (showPlaybackSpeed) View.VISIBLE else View.GONE)
views.setInt(R.id.butRew, "setVisibility", if (showRewind) View.VISIBLE else View.GONE)
views.setInt(R.id.butFastForward, "setVisibility", if (showFastForward) View.VISIBLE else View.GONE)
views.setInt(R.id.butSkip, "setVisibility", if (showSkip) View.VISIBLE else View.GONE)
} else {
views.setInt(R.id.extendedButtonsContainer, "setVisibility", View.GONE)
views.setInt(R.id.butPlay, "setVisibility", View.VISIBLE)
}
val backgroundColor = prefs!!.getInt(PlayerWidget.Prefs.widget_color.name + id, PlayerWidget.DEFAULT_COLOR)
views.setInt(R.id.widgetLayout, "setBackgroundColor", backgroundColor)
manager.updateAppWidget(id, views)
}
}
/**
* Returns number of cells needed for given size of the widget.
*
* @param size Widget size in dp.
* @return Size in number of cells.
*/
private fun getCellsForSize(size: Int): Int {
var n = 2
while (70 * n - 30 < size) {
++n
}
return n - 1
}
private fun getProgressString(position: Int, duration: Int, speed: Float): String? {
if (position < 0 || duration <= 0) return null
val converter = TimeSpeedConverter(speed)
return if (shouldShowRemainingTime())
("${getDurationStringLong(converter.convert(position))} / -${getDurationStringLong(converter.convert(max(0.0, (duration - position).toDouble()).toInt()))}")
else (getDurationStringLong(converter.convert(position)) + " / " + getDurationStringLong(converter.convert(duration)))
}
class WidgetState(val media: Playable?, val status: PlayerStatus, val position: Int, val duration: Int, val playbackSpeed: Float) {
constructor(status: PlayerStatus) : this(null, status, Playable.INVALID_TIME, Playable.INVALID_TIME, 1.0f)
}
/**
* Launches the playback speed dialog activity of the app with specific arguments.
* Does not require a dependency on the actual implementation of the activity.
*/
class PlaybackSpeedActivityStarter(private val context: Context) {
val intent: Intent = Intent(INTENT)
init {
intent.setPackage(context.packageName)
intent.addFlags(Intent.FLAG_ACTIVITY_NEW_DOCUMENT)
}
val pendingIntent: PendingIntent
get() = PendingIntent.getActivity(context, R.id.pending_intent_playback_speed, intent,
PendingIntent.FLAG_UPDATE_CURRENT or PendingIntent.FLAG_IMMUTABLE)
fun start() {
context.startActivity(intent)
}
companion object {
const val INTENT: String = "ac.mdiq.podcini.intents.PLAYBACK_SPEED"
}
}
}

View File

@ -1,39 +0,0 @@
package ac.mdiq.podcini.ui.widget
import ac.mdiq.podcini.playback.base.InTheatre.curMedia
import ac.mdiq.podcini.playback.base.MediaPlayerBase.Companion.getCurrentPlaybackSpeed
import ac.mdiq.podcini.playback.base.PlayerStatus
import ac.mdiq.podcini.ui.widget.WidgetUpdater.WidgetState
import ac.mdiq.podcini.util.Logd
import android.content.Context
import androidx.work.*
class WidgetUpdaterWorker(context: Context, workerParams: WorkerParameters) : Worker(context, workerParams) {
override fun doWork(): Result {
try { updateWidget()
} catch (e: Exception) {
Logd(TAG, "Failed to update Podcini widget: $e")
return Result.failure()
}
return Result.success()
}
/**
* Loads the current media from the database and updates the widget in a background job.
*/
private fun updateWidget() {
val media = curMedia
if (media != null) WidgetUpdater.updateWidget(applicationContext, WidgetState(media, PlayerStatus.STOPPED, media.getPosition(), media.getDuration(), getCurrentPlaybackSpeed(media)))
else WidgetUpdater.updateWidget(applicationContext, WidgetState(PlayerStatus.STOPPED))
}
companion object {
private val TAG: String = WidgetUpdaterWorker::class.simpleName ?: "Anonymous"
fun enqueueWork(context: Context) {
val workRequest: OneTimeWorkRequest = OneTimeWorkRequest.Builder(WidgetUpdaterWorker::class.java).build()
WorkManager.getInstance(context).enqueueUniqueWork(TAG, ExistingWorkPolicy.REPLACE, workRequest)
}
}
}

View File

@ -1,125 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<LinearLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
xmlns:app="http://schemas.android.com/apk/res-auto"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:id="@+id/activity_widget_config"
tools:context="ac.mdiq.podcini.ui.activity.WidgetConfigActivity">
<FrameLayout
android:layout_width="match_parent"
android:layout_height="200dp"
android:layout_gravity="center">
<ImageView
android:id="@+id/widget_config_background"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:scaleType="centerCrop"
app:srcCompat="@drawable/teaser" />
<include
android:id="@+id/widget_config_preview"
android:layout_width="match_parent"
android:layout_height="96dp"
android:layout_gravity="center"
android:layout_margin="16dp"
layout="@layout/player_widget" />
</FrameLayout>
<androidx.core.widget.NestedScrollView
android:layout_width="match_parent"
android:layout_height="wrap_content">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="match_parent"
android:orientation="vertical"
android:padding="16dp">
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:gravity="center_vertical"
android:orientation="horizontal">
<TextView
android:id="@+id/textView"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:text="@string/widget_opacity"
android:textSize="16sp"
android:textColor="?android:attr/textColorPrimary" />
<TextView
android:id="@+id/widget_opacity_textView"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
android:gravity="end"
android:text="100%"
android:textSize="16sp"
android:textColor="?android:attr/textColorSecondary" />
</LinearLayout>
<SeekBar
android:id="@+id/widget_opacity_seekBar"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginTop="16dp"
android:layout_marginBottom="16dp"
android:max="100"
android:progress="100" />
<LinearLayout
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="vertical">
<CheckBox
android:id="@+id/ckPlaybackSpeed"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/playback_speed" />
<CheckBox
android:id="@+id/ckRewind"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/rewind_label" />
<CheckBox
android:id="@+id/ckFastForward"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/fast_forward_label" />
<CheckBox
android:id="@+id/ckSkip"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_weight="1"
android:text="@string/skip_episode_label" />
</LinearLayout>
<Button
android:id="@+id/butConfirm"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="center"
android:text="@string/widget_create_button" />
</LinearLayout>
</androidx.core.widget.NestedScrollView>
</LinearLayout>

View File

@ -1,160 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<FrameLayout
xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:id="@+id/player_widget"
android:padding="@dimen/widget_margin">
<RelativeLayout
android:id="@+id/widgetLayout"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:background="#262C31"
tools:ignore="UselessParent">
<TextView
android:id="@+id/txtvTitle"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_alignParentTop="true"
android:layout_marginStart="12dp"
android:gravity="center"
android:maxLines="1"
android:ellipsize="end"
android:textColor="@color/white"
android:textSize="15sp"
android:textStyle="bold"
android:visibility="gone" />
<ImageButton
android:id="@+id/butPlay"
android:layout_width="@android:dimen/app_icon_size"
android:layout_height="match_parent"
android:contentDescription="@string/play_label"
android:layout_alignParentEnd="true"
android:layout_below="@id/txtvTitle"
android:layout_marginHorizontal="12dp"
android:background="?android:attr/selectableItemBackground"
android:scaleType="fitCenter"
android:padding="8dp"
android:src="@drawable/ic_widget_play" />
<LinearLayout
android:id="@+id/layout_left"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_alignParentStart="true"
android:layout_below="@id/txtvTitle"
android:layout_toStartOf="@id/butPlay"
android:background="@android:color/transparent"
android:gravity="fill_horizontal"
android:orientation="horizontal">
<ImageView
android:id="@+id/imgvCover"
android:layout_width="@android:dimen/app_icon_size"
android:layout_height="match_parent"
android:src="@mipmap/ic_launcher"
android:importantForAccessibility="no"
android:layout_marginStart="12dp"
android:layout_marginEnd="12dp"/>
<LinearLayout
android:id="@+id/layout_center"
android:layout_width="match_parent"
android:layout_height="match_parent"
android:gravity="center_vertical"
android:orientation="vertical">
<TextView
android:id="@+id/txtNoPlaying"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:maxLines="3"
android:text="@string/no_media_playing_label"
android:textColor="@color/white"
android:textSize="16sp"
android:textStyle="bold" />
<TextView
android:id="@+id/txtvProgress"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="4dp"
android:textColor="@color/white"
android:textSize="14sp"
android:visibility="gone" />
<LinearLayout
android:id="@+id/extendedButtonsContainer"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:orientation="horizontal"
android:visibility="gone">
<ImageButton
android:id="@+id/butPlaybackSpeed"
android:layout_width="36dp"
android:layout_height="36dp"
android:layout_weight="1"
android:background="?android:attr/selectableItemBackground"
android:contentDescription="@string/playback_speed"
android:layout_marginEnd="2dp"
android:scaleType="centerInside"
android:src="@drawable/ic_widget_playback_speed" />
<ImageButton
android:id="@+id/butRew"
android:layout_width="36dp"
android:layout_height="36dp"
android:layout_weight="1"
android:background="?android:attr/selectableItemBackground"
android:contentDescription="@string/rewind_label"
android:layout_marginEnd="2dp"
android:scaleType="centerInside"
android:src="@drawable/ic_widget_fast_rewind" />
<ImageButton
android:id="@+id/butPlayExtended"
android:layout_width="36dp"
android:layout_height="36dp"
android:layout_weight="1"
android:background="?android:attr/selectableItemBackground"
android:contentDescription="@string/play_label"
android:layout_marginEnd="2dp"
android:scaleType="centerInside"
android:src="@drawable/ic_widget_play" />
<ImageButton
android:id="@+id/butFastForward"
android:layout_width="36dp"
android:layout_height="36dp"
android:layout_weight="1"
android:background="?android:attr/selectableItemBackground"
android:contentDescription="@string/fast_forward_label"
android:layout_marginEnd="2dp"
android:scaleType="centerInside"
android:src="@drawable/ic_widget_fast_forward" />
<ImageButton
android:id="@+id/butSkip"
android:layout_width="36dp"
android:layout_height="36dp"
android:layout_weight="1"
android:background="?android:attr/selectableItemBackground"
android:contentDescription="@string/skip_episode_label"
android:layout_marginEnd="2dp"
android:scaleType="centerInside"
android:src="@drawable/ic_widget_skip" />
</LinearLayout>
</LinearLayout>
</LinearLayout>
</RelativeLayout>
</FrameLayout>

View File

@ -1,13 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<appwidget-provider
xmlns:android="http://schemas.android.com/apk/res/android"
android:resizeMode="horizontal|vertical"
android:initialLayout="@layout/player_widget"
android:updatePeriodMillis="86400000"
android:previewImage="@drawable/ic_widget_preview"
android:minHeight="40dp"
android:minWidth="100dp"
android:minResizeWidth="70dp"
android:configure="ac.mdiq.podcini.ui.activity.WidgetConfigActivity"
android:widgetFeatures="reconfigurable">
</appwidget-provider>

View File

@ -1,3 +1,9 @@
# 6.15.1
* Consolidated Compose code blocks in PreferenceActivity with function calls
* removed support for widget
* some minor cleanups
# 6.15.0
* added a combo Epiosdes fragment with easy access to various filters

View File

@ -0,0 +1,5 @@
Version 6.15.1
* Consolidated Compose code blocks in PreferenceActivity with function calls
* removed support for widget
* some minor cleanups