play flavor converted
This commit is contained in:
parent
6baf126a0d
commit
82c498c466
|
@ -64,7 +64,7 @@ class UITestUtils(private val context: Context) {
|
|||
|
||||
@Throws(IOException::class)
|
||||
fun hostFeed(feed: Feed): String {
|
||||
val feedFile = File(hostedFeedDir, feed.title)
|
||||
val feedFile = File(hostedFeedDir, feed.title?:"")
|
||||
val out = FileOutputStream(feedFile)
|
||||
val generator = Rss2Generator()
|
||||
generator.writeFeed(feed, out, "UTF-8", 0)
|
||||
|
@ -171,7 +171,7 @@ class UITestUtils(private val context: Context) {
|
|||
for (feed in hostedFeeds) {
|
||||
feed.setDownloaded(true)
|
||||
if (downloadEpisodes) {
|
||||
for (item in feed.items!!) {
|
||||
for (item in feed.items) {
|
||||
if (item.hasMedia()) {
|
||||
val media = item.media
|
||||
val fileId = StringUtils.substringAfter(media!!.download_url, "files/").toInt()
|
||||
|
@ -181,9 +181,9 @@ class UITestUtils(private val context: Context) {
|
|||
}
|
||||
}
|
||||
|
||||
queue.add(feed.items!![0])
|
||||
if (feed.items!![1].hasMedia()) {
|
||||
feed.items!![1].media!!.setPlaybackCompletionDate(Date())
|
||||
queue.add(feed.items[0])
|
||||
if (feed.items[1].hasMedia()) {
|
||||
feed.items[1].media!!.setPlaybackCompletionDate(Date())
|
||||
}
|
||||
}
|
||||
localFeedDataAdded = true
|
||||
|
|
|
@ -11,11 +11,13 @@ import ac.mdiq.podvinci.core.storage.DBWriter
|
|||
import ac.mdiq.podvinci.core.util.download.FeedUpdateManager.runOnce
|
||||
import ac.mdiq.podvinci.databinding.EditTextDialogBinding
|
||||
import ac.mdiq.podvinci.model.feed.Feed
|
||||
import androidx.media3.common.util.UnstableApi
|
||||
import java.lang.ref.WeakReference
|
||||
import java.util.*
|
||||
import java.util.concurrent.ExecutionException
|
||||
|
||||
abstract class EditUrlSettingsDialog(activity: Activity, private val feed: Feed) {
|
||||
@UnstableApi
|
||||
abstract class EditUrlSettingsDialog(activity: Activity, private val feed: Feed) {
|
||||
private val activityRef = WeakReference(activity)
|
||||
|
||||
fun show() {
|
||||
|
@ -33,7 +35,7 @@ abstract class EditUrlSettingsDialog(activity: Activity, private val feed: Feed)
|
|||
.show()
|
||||
}
|
||||
|
||||
private fun onConfirmed(original: String, updated: String) {
|
||||
@UnstableApi private fun onConfirmed(original: String, updated: String) {
|
||||
try {
|
||||
DBWriter.updateFeedDownloadURL(original, updated).get()
|
||||
feed.download_url = updated
|
||||
|
@ -45,7 +47,7 @@ abstract class EditUrlSettingsDialog(activity: Activity, private val feed: Feed)
|
|||
}
|
||||
}
|
||||
|
||||
private fun showConfirmAlertDialog(url: String) {
|
||||
@UnstableApi private fun showConfirmAlertDialog(url: String) {
|
||||
val activity = activityRef.get()
|
||||
|
||||
val alertDialog = MaterialAlertDialogBuilder(activity!!)
|
||||
|
|
|
@ -1,6 +1,19 @@
|
|||
package ac.mdiq.podvinci.fragment
|
||||
|
||||
import ac.mdiq.podvinci.R
|
||||
import ac.mdiq.podvinci.activity.MainActivity
|
||||
import ac.mdiq.podvinci.core.storage.DBReader
|
||||
import ac.mdiq.podvinci.core.storage.DBTasks
|
||||
import ac.mdiq.podvinci.core.util.IntentUtils
|
||||
import ac.mdiq.podvinci.core.util.ShareUtils
|
||||
import ac.mdiq.podvinci.core.util.syndication.HtmlToPlainText
|
||||
import ac.mdiq.podvinci.dialog.EditUrlSettingsDialog
|
||||
import ac.mdiq.podvinci.model.feed.Feed
|
||||
import ac.mdiq.podvinci.model.feed.FeedFunding
|
||||
import ac.mdiq.podvinci.ui.glide.FastBlurTransformation
|
||||
import ac.mdiq.podvinci.ui.statistics.StatisticsFragment
|
||||
import ac.mdiq.podvinci.ui.statistics.feed.FeedStatisticsFragment
|
||||
import ac.mdiq.podvinci.view.ToolbarIconTintManager
|
||||
import android.R.string
|
||||
import android.app.Activity
|
||||
import android.content.*
|
||||
|
@ -31,23 +44,9 @@ import com.google.android.material.appbar.CollapsingToolbarLayout
|
|||
import com.google.android.material.appbar.MaterialToolbar
|
||||
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||
import com.google.android.material.snackbar.Snackbar
|
||||
import ac.mdiq.podvinci.R
|
||||
import ac.mdiq.podvinci.core.storage.DBReader
|
||||
import ac.mdiq.podvinci.core.storage.DBTasks
|
||||
import ac.mdiq.podvinci.core.util.IntentUtils
|
||||
import ac.mdiq.podvinci.core.util.ShareUtils
|
||||
import ac.mdiq.podvinci.core.util.syndication.HtmlToPlainText
|
||||
import ac.mdiq.podvinci.dialog.EditUrlSettingsDialog
|
||||
import ac.mdiq.podvinci.model.feed.Feed
|
||||
import ac.mdiq.podvinci.model.feed.FeedFunding
|
||||
import ac.mdiq.podvinci.ui.glide.FastBlurTransformation
|
||||
import ac.mdiq.podvinci.ui.statistics.StatisticsFragment
|
||||
import ac.mdiq.podvinci.ui.statistics.feed.FeedStatisticsFragment
|
||||
import ac.mdiq.podvinci.view.ToolbarIconTintManager
|
||||
import io.reactivex.Completable
|
||||
import io.reactivex.Maybe
|
||||
import io.reactivex.MaybeEmitter
|
||||
import io.reactivex.MaybeOnSubscribe
|
||||
import io.reactivex.android.schedulers.AndroidSchedulers
|
||||
import io.reactivex.disposables.Disposable
|
||||
import io.reactivex.schedulers.Schedulers
|
||||
|
@ -148,14 +147,14 @@ class FeedInfoFragment : Fragment(), Toolbar.OnMenuItemClickListener {
|
|||
|
||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||
val feedId = requireArguments().getLong(EXTRA_FEED_ID)
|
||||
disposable = Maybe.create<Feed>(MaybeOnSubscribe<Feed> { emitter: MaybeEmitter<Feed?> ->
|
||||
disposable = Maybe.create { emitter: MaybeEmitter<Feed?> ->
|
||||
val feed: Feed? = DBReader.getFeed(feedId)
|
||||
if (feed != null) {
|
||||
emitter.onSuccess(feed)
|
||||
} else {
|
||||
emitter.onComplete()
|
||||
}
|
||||
} as MaybeOnSubscribe<Feed?>?)
|
||||
}
|
||||
.subscribeOn(Schedulers.io())
|
||||
.observeOn(AndroidSchedulers.mainThread())
|
||||
.subscribe({ result: Feed? ->
|
||||
|
@ -211,12 +210,12 @@ class FeedInfoFragment : Fragment(), Toolbar.OnMenuItemClickListener {
|
|||
txtvUrl?.text = feed!!.download_url
|
||||
txtvUrl?.setCompoundDrawablesRelativeWithIntrinsicBounds(0, 0, R.drawable.ic_paperclip, 0)
|
||||
|
||||
if (feed!!.paymentLinks == null || feed!!.paymentLinks!!.size == 0) {
|
||||
if (feed!!.paymentLinks.isEmpty()) {
|
||||
lblSupport?.visibility = View.GONE
|
||||
txtvFundingUrl?.visibility = View.GONE
|
||||
} else {
|
||||
lblSupport?.visibility = View.VISIBLE
|
||||
val fundingList: ArrayList<FeedFunding> = feed!!.paymentLinks!!
|
||||
val fundingList: ArrayList<FeedFunding> = feed!!.paymentLinks
|
||||
|
||||
// Filter for duplicates, but keep items in the order that they have in the feed.
|
||||
val i: MutableIterator<FeedFunding> = fundingList.iterator()
|
||||
|
@ -311,8 +310,7 @@ class FeedInfoFragment : Fragment(), Toolbar.OnMenuItemClickListener {
|
|||
Completable.fromAction {
|
||||
requireActivity().contentResolver
|
||||
.takePersistableUriPermission(uri, Intent.FLAG_GRANT_READ_URI_PERMISSION)
|
||||
val documentFile = DocumentFile.fromTreeUri(
|
||||
requireContext(), uri)
|
||||
val documentFile = DocumentFile.fromTreeUri(requireContext(), uri)
|
||||
requireNotNull(documentFile) { "Unable to retrieve document tree" }
|
||||
feed?.download_url = Feed.PREFIX_LOCAL_FOLDER + uri.toString()
|
||||
DBTasks.updateFeed(requireContext(), feed!!, true)
|
||||
|
|
|
@ -1,123 +0,0 @@
|
|||
package ac.mdiq.podvinci.dialog;
|
||||
|
||||
import android.app.Activity;
|
||||
|
||||
import android.content.Context;
|
||||
import android.content.SharedPreferences;
|
||||
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
import android.util.Log;
|
||||
|
||||
import java.lang.ref.WeakReference;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import com.google.android.play.core.review.ReviewInfo;
|
||||
import com.google.android.play.core.review.ReviewManager;
|
||||
import com.google.android.play.core.review.ReviewManagerFactory;
|
||||
import com.google.android.play.core.tasks.Task;
|
||||
|
||||
import ac.mdiq.podvinci.BuildConfig;
|
||||
|
||||
public class RatingDialog {
|
||||
|
||||
private RatingDialog() {
|
||||
}
|
||||
|
||||
private static final String TAG = RatingDialog.class.getSimpleName();
|
||||
private static final int AFTER_DAYS = 14;
|
||||
|
||||
private static WeakReference<Context> mContext;
|
||||
private static SharedPreferences mPreferences;
|
||||
|
||||
private static final String PREFS_NAME = "RatingPrefs";
|
||||
private static final String KEY_RATED = "KEY_WAS_RATED";
|
||||
private static final String KEY_FIRST_START_DATE = "KEY_FIRST_HIT_DATE";
|
||||
private static final String KEY_NUMBER_OF_REVIEWS = "NUMBER_OF_REVIEW_ATTEMPTS";
|
||||
|
||||
public static void init(Context context) {
|
||||
mContext = new WeakReference<>(context);
|
||||
mPreferences = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE);
|
||||
|
||||
long firstDate = mPreferences.getLong(KEY_FIRST_START_DATE, 0);
|
||||
if (firstDate == 0) {
|
||||
resetStartDate();
|
||||
}
|
||||
}
|
||||
|
||||
public static void check() {
|
||||
if (shouldShow()) {
|
||||
try {
|
||||
showInAppReview();
|
||||
} catch (Exception e) {
|
||||
Log.e(TAG, Log.getStackTraceString(e));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void showInAppReview() {
|
||||
Context context = mContext.get();
|
||||
if (context == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
ReviewManager manager = ReviewManagerFactory.create(context);
|
||||
Task<ReviewInfo> request = manager.requestReviewFlow();
|
||||
|
||||
request.addOnCompleteListener(task -> {
|
||||
if (task.isSuccessful()) {
|
||||
ReviewInfo reviewInfo = task.getResult();
|
||||
Task<Void> flow = manager.launchReviewFlow((Activity) context, reviewInfo);
|
||||
flow.addOnCompleteListener(task1 -> {
|
||||
int previousAttempts = mPreferences.getInt(KEY_NUMBER_OF_REVIEWS, 0);
|
||||
if (previousAttempts >= 3) {
|
||||
saveRated();
|
||||
} else {
|
||||
resetStartDate();
|
||||
mPreferences
|
||||
.edit()
|
||||
.putInt(KEY_NUMBER_OF_REVIEWS, previousAttempts + 1)
|
||||
.apply();
|
||||
}
|
||||
Log.i("ReviewDialog", "Successfully finished in-app review");
|
||||
})
|
||||
.addOnFailureListener(error -> {
|
||||
Log.i("ReviewDialog", "failed in reviewing process");
|
||||
});
|
||||
}
|
||||
})
|
||||
.addOnFailureListener(error -> {
|
||||
Log.i("ReviewDialog", "failed to get in-app review request");
|
||||
});
|
||||
}
|
||||
|
||||
private static boolean rated() {
|
||||
return mPreferences.getBoolean(KEY_RATED, false);
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
public static void saveRated() {
|
||||
mPreferences
|
||||
.edit()
|
||||
.putBoolean(KEY_RATED, true)
|
||||
.apply();
|
||||
}
|
||||
|
||||
private static void resetStartDate() {
|
||||
mPreferences
|
||||
.edit()
|
||||
.putLong(KEY_FIRST_START_DATE, System.currentTimeMillis())
|
||||
.apply();
|
||||
}
|
||||
|
||||
private static boolean shouldShow() {
|
||||
if (rated() || BuildConfig.DEBUG) {
|
||||
return false;
|
||||
}
|
||||
|
||||
long now = System.currentTimeMillis();
|
||||
long firstDate = mPreferences.getLong(KEY_FIRST_START_DATE, now);
|
||||
long diff = now - firstDate;
|
||||
long diffDays = TimeUnit.DAYS.convert(diff, TimeUnit.MILLISECONDS);
|
||||
return diffDays >= AFTER_DAYS;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,111 @@
|
|||
package ac.mdiq.podvinci.dialog
|
||||
|
||||
import ac.mdiq.podvinci.BuildConfig
|
||||
import android.app.Activity
|
||||
import android.content.Context
|
||||
import android.content.SharedPreferences
|
||||
import android.util.Log
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import com.google.android.play.core.review.ReviewInfo
|
||||
import com.google.android.play.core.review.ReviewManager
|
||||
import com.google.android.play.core.review.ReviewManagerFactory
|
||||
import com.google.android.play.core.tasks.Task
|
||||
import java.lang.ref.WeakReference
|
||||
import java.util.concurrent.TimeUnit
|
||||
|
||||
object RatingDialog {
|
||||
private val TAG: String = RatingDialog::class.java.simpleName
|
||||
private const val AFTER_DAYS = 14
|
||||
|
||||
private var mContext: WeakReference<Context>? = null
|
||||
private lateinit var mPreferences: SharedPreferences
|
||||
|
||||
private const val PREFS_NAME = "RatingPrefs"
|
||||
private const val KEY_RATED = "KEY_WAS_RATED"
|
||||
private const val KEY_FIRST_START_DATE = "KEY_FIRST_HIT_DATE"
|
||||
private const val KEY_NUMBER_OF_REVIEWS = "NUMBER_OF_REVIEW_ATTEMPTS"
|
||||
|
||||
fun init(context: Context) {
|
||||
mContext = WeakReference(context)
|
||||
mPreferences = context.getSharedPreferences(PREFS_NAME, Context.MODE_PRIVATE)
|
||||
|
||||
val firstDate: Long = mPreferences.getLong(KEY_FIRST_START_DATE, 0)
|
||||
if (firstDate == 0L) {
|
||||
resetStartDate()
|
||||
}
|
||||
}
|
||||
|
||||
fun check() {
|
||||
if (shouldShow()) {
|
||||
try {
|
||||
showInAppReview()
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, Log.getStackTraceString(e))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun showInAppReview() {
|
||||
val context = mContext!!.get() ?: return
|
||||
|
||||
val manager: ReviewManager = ReviewManagerFactory.create(context)
|
||||
val request: Task<ReviewInfo> = manager.requestReviewFlow()
|
||||
|
||||
request.addOnCompleteListener { task: Task<ReviewInfo?> ->
|
||||
if (task.isSuccessful) {
|
||||
val reviewInfo: ReviewInfo = task.result
|
||||
val flow: Task<Void?> = manager.launchReviewFlow(context as Activity, reviewInfo)
|
||||
flow.addOnCompleteListener { task1: Task<Void?>? ->
|
||||
val previousAttempts: Int = mPreferences.getInt(KEY_NUMBER_OF_REVIEWS, 0)
|
||||
if (previousAttempts >= 3) {
|
||||
saveRated()
|
||||
} else {
|
||||
resetStartDate()
|
||||
mPreferences
|
||||
.edit()
|
||||
.putInt(KEY_NUMBER_OF_REVIEWS, previousAttempts + 1)
|
||||
.apply()
|
||||
}
|
||||
Log.i("ReviewDialog", "Successfully finished in-app review")
|
||||
}
|
||||
.addOnFailureListener { error: Exception? ->
|
||||
Log.i("ReviewDialog", "failed in reviewing process")
|
||||
}
|
||||
}
|
||||
}
|
||||
.addOnFailureListener { error: Exception? ->
|
||||
Log.i("ReviewDialog", "failed to get in-app review request")
|
||||
}
|
||||
}
|
||||
|
||||
private fun rated(): Boolean {
|
||||
return mPreferences.getBoolean(KEY_RATED, false)
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
fun saveRated() {
|
||||
mPreferences
|
||||
.edit()
|
||||
.putBoolean(KEY_RATED, true)
|
||||
.apply()
|
||||
}
|
||||
|
||||
private fun resetStartDate() {
|
||||
mPreferences
|
||||
.edit()
|
||||
.putLong(KEY_FIRST_START_DATE, System.currentTimeMillis())
|
||||
.apply()
|
||||
}
|
||||
|
||||
private fun shouldShow(): Boolean {
|
||||
if (rated() || BuildConfig.DEBUG) {
|
||||
return false
|
||||
}
|
||||
|
||||
val now = System.currentTimeMillis()
|
||||
val firstDate: Long = mPreferences.getLong(KEY_FIRST_START_DATE, now)
|
||||
val diff = now - firstDate
|
||||
val diffDays = TimeUnit.DAYS.convert(diff, TimeUnit.MILLISECONDS)
|
||||
return diffDays >= AFTER_DAYS
|
||||
}
|
||||
}
|
|
@ -43,7 +43,7 @@ object LocalFeedUpdater {
|
|||
fun updateFeed(feed: Feed, context: Context,
|
||||
updaterProgressListener: UpdaterProgressListener?
|
||||
) {
|
||||
if (feed.download_url == null) return
|
||||
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))
|
||||
|
@ -71,9 +71,6 @@ object LocalFeedUpdater {
|
|||
updaterProgressListener: UpdaterProgressListener?
|
||||
) {
|
||||
var feed = feed
|
||||
if (feed.items == null) {
|
||||
feed.items = mutableListOf()
|
||||
}
|
||||
//make sure it is the latest 'version' of this feed from the db (all items etc)
|
||||
feed = DBTasks.updateFeed(context, feed, false)?: feed
|
||||
|
||||
|
|
|
@ -31,27 +31,29 @@ class BasicAuthorizationInterceptor : Interceptor {
|
|||
newRequest.url(response.request.url)
|
||||
|
||||
val authorizationHeaders = request.headers.values(HEADER_AUTHORIZATION)
|
||||
if (!authorizationHeaders.isEmpty() && !TextUtils.isEmpty(authorizationHeaders[0])) {
|
||||
if (authorizationHeaders.isNotEmpty() && !TextUtils.isEmpty(authorizationHeaders[0])) {
|
||||
// Call already had authorization headers. Try again with the same credentials.
|
||||
newRequest.header(HEADER_AUTHORIZATION, authorizationHeaders[0])
|
||||
return chain.proceed(newRequest.build())
|
||||
}
|
||||
}
|
||||
|
||||
var userInfo: String
|
||||
var userInfo = ""
|
||||
if (request.tag() is DownloadRequest) {
|
||||
val downloadRequest = request.tag() as DownloadRequest?
|
||||
userInfo = URIUtil.getURIFromRequestUrl(downloadRequest!!.source).userInfo
|
||||
val downloadRequest = request.tag() as? DownloadRequest
|
||||
if (downloadRequest?.source != null) {
|
||||
userInfo = URIUtil.getURIFromRequestUrl(downloadRequest.source!!).userInfo
|
||||
if (TextUtils.isEmpty(userInfo)
|
||||
&& (!TextUtils.isEmpty(downloadRequest.username)
|
||||
|| !TextUtils.isEmpty(downloadRequest.password))) {
|
||||
userInfo = downloadRequest.username + ":" + downloadRequest.password
|
||||
}
|
||||
}
|
||||
} else {
|
||||
userInfo = DBReader.getImageAuthentication(request.url.toString())
|
||||
}
|
||||
|
||||
if (TextUtils.isEmpty(userInfo)) {
|
||||
if (userInfo.isEmpty()) {
|
||||
Log.d(TAG, "no credentials for '" + request.url + "'")
|
||||
return response
|
||||
}
|
||||
|
|
|
@ -40,7 +40,7 @@ class FeedUpdateWorker(context: Context, params: WorkerParameters) : Worker(cont
|
|||
ClientConfigurator.initialize(applicationContext)
|
||||
newEpisodesNotification.loadCountersBeforeRefresh()
|
||||
|
||||
val toUpdate: MutableList<Feed?>
|
||||
val toUpdate: MutableList<Feed>
|
||||
val feedId = inputData.getLong(FeedUpdateManager.EXTRA_FEED_ID, -1)
|
||||
var allAreLocal = true
|
||||
var force = false
|
||||
|
@ -49,7 +49,7 @@ class FeedUpdateWorker(context: Context, params: WorkerParameters) : Worker(cont
|
|||
val itr = toUpdate.iterator()
|
||||
while (itr.hasNext()) {
|
||||
val feed = itr.next()
|
||||
if (feed!!.preferences?.keepUpdated == false) {
|
||||
if (feed.preferences?.keepUpdated == false) {
|
||||
itr.remove()
|
||||
}
|
||||
if (!feed.isLocalFeed) {
|
||||
|
@ -105,7 +105,7 @@ class FeedUpdateWorker(context: Context, params: WorkerParameters) : Worker(cont
|
|||
return Futures.immediateFuture(ForegroundInfo(R.id.notification_updating_feeds, createNotification(null)))
|
||||
}
|
||||
|
||||
private fun refreshFeeds(toUpdate: MutableList<Feed?>, force: Boolean) {
|
||||
@UnstableApi private fun refreshFeeds(toUpdate: MutableList<Feed>, force: Boolean) {
|
||||
while (toUpdate.isNotEmpty()) {
|
||||
if (isStopped) {
|
||||
return
|
||||
|
@ -124,13 +124,13 @@ class FeedUpdateWorker(context: Context, params: WorkerParameters) : Worker(cont
|
|||
notificationManager.notify(R.id.notification_updating_feeds, createNotification(toUpdate))
|
||||
val feed = toUpdate[0]
|
||||
try {
|
||||
if (feed!!.isLocalFeed) {
|
||||
if (feed.isLocalFeed) {
|
||||
LocalFeedUpdater.updateFeed(feed, applicationContext, null)
|
||||
} else {
|
||||
refreshFeed(feed, force)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
DBWriter.setFeedLastUpdateFailed(feed!!.id, true)
|
||||
DBWriter.setFeedLastUpdateFailed(feed.id, true)
|
||||
val status = DownloadResult(feed, feed.title?:"",
|
||||
DownloadError.ERROR_IO_ERROR, false, e.message?:"")
|
||||
DBWriter.addDownloadStatus(status)
|
||||
|
@ -139,22 +139,21 @@ class FeedUpdateWorker(context: Context, params: WorkerParameters) : Worker(cont
|
|||
}
|
||||
}
|
||||
|
||||
@Throws(Exception::class)
|
||||
fun refreshFeed(feed: Feed?, force: Boolean) {
|
||||
@UnstableApi @Throws(Exception::class)
|
||||
fun refreshFeed(feed: Feed, force: Boolean) {
|
||||
val nextPage = (inputData.getBoolean(FeedUpdateManager.EXTRA_NEXT_PAGE, false)
|
||||
&& feed!!.nextPageLink != null)
|
||||
&& feed.nextPageLink != null)
|
||||
if (nextPage) {
|
||||
feed!!.pageNr = feed.pageNr + 1
|
||||
feed.pageNr += 1
|
||||
}
|
||||
val builder = create(feed!!)
|
||||
val builder = create(feed)
|
||||
builder.setForce(force || feed.hasLastUpdateFailed())
|
||||
if (nextPage) {
|
||||
builder.source = feed.nextPageLink
|
||||
}
|
||||
val request = builder.build()
|
||||
|
||||
val downloader = DefaultDownloaderFactory().create(request)
|
||||
?: throw Exception("Unable to create downloader")
|
||||
val downloader = DefaultDownloaderFactory().create(request) ?: throw Exception("Unable to create downloader")
|
||||
|
||||
downloader.call()
|
||||
|
||||
|
@ -185,10 +184,10 @@ class FeedUpdateWorker(context: Context, params: WorkerParameters) : Worker(cont
|
|||
DBWriter.addDownloadStatus(feedSyncTask.downloadStatus)
|
||||
}
|
||||
newEpisodesNotification.showIfNeeded(applicationContext, feedSyncTask.savedFeed!!)
|
||||
if (request.source != null) {
|
||||
if (downloader.permanentRedirectUrl != null) {
|
||||
if (!request.source.isNullOrEmpty()) {
|
||||
if (!downloader.permanentRedirectUrl.isNullOrEmpty()) {
|
||||
DBWriter.updateFeedDownloadURL(request.source!!, downloader.permanentRedirectUrl!!)
|
||||
} else if (feedSyncTask.redirectUrl != null && feedSyncTask.redirectUrl != request.source) {
|
||||
} else if (feedSyncTask.redirectUrl != request.source) {
|
||||
DBWriter.updateFeedDownloadURL(request.source!!, feedSyncTask.redirectUrl)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -36,11 +36,10 @@ object DownloadRequestCreator {
|
|||
|
||||
@JvmStatic
|
||||
fun create(media: FeedMedia): DownloadRequest.Builder {
|
||||
val partiallyDownloadedFileExists =
|
||||
media.file_url != null && File(media.file_url).exists()
|
||||
val partiallyDownloadedFileExists = media.file_url != null && File(media.file_url!!).exists()
|
||||
var dest: File
|
||||
dest = if (partiallyDownloadedFileExists) {
|
||||
File(media.file_url)
|
||||
File(media.file_url!!)
|
||||
} else {
|
||||
File(getMediafilePath(media), getMediafilename(media))
|
||||
}
|
||||
|
@ -60,8 +59,7 @@ object DownloadRequestCreator {
|
|||
// find different name
|
||||
var newDest: File? = null
|
||||
for (i in 1 until Int.MAX_VALUE) {
|
||||
val newName = (FilenameUtils.getBaseName(dest
|
||||
.name)
|
||||
val newName = (FilenameUtils.getBaseName(dest.name)
|
||||
+ "-"
|
||||
+ i
|
||||
+ FilenameUtils.EXTENSION_SEPARATOR
|
||||
|
|
|
@ -25,9 +25,11 @@ import java.net.SocketTimeoutException
|
|||
import java.net.UnknownHostException
|
||||
import java.util.*
|
||||
|
||||
class HttpDownloader(request: DownloadRequest?) : Downloader(request!!) {
|
||||
class HttpDownloader(request: DownloadRequest) : Downloader(request) {
|
||||
override fun download() {
|
||||
val destination = File(downloadRequest.destination)
|
||||
if (downloadRequest.source == null || downloadRequest.destination == null) return
|
||||
|
||||
val destination = File(downloadRequest.destination!!)
|
||||
val fileExists = destination.exists()
|
||||
|
||||
var out: RandomAccessFile? = null
|
||||
|
@ -35,7 +37,7 @@ class HttpDownloader(request: DownloadRequest?) : Downloader(request!!) {
|
|||
var responseBody: ResponseBody? = null
|
||||
|
||||
try {
|
||||
val uri = getURIFromRequestUrl(downloadRequest.source)
|
||||
val uri = getURIFromRequestUrl(downloadRequest.source!!)
|
||||
val httpReq: Request.Builder = Request.Builder().url(uri.toURL())
|
||||
httpReq.tag(downloadRequest)
|
||||
httpReq.cacheControl(CacheControl.Builder().noStore().build())
|
||||
|
@ -52,7 +54,7 @@ class HttpDownloader(request: DownloadRequest?) : Downloader(request!!) {
|
|||
httpReq.addHeader("Upgrade-Insecure-Requests", "1")
|
||||
}
|
||||
|
||||
if (!TextUtils.isEmpty(downloadRequest.lastModified)) {
|
||||
if (!downloadRequest.lastModified.isNullOrEmpty()) {
|
||||
val lastModified = downloadRequest.lastModified
|
||||
val lastModifiedDate = parse(lastModified)
|
||||
if (lastModifiedDate != null) {
|
||||
|
|
|
@ -82,7 +82,7 @@ import ac.mdiq.podvinci.playback.base.PlaybackServiceMediaPlayer
|
|||
import ac.mdiq.podvinci.playback.base.PlaybackServiceMediaPlayer.PSMPCallback
|
||||
import ac.mdiq.podvinci.playback.base.PlaybackServiceMediaPlayer.PSMPInfo
|
||||
import ac.mdiq.podvinci.playback.base.PlayerStatus
|
||||
import ac.mdiq.podvinci.playback.cast.CastPsmp.getInstanceIfConnected
|
||||
import ac.mdiq.podvinci.playback.cast.CastPsmp
|
||||
import ac.mdiq.podvinci.playback.cast.CastStateListener
|
||||
import ac.mdiq.podvinci.storage.preferences.UserPreferences.allEpisodesSortOrder
|
||||
import ac.mdiq.podvinci.storage.preferences.UserPreferences.downloadsSortedOrder
|
||||
|
@ -221,7 +221,7 @@ class PlaybackService : MediaBrowserServiceCompat() {
|
|||
mediaPlayer!!.pause(true, false)
|
||||
mediaPlayer!!.shutdown()
|
||||
}
|
||||
mediaPlayer = getInstanceIfConnected(this, mediaPlayerCallback)
|
||||
mediaPlayer = CastPsmp.getInstanceIfConnected(this, mediaPlayerCallback)
|
||||
if (mediaPlayer == null) {
|
||||
mediaPlayer = LocalPSMP(this, mediaPlayerCallback) // Cast not supported or not connected
|
||||
}
|
||||
|
@ -1204,7 +1204,7 @@ class PlaybackService : MediaBrowserServiceCompat() {
|
|||
)
|
||||
}
|
||||
|
||||
WearMediaSession.mediaSessionSetExtraForWear(mediaSession)
|
||||
WearMediaSession.mediaSessionSetExtraForWear(mediaSession!!)
|
||||
|
||||
mediaSession!!.setPlaybackState(sessionState.build())
|
||||
}
|
||||
|
|
|
@ -116,7 +116,7 @@ import java.util.concurrent.TimeUnit
|
|||
localDelete = true
|
||||
} else if (media.getFile_url() != null) {
|
||||
// delete downloaded media file
|
||||
val mediaFile = File(media.getFile_url())
|
||||
val mediaFile = File(media.getFile_url()!!)
|
||||
if (mediaFile.exists() && !mediaFile.delete()) {
|
||||
val evt = MessageEvent(context.getString(R.string.delete_failed))
|
||||
EventBus.getDefault().post(evt)
|
||||
|
@ -167,10 +167,10 @@ import java.util.concurrent.TimeUnit
|
|||
return runOnDbThread {
|
||||
val feed = getFeed(feedId) ?: return@runOnDbThread
|
||||
// delete stored media files and mark them as read
|
||||
if (feed.items == null) {
|
||||
if (feed.items.isEmpty()) {
|
||||
getFeedItemList(feed)
|
||||
}
|
||||
deleteFeedItemsSynchronous(context, feed.items!!)
|
||||
deleteFeedItemsSynchronous(context, feed.items)
|
||||
|
||||
// delete feed
|
||||
val adapter = getInstance()
|
||||
|
@ -333,7 +333,6 @@ import java.util.concurrent.TimeUnit
|
|||
val queue = getQueue(adapter).toMutableList()
|
||||
val item: FeedItem?
|
||||
|
||||
if (queue != null) {
|
||||
if (!itemListContains(queue, itemId)) {
|
||||
item = getFeedItem(itemId)
|
||||
if (item != null) {
|
||||
|
@ -347,7 +346,6 @@ import java.util.concurrent.TimeUnit
|
|||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
adapter?.close()
|
||||
if (performAutoDownload) {
|
||||
|
@ -523,7 +521,6 @@ import java.util.concurrent.TimeUnit
|
|||
adapter?.open()
|
||||
val queue = getQueue(adapter).toMutableList()
|
||||
|
||||
if (queue != null) {
|
||||
var queueModified = false
|
||||
val events: MutableList<QueueEvent> = ArrayList()
|
||||
val updatedItems: MutableList<FeedItem> = ArrayList()
|
||||
|
@ -554,9 +551,6 @@ import java.util.concurrent.TimeUnit
|
|||
} else {
|
||||
Log.w(TAG, "Queue was not modified by call to removeQueueItem")
|
||||
}
|
||||
} else {
|
||||
Log.e(TAG, "removeQueueItem: Could not load queue")
|
||||
}
|
||||
adapter?.close()
|
||||
if (performAutoDownload) {
|
||||
autodownloadUndownloadedItems(context)
|
||||
|
@ -954,15 +948,11 @@ import java.util.concurrent.TimeUnit
|
|||
adapter!!.open()
|
||||
val queue = getQueue(adapter).toMutableList()
|
||||
|
||||
if (queue != null) {
|
||||
permutor.reorder(queue)
|
||||
adapter.setQueue(queue)
|
||||
if (broadcastUpdate) {
|
||||
EventBus.getDefault().post(QueueEvent.sorted(queue))
|
||||
}
|
||||
} else {
|
||||
Log.e(TAG, "reorderQueue: Could not load queue")
|
||||
}
|
||||
adapter.close()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -14,7 +14,7 @@ object URIUtil {
|
|||
private const val TAG = "URIUtil"
|
||||
|
||||
@JvmStatic
|
||||
fun getURIFromRequestUrl(source: String?): URI {
|
||||
fun getURIFromRequestUrl(source: String): URI {
|
||||
// try without encoding the URI
|
||||
try {
|
||||
return URI(source)
|
||||
|
@ -25,8 +25,10 @@ object URIUtil {
|
|||
val url = URL(source)
|
||||
return URI(url.protocol, url.userInfo, url.host, url.port, url.path, url.query, url.ref)
|
||||
} catch (e: MalformedURLException) {
|
||||
Log.d(TAG, "source: $source")
|
||||
throw IllegalArgumentException(e)
|
||||
} catch (e: URISyntaxException) {
|
||||
Log.d(TAG, "source: $source")
|
||||
throw IllegalArgumentException(e)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,25 +0,0 @@
|
|||
package ac.mdiq.podvinci.core.service.playback;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.support.v4.media.session.MediaSessionCompat;
|
||||
import android.support.v4.media.session.PlaybackStateCompat;
|
||||
import android.support.wearable.media.MediaControlConstants;
|
||||
|
||||
public class WearMediaSession {
|
||||
/**
|
||||
* Take a custom action builder and make sure the custom action shows on Wear OS because this is the Play version
|
||||
* of the app.
|
||||
*/
|
||||
static void addWearExtrasToAction(PlaybackStateCompat.CustomAction.Builder actionBuilder) {
|
||||
Bundle actionExtras = new Bundle();
|
||||
actionExtras.putBoolean(MediaControlConstants.EXTRA_CUSTOM_ACTION_SHOW_ON_WEAR, true);
|
||||
actionBuilder.setExtras(actionExtras);
|
||||
}
|
||||
|
||||
static void mediaSessionSetExtraForWear(MediaSessionCompat mediaSession) {
|
||||
Bundle sessionExtras = new Bundle();
|
||||
sessionExtras.putBoolean(MediaControlConstants.EXTRA_RESERVE_SLOT_SKIP_TO_PREVIOUS, false);
|
||||
sessionExtras.putBoolean(MediaControlConstants.EXTRA_RESERVE_SLOT_SKIP_TO_NEXT, false);
|
||||
mediaSession.setExtras(sessionExtras);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,25 @@
|
|||
package ac.mdiq.podvinci.core.service.playback
|
||||
|
||||
import android.os.Bundle
|
||||
import android.support.v4.media.session.MediaSessionCompat
|
||||
import android.support.v4.media.session.PlaybackStateCompat
|
||||
import android.support.wearable.media.MediaControlConstants
|
||||
|
||||
object WearMediaSession {
|
||||
/**
|
||||
* Take a custom action builder and make sure the custom action shows on Wear OS because this is the Play version
|
||||
* of the app.
|
||||
*/
|
||||
fun addWearExtrasToAction(actionBuilder: PlaybackStateCompat.CustomAction.Builder) {
|
||||
val actionExtras = Bundle()
|
||||
actionExtras.putBoolean(MediaControlConstants.EXTRA_CUSTOM_ACTION_SHOW_ON_WEAR, true)
|
||||
actionBuilder.setExtras(actionExtras)
|
||||
}
|
||||
|
||||
fun mediaSessionSetExtraForWear(mediaSession: MediaSessionCompat) {
|
||||
val sessionExtras = Bundle()
|
||||
sessionExtras.putBoolean(MediaControlConstants.EXTRA_RESERVE_SLOT_SKIP_TO_PREVIOUS, false)
|
||||
sessionExtras.putBoolean(MediaControlConstants.EXTRA_RESERVE_SLOT_SKIP_TO_NEXT, false)
|
||||
mediaSession.setExtras(sessionExtras)
|
||||
}
|
||||
}
|
|
@ -1,41 +0,0 @@
|
|||
package android.text;
|
||||
|
||||
/**
|
||||
* A slim-down version of standard {@link android.text.TextUtils} to be used in unit tests.
|
||||
*/
|
||||
public class TextUtils {
|
||||
|
||||
/**
|
||||
* Returns true if a and b are equal, including if they are both null.
|
||||
* <p><i>Note: In platform versions 1.1 and earlier, this method only worked well if
|
||||
* both the arguments were instances of String.</i></p>
|
||||
* @param a first CharSequence to check
|
||||
* @param b second CharSequence to check
|
||||
* @return true if a and b are equal
|
||||
*/
|
||||
public static boolean equals(CharSequence a, CharSequence b) {
|
||||
if (a == b) return true;
|
||||
int length;
|
||||
if (a != null && b != null && (length = a.length()) == b.length()) {
|
||||
if (a instanceof String && b instanceof String) {
|
||||
return a.equals(b);
|
||||
} else {
|
||||
for (int i = 0; i < length; i++) {
|
||||
if (a.charAt(i) != b.charAt(i)) return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns <code>true</code> if the string is <code>null</code> or has zero length.
|
||||
*
|
||||
* @param str The string to be examined, can be <code>null</code>.
|
||||
* @return <code>true</code> if the string is <code>null</code> or has zero length.
|
||||
*/
|
||||
public static boolean isEmpty(CharSequence str) {
|
||||
return str == null || str.length() == 0;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
package android.text
|
||||
|
||||
/**
|
||||
* A slim-down version of standard [android.text.TextUtils] to be used in unit tests.
|
||||
*/
|
||||
object TextUtils {
|
||||
/**
|
||||
* Returns true if a and b are equal, including if they are both null.
|
||||
*
|
||||
* *Note: In platform versions 1.1 and earlier, this method only worked well if
|
||||
* both the arguments were instances of String.*
|
||||
* @param a first CharSequence to check
|
||||
* @param b second CharSequence to check
|
||||
* @return true if a and b are equal
|
||||
*/
|
||||
fun equals(a: CharSequence?, b: CharSequence?): Boolean {
|
||||
if (a === b) return true
|
||||
var length: Int = 0
|
||||
if ((a != null && b != null) && (a.length.also { length = it }) == b.length) {
|
||||
if (a is String && b is String) {
|
||||
return a == b
|
||||
} else {
|
||||
for (i in 0 until length) {
|
||||
if (a[i] != b[i]) return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns `true` if the string is `null` or has zero length.
|
||||
*
|
||||
* @param str The string to be examined, can be `null`.
|
||||
* @return `true` if the string is `null` or has zero length.
|
||||
*/
|
||||
fun isEmpty(str: CharSequence?): Boolean {
|
||||
return str.isNullOrEmpty()
|
||||
}
|
||||
}
|
|
@ -1,246 +0,0 @@
|
|||
package android.util;
|
||||
|
||||
import java.io.PrintWriter;
|
||||
import java.io.StringWriter;
|
||||
|
||||
/**
|
||||
* A stub for {@link android.util.Log} to be used in unit tests.
|
||||
*
|
||||
* It outputs the log statements to standard error.
|
||||
*/
|
||||
public final class Log {
|
||||
|
||||
/**
|
||||
* Priority constant for the println method; use Log.v.
|
||||
*/
|
||||
public static final int VERBOSE = 2;
|
||||
|
||||
/**
|
||||
* Priority constant for the println method; use Log.d.
|
||||
*/
|
||||
public static final int DEBUG = 3;
|
||||
|
||||
/**
|
||||
* Priority constant for the println method; use Log.i.
|
||||
*/
|
||||
public static final int INFO = 4;
|
||||
|
||||
/**
|
||||
* Priority constant for the println method; use Log.w.
|
||||
*/
|
||||
public static final int WARN = 5;
|
||||
|
||||
/**
|
||||
* Priority constant for the println method; use Log.e.
|
||||
*/
|
||||
public static final int ERROR = 6;
|
||||
|
||||
/**
|
||||
* Priority constant for the println method.
|
||||
*/
|
||||
public static final int ASSERT = 7;
|
||||
|
||||
private Log() {
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a {@link #VERBOSE} log message.
|
||||
* @param tag Used to identify the source of a log message. It usually identifies
|
||||
* the class or activity where the log call occurs.
|
||||
* @param msg The message you would like logged.
|
||||
*/
|
||||
public static int v(String tag, String msg) {
|
||||
return println_native(LOG_ID_MAIN, VERBOSE, tag, msg);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a {@link #VERBOSE} log message and log the exception.
|
||||
* @param tag Used to identify the source of a log message. It usually identifies
|
||||
* the class or activity where the log call occurs.
|
||||
* @param msg The message you would like logged.
|
||||
* @param tr An exception to log
|
||||
*/
|
||||
public static int v(String tag, String msg, Throwable tr) {
|
||||
return printlns(LOG_ID_MAIN, VERBOSE, tag, msg, tr);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a {@link #DEBUG} log message.
|
||||
* @param tag Used to identify the source of a log message. It usually identifies
|
||||
* the class or activity where the log call occurs.
|
||||
* @param msg The message you would like logged.
|
||||
*/
|
||||
public static int d(String tag, String msg) {
|
||||
return println_native(LOG_ID_MAIN, DEBUG, tag, msg);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a {@link #DEBUG} log message and log the exception.
|
||||
* @param tag Used to identify the source of a log message. It usually identifies
|
||||
* the class or activity where the log call occurs.
|
||||
* @param msg The message you would like logged.
|
||||
* @param tr An exception to log
|
||||
*/
|
||||
public static int d(String tag, String msg, Throwable tr) {
|
||||
return printlns(LOG_ID_MAIN, DEBUG, tag, msg, tr);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send an {@link #INFO} log message.
|
||||
* @param tag Used to identify the source of a log message. It usually identifies
|
||||
* the class or activity where the log call occurs.
|
||||
* @param msg The message you would like logged.
|
||||
*/
|
||||
public static int i(String tag, String msg) {
|
||||
return println_native(LOG_ID_MAIN, INFO, tag, msg);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a {@link #INFO} log message and log the exception.
|
||||
* @param tag Used to identify the source of a log message. It usually identifies
|
||||
* the class or activity where the log call occurs.
|
||||
* @param msg The message you would like logged.
|
||||
* @param tr An exception to log
|
||||
*/
|
||||
public static int i(String tag, String msg, Throwable tr) {
|
||||
return printlns(LOG_ID_MAIN, INFO, tag, msg, tr);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a {@link #WARN} log message.
|
||||
* @param tag Used to identify the source of a log message. It usually identifies
|
||||
* the class or activity where the log call occurs.
|
||||
* @param msg The message you would like logged.
|
||||
*/
|
||||
public static int w(String tag, String msg) {
|
||||
return println_native(LOG_ID_MAIN, WARN, tag, msg);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a {@link #WARN} log message and log the exception.
|
||||
* @param tag Used to identify the source of a log message. It usually identifies
|
||||
* the class or activity where the log call occurs.
|
||||
* @param msg The message you would like logged.
|
||||
* @param tr An exception to log
|
||||
*/
|
||||
public static int w(String tag, String msg, Throwable tr) {
|
||||
return printlns(LOG_ID_MAIN, WARN, tag, msg, tr);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks to see whether or not a log for the specified tag is loggable at the specified level.
|
||||
*
|
||||
* @return true in all cases (for unit test environment)
|
||||
*/
|
||||
public static boolean isLoggable(String tag, int level) {
|
||||
return true;
|
||||
}
|
||||
|
||||
/*
|
||||
* Send a {@link #WARN} log message and log the exception.
|
||||
* @param tag Used to identify the source of a log message. It usually identifies
|
||||
* the class or activity where the log call occurs.
|
||||
* @param tr An exception to log
|
||||
*/
|
||||
public static int w(String tag, Throwable tr) {
|
||||
return printlns(LOG_ID_MAIN, WARN, tag, "", tr);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send an {@link #ERROR} log message.
|
||||
* @param tag Used to identify the source of a log message. It usually identifies
|
||||
* the class or activity where the log call occurs.
|
||||
* @param msg The message you would like logged.
|
||||
*/
|
||||
public static int e(String tag, String msg) {
|
||||
return println_native(LOG_ID_MAIN, ERROR, tag, msg);
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a {@link #ERROR} log message and log the exception.
|
||||
* @param tag Used to identify the source of a log message. It usually identifies
|
||||
* the class or activity where the log call occurs.
|
||||
* @param msg The message you would like logged.
|
||||
* @param tr An exception to log
|
||||
*/
|
||||
public static int e(String tag, String msg, Throwable tr) {
|
||||
return printlns(LOG_ID_MAIN, ERROR, tag, msg, tr);
|
||||
}
|
||||
|
||||
/**
|
||||
* What a Terrible Failure: Report a condition that should never happen.
|
||||
* The error will always be logged at level ASSERT with the call stack.
|
||||
* Depending on system configuration, a report may be added to the
|
||||
* {@link android.os.DropBoxManager} and/or the process may be terminated
|
||||
* immediately with an error dialog.
|
||||
* @param tag Used to identify the source of a log message.
|
||||
* @param msg The message you would like logged.
|
||||
*/
|
||||
public static int wtf(String tag, String msg) {
|
||||
return wtf(LOG_ID_MAIN, tag, msg, null, false, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Like {@link #wtf(String, String)}, but also writes to the log the full
|
||||
* call stack.
|
||||
* @hide
|
||||
*/
|
||||
public static int wtfStack(String tag, String msg) {
|
||||
return wtf(LOG_ID_MAIN, tag, msg, null, true, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* What a Terrible Failure: Report an exception that should never happen.
|
||||
* Similar to {@link #wtf(String, String)}, with an exception to log.
|
||||
* @param tag Used to identify the source of a log message.
|
||||
* @param tr An exception to log.
|
||||
*/
|
||||
public static int wtf(String tag, Throwable tr) {
|
||||
return wtf(LOG_ID_MAIN, tag, tr.getMessage(), tr, false, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* What a Terrible Failure: Report an exception that should never happen.
|
||||
* Similar to {@link #wtf(String, Throwable)}, with a message as well.
|
||||
* @param tag Used to identify the source of a log message.
|
||||
* @param msg The message you would like logged.
|
||||
* @param tr An exception to log. May be null.
|
||||
*/
|
||||
public static int wtf(String tag, String msg, Throwable tr) {
|
||||
return wtf(LOG_ID_MAIN, tag, msg, tr, false, false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Priority Constant for wtf.
|
||||
* Added for this custom Log implementation, not in android sources.
|
||||
*/
|
||||
private static final int WTF = 8;
|
||||
|
||||
static int wtf(int logId, String tag, String msg, Throwable tr, boolean localStack,
|
||||
boolean system) {
|
||||
return printlns(LOG_ID_MAIN, WTF, tag, msg, tr);
|
||||
}
|
||||
|
||||
private static final int LOG_ID_MAIN = 0;
|
||||
|
||||
private static final String[] PRIORITY_ABBREV = { "0", "1", "V", "D", "I", "W", "E", "A", "WTF" };
|
||||
|
||||
private static int println_native(int bufID, int priority, String tag, String msg) {
|
||||
String res = PRIORITY_ABBREV[priority] + "/" + tag + " " + msg + System.lineSeparator();
|
||||
System.err.print(res);
|
||||
return res.length();
|
||||
}
|
||||
|
||||
private static int printlns(int bufID, int priority, String tag, String msg,
|
||||
Throwable tr) {
|
||||
StringWriter trSW = new StringWriter();
|
||||
if (tr != null) {
|
||||
trSW.append(" , Exception: ");
|
||||
PrintWriter trPW = new PrintWriter(trSW);
|
||||
tr.printStackTrace(trPW);
|
||||
trPW.flush();
|
||||
}
|
||||
return println_native(bufID, priority, tag, msg + trSW.toString());
|
||||
}
|
||||
|
||||
}
|
|
@ -0,0 +1,243 @@
|
|||
package android.util
|
||||
|
||||
import java.io.PrintWriter
|
||||
import java.io.StringWriter
|
||||
|
||||
/**
|
||||
* A stub for [android.util.Log] to be used in unit tests.
|
||||
*
|
||||
* It outputs the log statements to standard error.
|
||||
*/
|
||||
object Log {
|
||||
/**
|
||||
* Priority constant for the println method; use Log.v.
|
||||
*/
|
||||
const val VERBOSE: Int = 2
|
||||
|
||||
/**
|
||||
* Priority constant for the println method; use Log.d.
|
||||
*/
|
||||
const val DEBUG: Int = 3
|
||||
|
||||
/**
|
||||
* Priority constant for the println method; use Log.i.
|
||||
*/
|
||||
const val INFO: Int = 4
|
||||
|
||||
/**
|
||||
* Priority constant for the println method; use Log.w.
|
||||
*/
|
||||
const val WARN: Int = 5
|
||||
|
||||
/**
|
||||
* Priority constant for the println method; use Log.e.
|
||||
*/
|
||||
const val ERROR: Int = 6
|
||||
|
||||
/**
|
||||
* Priority constant for the println method.
|
||||
*/
|
||||
const val ASSERT: Int = 7
|
||||
|
||||
/**
|
||||
* Send a [.VERBOSE] log message.
|
||||
* @param tag Used to identify the source of a log message. It usually identifies
|
||||
* the class or activity where the log call occurs.
|
||||
* @param msg The message you would like logged.
|
||||
*/
|
||||
fun v(tag: String, msg: String): Int {
|
||||
return println_native(LOG_ID_MAIN, VERBOSE, tag, msg)
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a [.VERBOSE] log message and log the exception.
|
||||
* @param tag Used to identify the source of a log message. It usually identifies
|
||||
* the class or activity where the log call occurs.
|
||||
* @param msg The message you would like logged.
|
||||
* @param tr An exception to log
|
||||
*/
|
||||
fun v(tag: String, msg: String, tr: Throwable?): Int {
|
||||
return printlns(LOG_ID_MAIN, VERBOSE, tag, msg, tr)
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a [.DEBUG] log message.
|
||||
* @param tag Used to identify the source of a log message. It usually identifies
|
||||
* the class or activity where the log call occurs.
|
||||
* @param msg The message you would like logged.
|
||||
*/
|
||||
fun d(tag: String, msg: String): Int {
|
||||
return println_native(LOG_ID_MAIN, DEBUG, tag, msg)
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a [.DEBUG] log message and log the exception.
|
||||
* @param tag Used to identify the source of a log message. It usually identifies
|
||||
* the class or activity where the log call occurs.
|
||||
* @param msg The message you would like logged.
|
||||
* @param tr An exception to log
|
||||
*/
|
||||
fun d(tag: String, msg: String, tr: Throwable?): Int {
|
||||
return printlns(LOG_ID_MAIN, DEBUG, tag, msg, tr)
|
||||
}
|
||||
|
||||
/**
|
||||
* Send an [.INFO] log message.
|
||||
* @param tag Used to identify the source of a log message. It usually identifies
|
||||
* the class or activity where the log call occurs.
|
||||
* @param msg The message you would like logged.
|
||||
*/
|
||||
fun i(tag: String, msg: String): Int {
|
||||
return println_native(LOG_ID_MAIN, INFO, tag, msg)
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a [.INFO] log message and log the exception.
|
||||
* @param tag Used to identify the source of a log message. It usually identifies
|
||||
* the class or activity where the log call occurs.
|
||||
* @param msg The message you would like logged.
|
||||
* @param tr An exception to log
|
||||
*/
|
||||
fun i(tag: String, msg: String, tr: Throwable?): Int {
|
||||
return printlns(LOG_ID_MAIN, INFO, tag, msg, tr)
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a [.WARN] log message.
|
||||
* @param tag Used to identify the source of a log message. It usually identifies
|
||||
* the class or activity where the log call occurs.
|
||||
* @param msg The message you would like logged.
|
||||
*/
|
||||
fun w(tag: String, msg: String): Int {
|
||||
return println_native(LOG_ID_MAIN, WARN, tag, msg)
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a [.WARN] log message and log the exception.
|
||||
* @param tag Used to identify the source of a log message. It usually identifies
|
||||
* the class or activity where the log call occurs.
|
||||
* @param msg The message you would like logged.
|
||||
* @param tr An exception to log
|
||||
*/
|
||||
fun w(tag: String, msg: String, tr: Throwable?): Int {
|
||||
return printlns(LOG_ID_MAIN, WARN, tag, msg, tr)
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks to see whether or not a log for the specified tag is loggable at the specified level.
|
||||
*
|
||||
* @return true in all cases (for unit test environment)
|
||||
*/
|
||||
fun isLoggable(tag: String?, level: Int): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
/*
|
||||
* Send a {@link #WARN} log message and log the exception.
|
||||
* @param tag Used to identify the source of a log message. It usually identifies
|
||||
* the class or activity where the log call occurs.
|
||||
* @param tr An exception to log
|
||||
*/
|
||||
fun w(tag: String, tr: Throwable?): Int {
|
||||
return printlns(LOG_ID_MAIN, WARN, tag, "", tr)
|
||||
}
|
||||
|
||||
/**
|
||||
* Send an [.ERROR] log message.
|
||||
* @param tag Used to identify the source of a log message. It usually identifies
|
||||
* the class or activity where the log call occurs.
|
||||
* @param msg The message you would like logged.
|
||||
*/
|
||||
fun e(tag: String, msg: String): Int {
|
||||
return println_native(LOG_ID_MAIN, ERROR, tag, msg)
|
||||
}
|
||||
|
||||
/**
|
||||
* Send a [.ERROR] log message and log the exception.
|
||||
* @param tag Used to identify the source of a log message. It usually identifies
|
||||
* the class or activity where the log call occurs.
|
||||
* @param msg The message you would like logged.
|
||||
* @param tr An exception to log
|
||||
*/
|
||||
fun e(tag: String, msg: String, tr: Throwable?): Int {
|
||||
return printlns(LOG_ID_MAIN, ERROR, tag, msg, tr)
|
||||
}
|
||||
|
||||
/**
|
||||
* What a Terrible Failure: Report a condition that should never happen.
|
||||
* The error will always be logged at level ASSERT with the call stack.
|
||||
* Depending on system configuration, a report may be added to the
|
||||
* [android.os.DropBoxManager] and/or the process may be terminated
|
||||
* immediately with an error dialog.
|
||||
* @param tag Used to identify the source of a log message.
|
||||
* @param msg The message you would like logged.
|
||||
*/
|
||||
fun wtf(tag: String, msg: String): Int {
|
||||
return wtf(LOG_ID_MAIN, tag, msg, null, false, false)
|
||||
}
|
||||
|
||||
/**
|
||||
* Like [.wtf], but also writes to the log the full
|
||||
* call stack.
|
||||
* @hide
|
||||
*/
|
||||
fun wtfStack(tag: String, msg: String): Int {
|
||||
return wtf(LOG_ID_MAIN, tag, msg, null, true, false)
|
||||
}
|
||||
|
||||
/**
|
||||
* What a Terrible Failure: Report an exception that should never happen.
|
||||
* Similar to [.wtf], with an exception to log.
|
||||
* @param tag Used to identify the source of a log message.
|
||||
* @param tr An exception to log.
|
||||
*/
|
||||
fun wtf(tag: String, tr: Throwable): Int {
|
||||
return wtf(LOG_ID_MAIN, tag, tr.message?:"", tr, false, false)
|
||||
}
|
||||
|
||||
/**
|
||||
* What a Terrible Failure: Report an exception that should never happen.
|
||||
* Similar to [.wtf], with a message as well.
|
||||
* @param tag Used to identify the source of a log message.
|
||||
* @param msg The message you would like logged.
|
||||
* @param tr An exception to log. May be null.
|
||||
*/
|
||||
fun wtf(tag: String, msg: String, tr: Throwable?): Int {
|
||||
return wtf(LOG_ID_MAIN, tag, msg, tr, false, false)
|
||||
}
|
||||
|
||||
/**
|
||||
* Priority Constant for wtf.
|
||||
* Added for this custom Log implementation, not in android sources.
|
||||
*/
|
||||
private const val WTF = 8
|
||||
|
||||
fun wtf(logId: Int, tag: String, msg: String, tr: Throwable?, localStack: Boolean,
|
||||
system: Boolean
|
||||
): Int {
|
||||
return printlns(LOG_ID_MAIN, WTF, tag, msg, tr)
|
||||
}
|
||||
|
||||
private const val LOG_ID_MAIN = 0
|
||||
|
||||
private val PRIORITY_ABBREV = arrayOf("0", "1", "V", "D", "I", "W", "E", "A", "WTF")
|
||||
|
||||
private fun println_native(bufID: Int, priority: Int, tag: String, msg: String): Int {
|
||||
val res: String = PRIORITY_ABBREV[priority] + "/" + tag + " " + msg + System.lineSeparator()
|
||||
System.err.print(res)
|
||||
return res.length
|
||||
}
|
||||
|
||||
private fun printlns(bufID: Int, priority: Int, tag: String, msg: String,
|
||||
tr: Throwable?
|
||||
): Int {
|
||||
val trSW = StringWriter()
|
||||
if (tr != null) {
|
||||
trSW.append(" , Exception: ")
|
||||
val trPW = PrintWriter(trSW)
|
||||
tr.printStackTrace(trPW)
|
||||
trPW.flush()
|
||||
}
|
||||
return println_native(bufID, priority, tag, msg + trSW.toString())
|
||||
}
|
||||
}
|
|
@ -232,11 +232,11 @@ class Feed : FeedFile {
|
|||
* try to return the title. If the title is not given, it will use the link
|
||||
* of the feed.
|
||||
*/
|
||||
get() = if (feedIdentifier != null && feedIdentifier!!.isNotEmpty()) {
|
||||
get() = if (!feedIdentifier.isNullOrEmpty()) {
|
||||
feedIdentifier
|
||||
} else if (download_url != null && download_url!!.isNotEmpty()) {
|
||||
} else if (!download_url.isNullOrEmpty()) {
|
||||
download_url
|
||||
} else if (feedTitle != null && feedTitle!!.isNotEmpty()) {
|
||||
} else if (!feedTitle.isNullOrEmpty()) {
|
||||
feedTitle
|
||||
} else {
|
||||
link
|
||||
|
|
|
@ -142,7 +142,7 @@ class DownloadRequest private constructor(@JvmField val destination: String?,
|
|||
|
||||
constructor(destination: String, media: FeedMedia) {
|
||||
this.destination = destination
|
||||
this.source = prepareUrl(media.download_url!!)
|
||||
this.source = if (media.download_url != null) prepareUrl(media.download_url!!) else null
|
||||
this.title = media.getHumanReadableIdentifier()
|
||||
this.feedfileId = media.id
|
||||
this.feedfileType = media.getTypeAsInt()
|
||||
|
@ -150,7 +150,7 @@ class DownloadRequest private constructor(@JvmField val destination: String?,
|
|||
|
||||
constructor(destination: String, feed: Feed) {
|
||||
this.destination = destination
|
||||
this.source = if (feed.isLocalFeed) feed.download_url else prepareUrl(feed.download_url!!)
|
||||
this.source = if (feed.isLocalFeed) feed.download_url else if (feed.download_url != null) prepareUrl(feed.download_url!!) else null
|
||||
this.title = feed.getHumanReadableIdentifier()
|
||||
this.feedfileId = feed.id
|
||||
this.feedfileType = feed.getTypeAsInt()
|
||||
|
|
|
@ -1,20 +0,0 @@
|
|||
package ac.mdiq.podvinci.net.ssl;
|
||||
|
||||
import android.content.Context;
|
||||
import com.google.android.gms.common.GoogleApiAvailability;
|
||||
import com.google.android.gms.common.GooglePlayServicesNotAvailableException;
|
||||
import com.google.android.gms.common.GooglePlayServicesRepairableException;
|
||||
import com.google.android.gms.security.ProviderInstaller;
|
||||
|
||||
public class SslProviderInstaller {
|
||||
public static void install(Context context) {
|
||||
try {
|
||||
ProviderInstaller.installIfNeeded(context);
|
||||
} catch (GooglePlayServicesRepairableException e) {
|
||||
e.printStackTrace();
|
||||
GoogleApiAvailability.getInstance().showErrorNotification(context, e.getConnectionStatusCode());
|
||||
} catch (GooglePlayServicesNotAvailableException e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,20 @@
|
|||
package ac.mdiq.podvinci.net.ssl
|
||||
|
||||
import android.content.Context
|
||||
import com.google.android.gms.common.GoogleApiAvailability
|
||||
import com.google.android.gms.common.GooglePlayServicesNotAvailableException
|
||||
import com.google.android.gms.common.GooglePlayServicesRepairableException
|
||||
import com.google.android.gms.security.ProviderInstaller
|
||||
|
||||
object SslProviderInstaller {
|
||||
fun install(context: Context) {
|
||||
try {
|
||||
ProviderInstaller.installIfNeeded(context)
|
||||
} catch (e: GooglePlayServicesRepairableException) {
|
||||
e.printStackTrace()
|
||||
GoogleApiAvailability.getInstance().showErrorNotification(context, e.connectionStatusCode)
|
||||
} catch (e: GooglePlayServicesNotAvailableException) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,57 +0,0 @@
|
|||
package ac.mdiq.podvinci.playback.base;
|
||||
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import static org.junit.Assert.assertEquals;
|
||||
|
||||
/**
|
||||
* Tests for {@link RewindAfterPauseUtils}.
|
||||
*/
|
||||
public class RewindAfterPauseUtilTest {
|
||||
|
||||
@Test
|
||||
public void testCalculatePositionWithRewindNoRewind() {
|
||||
final int ORIGINAL_POSITION = 10000;
|
||||
long lastPlayed = System.currentTimeMillis();
|
||||
int position = RewindAfterPauseUtils.calculatePositionWithRewind(ORIGINAL_POSITION, lastPlayed);
|
||||
|
||||
assertEquals(ORIGINAL_POSITION, position);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCalculatePositionWithRewindSmallRewind() {
|
||||
final int ORIGINAL_POSITION = 10000;
|
||||
long lastPlayed = System.currentTimeMillis() - RewindAfterPauseUtils.ELAPSED_TIME_FOR_SHORT_REWIND - 1000;
|
||||
int position = RewindAfterPauseUtils.calculatePositionWithRewind(ORIGINAL_POSITION, lastPlayed);
|
||||
|
||||
assertEquals(ORIGINAL_POSITION - RewindAfterPauseUtils.SHORT_REWIND, position);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCalculatePositionWithRewindMediumRewind() {
|
||||
final int ORIGINAL_POSITION = 10000;
|
||||
long lastPlayed = System.currentTimeMillis() - RewindAfterPauseUtils.ELAPSED_TIME_FOR_MEDIUM_REWIND - 1000;
|
||||
int position = RewindAfterPauseUtils.calculatePositionWithRewind(ORIGINAL_POSITION, lastPlayed);
|
||||
|
||||
assertEquals(ORIGINAL_POSITION - RewindAfterPauseUtils.MEDIUM_REWIND, position);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCalculatePositionWithRewindLongRewind() {
|
||||
final int ORIGINAL_POSITION = 30000;
|
||||
long lastPlayed = System.currentTimeMillis() - RewindAfterPauseUtils.ELAPSED_TIME_FOR_LONG_REWIND - 1000;
|
||||
int position = RewindAfterPauseUtils.calculatePositionWithRewind(ORIGINAL_POSITION, lastPlayed);
|
||||
|
||||
assertEquals(ORIGINAL_POSITION - RewindAfterPauseUtils.LONG_REWIND, position);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testCalculatePositionWithRewindNegativeNumber() {
|
||||
final int ORIGINAL_POSITION = 100;
|
||||
long lastPlayed = System.currentTimeMillis() - RewindAfterPauseUtils.ELAPSED_TIME_FOR_LONG_REWIND - 1000;
|
||||
int position = RewindAfterPauseUtils.calculatePositionWithRewind(ORIGINAL_POSITION, lastPlayed);
|
||||
|
||||
assertEquals(0, position);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,56 @@
|
|||
package ac.mdiq.podvinci.playback.base
|
||||
|
||||
import ac.mdiq.podvinci.playback.base.RewindAfterPauseUtils.calculatePositionWithRewind
|
||||
import org.junit.Assert
|
||||
import org.junit.Test
|
||||
|
||||
|
||||
/**
|
||||
* Tests for [RewindAfterPauseUtils].
|
||||
*/
|
||||
class RewindAfterPauseUtilTest {
|
||||
@Test
|
||||
fun testCalculatePositionWithRewindNoRewind() {
|
||||
val ORIGINAL_POSITION = 10000
|
||||
val lastPlayed = System.currentTimeMillis()
|
||||
val position = calculatePositionWithRewind(ORIGINAL_POSITION, lastPlayed)
|
||||
|
||||
Assert.assertEquals(ORIGINAL_POSITION.toLong(), position.toLong())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testCalculatePositionWithRewindSmallRewind() {
|
||||
val ORIGINAL_POSITION = 10000
|
||||
val lastPlayed = System.currentTimeMillis() - RewindAfterPauseUtils.ELAPSED_TIME_FOR_SHORT_REWIND - 1000
|
||||
val position = calculatePositionWithRewind(ORIGINAL_POSITION, lastPlayed)
|
||||
|
||||
Assert.assertEquals(ORIGINAL_POSITION - RewindAfterPauseUtils.SHORT_REWIND, position.toLong())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testCalculatePositionWithRewindMediumRewind() {
|
||||
val ORIGINAL_POSITION = 10000
|
||||
val lastPlayed = System.currentTimeMillis() - RewindAfterPauseUtils.ELAPSED_TIME_FOR_MEDIUM_REWIND - 1000
|
||||
val position = calculatePositionWithRewind(ORIGINAL_POSITION, lastPlayed)
|
||||
|
||||
Assert.assertEquals(ORIGINAL_POSITION - RewindAfterPauseUtils.MEDIUM_REWIND, position.toLong())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testCalculatePositionWithRewindLongRewind() {
|
||||
val ORIGINAL_POSITION = 30000
|
||||
val lastPlayed = System.currentTimeMillis() - RewindAfterPauseUtils.ELAPSED_TIME_FOR_LONG_REWIND - 1000
|
||||
val position = calculatePositionWithRewind(ORIGINAL_POSITION, lastPlayed)
|
||||
|
||||
Assert.assertEquals(ORIGINAL_POSITION - RewindAfterPauseUtils.LONG_REWIND, position.toLong())
|
||||
}
|
||||
|
||||
@Test
|
||||
fun testCalculatePositionWithRewindNegativeNumber() {
|
||||
val ORIGINAL_POSITION = 100
|
||||
val lastPlayed = System.currentTimeMillis() - RewindAfterPauseUtils.ELAPSED_TIME_FOR_LONG_REWIND - 1000
|
||||
val position = calculatePositionWithRewind(ORIGINAL_POSITION, lastPlayed)
|
||||
|
||||
Assert.assertEquals(0, position.toLong())
|
||||
}
|
||||
}
|
|
@ -1,39 +0,0 @@
|
|||
package ac.mdiq.podvinci.playback.cast;
|
||||
|
||||
import android.os.Bundle;
|
||||
import android.view.Menu;
|
||||
import androidx.appcompat.app.AppCompatActivity;
|
||||
import com.google.android.gms.cast.framework.CastButtonFactory;
|
||||
import com.google.android.gms.cast.framework.CastContext;
|
||||
import com.google.android.gms.common.ConnectionResult;
|
||||
import com.google.android.gms.common.GoogleApiAvailability;
|
||||
|
||||
/**
|
||||
* Activity that allows for showing the MediaRouter button whenever there's a cast device in the
|
||||
* network.
|
||||
*/
|
||||
public abstract class CastEnabledActivity extends AppCompatActivity {
|
||||
private boolean canCast = false;
|
||||
|
||||
@Override
|
||||
protected void onCreate(Bundle savedInstanceState) {
|
||||
super.onCreate(savedInstanceState);
|
||||
canCast = GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(this) == ConnectionResult.SUCCESS;
|
||||
if (canCast) {
|
||||
try {
|
||||
CastContext.getSharedInstance(this);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
canCast = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void requestCastButton(Menu menu) {
|
||||
if (!canCast) {
|
||||
return;
|
||||
}
|
||||
getMenuInflater().inflate(R.menu.cast_button, menu);
|
||||
CastButtonFactory.setUpMediaRouteButton(getApplicationContext(), menu, R.id.media_route_menu_item);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,38 @@
|
|||
package ac.mdiq.podvinci.playback.cast
|
||||
|
||||
import android.os.Bundle
|
||||
import android.view.Menu
|
||||
import androidx.appcompat.app.AppCompatActivity
|
||||
import com.google.android.gms.cast.framework.CastButtonFactory
|
||||
import com.google.android.gms.cast.framework.CastContext
|
||||
import com.google.android.gms.common.ConnectionResult
|
||||
import com.google.android.gms.common.GoogleApiAvailability
|
||||
|
||||
/**
|
||||
* Activity that allows for showing the MediaRouter button whenever there's a cast device in the
|
||||
* network.
|
||||
*/
|
||||
abstract class CastEnabledActivity : AppCompatActivity() {
|
||||
private var canCast = false
|
||||
|
||||
override fun onCreate(savedInstanceState: Bundle?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
canCast = GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(this) == ConnectionResult.SUCCESS
|
||||
if (canCast) {
|
||||
try {
|
||||
CastContext.getSharedInstance(this)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
canCast = false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun requestCastButton(menu: Menu?) {
|
||||
if (!canCast) {
|
||||
return
|
||||
}
|
||||
menuInflater.inflate(R.menu.cast_button, menu)
|
||||
CastButtonFactory.setUpMediaRouteButton(applicationContext, menu!!, R.id.media_route_menu_item)
|
||||
}
|
||||
}
|
|
@ -1,27 +0,0 @@
|
|||
package ac.mdiq.podvinci.playback.cast;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import androidx.annotation.NonNull;
|
||||
import com.google.android.gms.cast.framework.CastOptions;
|
||||
import com.google.android.gms.cast.framework.OptionsProvider;
|
||||
import com.google.android.gms.cast.framework.SessionProvider;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
@SuppressWarnings("unused")
|
||||
@SuppressLint("VisibleForTests")
|
||||
public class CastOptionsProvider implements OptionsProvider {
|
||||
@Override
|
||||
@NonNull
|
||||
public CastOptions getCastOptions(@NonNull Context context) {
|
||||
return new CastOptions.Builder()
|
||||
.setReceiverApplicationId("BEBC1DB1")
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<SessionProvider> getAdditionalSessionProviders(@NonNull Context context) {
|
||||
return null;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,21 @@
|
|||
package ac.mdiq.podvinci.playback.cast
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import com.google.android.gms.cast.framework.CastOptions
|
||||
import com.google.android.gms.cast.framework.OptionsProvider
|
||||
import com.google.android.gms.cast.framework.SessionProvider
|
||||
|
||||
@Suppress("unused")
|
||||
@SuppressLint("VisibleForTests")
|
||||
class CastOptionsProvider : OptionsProvider {
|
||||
override fun getCastOptions(context: Context): CastOptions {
|
||||
return CastOptions.Builder()
|
||||
.setReceiverApplicationId("BEBC1DB1")
|
||||
.build()
|
||||
}
|
||||
|
||||
override fun getAdditionalSessionProviders(context: Context): List<SessionProvider>? {
|
||||
return null
|
||||
}
|
||||
}
|
|
@ -1,551 +0,0 @@
|
|||
package ac.mdiq.podvinci.playback.cast;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import androidx.annotation.NonNull;
|
||||
import android.util.Log;
|
||||
import android.util.Pair;
|
||||
import android.view.SurfaceHolder;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
import androidx.annotation.Nullable;
|
||||
import com.google.android.gms.cast.MediaError;
|
||||
import com.google.android.gms.cast.MediaInfo;
|
||||
import com.google.android.gms.cast.MediaLoadOptions;
|
||||
import com.google.android.gms.cast.MediaLoadRequestData;
|
||||
import com.google.android.gms.cast.MediaSeekOptions;
|
||||
import com.google.android.gms.cast.MediaStatus;
|
||||
import com.google.android.gms.cast.framework.CastContext;
|
||||
import com.google.android.gms.cast.framework.CastState;
|
||||
import com.google.android.gms.cast.framework.media.RemoteMediaClient;
|
||||
import com.google.android.gms.common.ConnectionResult;
|
||||
import com.google.android.gms.common.GoogleApiAvailability;
|
||||
import ac.mdiq.podvinci.event.PlayerErrorEvent;
|
||||
import ac.mdiq.podvinci.event.playback.BufferUpdateEvent;
|
||||
import ac.mdiq.podvinci.model.feed.FeedMedia;
|
||||
import ac.mdiq.podvinci.model.playback.MediaType;
|
||||
import ac.mdiq.podvinci.model.playback.Playable;
|
||||
import ac.mdiq.podvinci.model.playback.RemoteMedia;
|
||||
import ac.mdiq.podvinci.playback.base.PlaybackServiceMediaPlayer;
|
||||
import ac.mdiq.podvinci.playback.base.PlayerStatus;
|
||||
import ac.mdiq.podvinci.playback.base.RewindAfterPauseUtils;
|
||||
import org.greenrobot.eventbus.EventBus;
|
||||
|
||||
/**
|
||||
* Implementation of PlaybackServiceMediaPlayer suitable for remote playback on Cast Devices.
|
||||
*/
|
||||
@SuppressLint("VisibleForTests")
|
||||
public class CastPsmp extends PlaybackServiceMediaPlayer {
|
||||
|
||||
public static final String TAG = "CastPSMP";
|
||||
|
||||
private volatile Playable media;
|
||||
private volatile MediaType mediaType;
|
||||
private volatile MediaInfo remoteMedia;
|
||||
private volatile int remoteState;
|
||||
private final CastContext castContext;
|
||||
private final RemoteMediaClient remoteMediaClient;
|
||||
|
||||
private final AtomicBoolean isBuffering;
|
||||
|
||||
private final AtomicBoolean startWhenPrepared;
|
||||
|
||||
@Nullable
|
||||
public static PlaybackServiceMediaPlayer getInstanceIfConnected(@NonNull Context context,
|
||||
@NonNull PSMPCallback callback) {
|
||||
if (GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(context) != ConnectionResult.SUCCESS) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
if (CastContext.getSharedInstance(context).getCastState() == CastState.CONNECTED) {
|
||||
return new CastPsmp(context, callback);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public CastPsmp(@NonNull Context context, @NonNull PSMPCallback callback) {
|
||||
super(context, callback);
|
||||
|
||||
castContext = CastContext.getSharedInstance(context);
|
||||
remoteMediaClient = castContext.getSessionManager().getCurrentCastSession().getRemoteMediaClient();
|
||||
remoteMediaClient.registerCallback(remoteMediaClientCallback);
|
||||
media = null;
|
||||
mediaType = null;
|
||||
startWhenPrepared = new AtomicBoolean(false);
|
||||
isBuffering = new AtomicBoolean(false);
|
||||
remoteState = MediaStatus.PLAYER_STATE_UNKNOWN;
|
||||
}
|
||||
|
||||
private final RemoteMediaClient.Callback remoteMediaClientCallback = new RemoteMediaClient.Callback() {
|
||||
@Override
|
||||
public void onMetadataUpdated() {
|
||||
super.onMetadataUpdated();
|
||||
onRemoteMediaPlayerStatusUpdated();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onPreloadStatusUpdated() {
|
||||
super.onPreloadStatusUpdated();
|
||||
onRemoteMediaPlayerStatusUpdated();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStatusUpdated() {
|
||||
super.onStatusUpdated();
|
||||
onRemoteMediaPlayerStatusUpdated();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onMediaError(@NonNull MediaError mediaError) {
|
||||
EventBus.getDefault().post(new PlayerErrorEvent(mediaError.getReason()));
|
||||
}
|
||||
};
|
||||
|
||||
private void setBuffering(boolean buffering) {
|
||||
if (buffering && isBuffering.compareAndSet(false, true)) {
|
||||
EventBus.getDefault().post(BufferUpdateEvent.started());
|
||||
} else if (!buffering && isBuffering.compareAndSet(true, false)) {
|
||||
EventBus.getDefault().post(BufferUpdateEvent.ended());
|
||||
}
|
||||
}
|
||||
|
||||
private Playable localVersion(MediaInfo info) {
|
||||
if (info == null || info.getMetadata() == null) {
|
||||
return null;
|
||||
}
|
||||
if (CastUtils.matches(info, media)) {
|
||||
return media;
|
||||
}
|
||||
String streamUrl = info.getMetadata().getString(CastUtils.KEY_STREAM_URL);
|
||||
return streamUrl == null ? CastUtils.makeRemoteMedia(info) : callback.findMedia(streamUrl);
|
||||
}
|
||||
|
||||
private MediaInfo remoteVersion(Playable playable) {
|
||||
if (playable == null) {
|
||||
return null;
|
||||
}
|
||||
if (CastUtils.matches(remoteMedia, playable)) {
|
||||
return remoteMedia;
|
||||
}
|
||||
if (playable instanceof FeedMedia) {
|
||||
return MediaInfoCreator.from((FeedMedia) playable);
|
||||
}
|
||||
if (playable instanceof RemoteMedia) {
|
||||
return MediaInfoCreator.from((RemoteMedia) playable);
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
private void onRemoteMediaPlayerStatusUpdated() {
|
||||
MediaStatus status = remoteMediaClient.getMediaStatus();
|
||||
if (status == null) {
|
||||
Log.d(TAG, "Received null MediaStatus");
|
||||
return;
|
||||
} else {
|
||||
Log.d(TAG, "Received remote status/media update. New state=" + status.getPlayerState());
|
||||
}
|
||||
int state = status.getPlayerState();
|
||||
int oldState = remoteState;
|
||||
remoteMedia = status.getMediaInfo();
|
||||
boolean mediaChanged = !CastUtils.matches(remoteMedia, media);
|
||||
boolean stateChanged = state != oldState;
|
||||
if (!mediaChanged && !stateChanged) {
|
||||
Log.d(TAG, "Both media and state haven't changed, so nothing to do");
|
||||
return;
|
||||
}
|
||||
Playable currentMedia = mediaChanged ? localVersion(remoteMedia) : media;
|
||||
Playable oldMedia = media;
|
||||
int position = (int) status.getStreamPosition();
|
||||
// check for incompatible states
|
||||
if ((state == MediaStatus.PLAYER_STATE_PLAYING || state == MediaStatus.PLAYER_STATE_PAUSED)
|
||||
&& currentMedia == null) {
|
||||
Log.w(TAG, "RemoteMediaPlayer returned playing or pausing state, but with no media");
|
||||
state = MediaStatus.PLAYER_STATE_UNKNOWN;
|
||||
stateChanged = oldState != MediaStatus.PLAYER_STATE_UNKNOWN;
|
||||
}
|
||||
|
||||
if (stateChanged) {
|
||||
remoteState = state;
|
||||
}
|
||||
|
||||
if (mediaChanged && stateChanged && oldState == MediaStatus.PLAYER_STATE_PLAYING
|
||||
&& state != MediaStatus.PLAYER_STATE_IDLE) {
|
||||
callback.onPlaybackPause(null, Playable.INVALID_TIME);
|
||||
// We don't want setPlayerStatus to handle the onPlaybackPause callback
|
||||
setPlayerStatus(PlayerStatus.INDETERMINATE, currentMedia);
|
||||
}
|
||||
|
||||
setBuffering(state == MediaStatus.PLAYER_STATE_BUFFERING);
|
||||
|
||||
switch (state) {
|
||||
case MediaStatus.PLAYER_STATE_PLAYING:
|
||||
if (!stateChanged) {
|
||||
//These steps are necessary because they won't be performed by setPlayerStatus()
|
||||
if (position >= 0) {
|
||||
currentMedia.setPosition(position);
|
||||
}
|
||||
currentMedia.onPlaybackStart();
|
||||
}
|
||||
setPlayerStatus(PlayerStatus.PLAYING, currentMedia, position);
|
||||
break;
|
||||
case MediaStatus.PLAYER_STATE_PAUSED:
|
||||
setPlayerStatus(PlayerStatus.PAUSED, currentMedia, position);
|
||||
break;
|
||||
case MediaStatus.PLAYER_STATE_BUFFERING:
|
||||
setPlayerStatus((mediaChanged || playerStatus == PlayerStatus.PREPARING)
|
||||
? PlayerStatus.PREPARING : PlayerStatus.SEEKING, currentMedia,
|
||||
currentMedia != null ? currentMedia.getPosition() : Playable.INVALID_TIME);
|
||||
break;
|
||||
case MediaStatus.PLAYER_STATE_IDLE:
|
||||
int reason = status.getIdleReason();
|
||||
switch (reason) {
|
||||
case MediaStatus.IDLE_REASON_CANCELED:
|
||||
// Essentially means stopped at the request of a user
|
||||
callback.onPlaybackEnded(null, true);
|
||||
setPlayerStatus(PlayerStatus.STOPPED, currentMedia);
|
||||
if (oldMedia != null) {
|
||||
if (position >= 0) {
|
||||
oldMedia.setPosition(position);
|
||||
}
|
||||
callback.onPostPlayback(oldMedia, false, false, false);
|
||||
}
|
||||
// onPlaybackEnded pretty much takes care of updating the UI
|
||||
return;
|
||||
case MediaStatus.IDLE_REASON_INTERRUPTED:
|
||||
// Means that a request to load a different media was sent
|
||||
// Not sure if currentMedia already reflects the to be loaded one
|
||||
if (mediaChanged && oldState == MediaStatus.PLAYER_STATE_PLAYING) {
|
||||
callback.onPlaybackPause(null, Playable.INVALID_TIME);
|
||||
setPlayerStatus(PlayerStatus.INDETERMINATE, currentMedia);
|
||||
}
|
||||
setPlayerStatus(PlayerStatus.PREPARING, currentMedia);
|
||||
break;
|
||||
case MediaStatus.IDLE_REASON_NONE:
|
||||
// This probably only happens when we connected but no command has been sent yet.
|
||||
setPlayerStatus(PlayerStatus.INITIALIZED, currentMedia);
|
||||
break;
|
||||
case MediaStatus.IDLE_REASON_FINISHED:
|
||||
// This is our onCompletionListener...
|
||||
if (mediaChanged && currentMedia != null) {
|
||||
media = currentMedia;
|
||||
}
|
||||
endPlayback(true, false, true, true);
|
||||
return;
|
||||
case MediaStatus.IDLE_REASON_ERROR:
|
||||
Log.w(TAG, "Got an error status from the Chromecast. "
|
||||
+ "Skipping, if possible, to the next episode...");
|
||||
EventBus.getDefault().post(new PlayerErrorEvent("Chromecast error code 1"));
|
||||
endPlayback(false, false, true, true);
|
||||
return;
|
||||
default:
|
||||
return;
|
||||
}
|
||||
break;
|
||||
case MediaStatus.PLAYER_STATE_UNKNOWN:
|
||||
if (playerStatus != PlayerStatus.INDETERMINATE || media != currentMedia) {
|
||||
setPlayerStatus(PlayerStatus.INDETERMINATE, currentMedia);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
Log.w(TAG, "Remote media state undetermined!");
|
||||
}
|
||||
if (mediaChanged) {
|
||||
callback.onMediaChanged(true);
|
||||
if (oldMedia != null) {
|
||||
callback.onPostPlayback(oldMedia, false, false, currentMedia != null);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void playMediaObject(@NonNull final Playable playable, final boolean stream,
|
||||
final boolean startWhenPrepared, final boolean prepareImmediately) {
|
||||
Log.d(TAG, "playMediaObject() called");
|
||||
playMediaObject(playable, false, stream, startWhenPrepared, prepareImmediately);
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal implementation of playMediaObject. This method has an additional parameter that
|
||||
* allows the caller to force a media player reset even if
|
||||
* the given playable parameter is the same object as the currently playing media.
|
||||
*
|
||||
* @see #playMediaObject(Playable, boolean, boolean, boolean)
|
||||
*/
|
||||
private void playMediaObject(@NonNull final Playable playable, final boolean forceReset,
|
||||
final boolean stream, final boolean startWhenPrepared, final boolean prepareImmediately) {
|
||||
if (!CastUtils.isCastable(playable, castContext.getSessionManager().getCurrentCastSession())) {
|
||||
Log.d(TAG, "media provided is not compatible with cast device");
|
||||
EventBus.getDefault().post(new PlayerErrorEvent("Media not compatible with cast device"));
|
||||
Playable nextPlayable = playable;
|
||||
do {
|
||||
nextPlayable = callback.getNextInQueue(nextPlayable);
|
||||
} while (nextPlayable != null && !CastUtils.isCastable(nextPlayable,
|
||||
castContext.getSessionManager().getCurrentCastSession()));
|
||||
if (nextPlayable != null) {
|
||||
playMediaObject(nextPlayable, forceReset, stream, startWhenPrepared, prepareImmediately);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
if (media != null) {
|
||||
if (!forceReset && media.getIdentifier().equals(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 {
|
||||
// set temporarily to pause in order to update list with current position
|
||||
boolean isPlaying = remoteMediaClient.isPlaying();
|
||||
int position = (int) remoteMediaClient.getApproximateStreamPosition();
|
||||
if (isPlaying) {
|
||||
callback.onPlaybackPause(media, position);
|
||||
}
|
||||
if (!media.getIdentifier().equals(playable.getIdentifier())) {
|
||||
final Playable oldMedia = media;
|
||||
callback.onPostPlayback(oldMedia, false, false, true);
|
||||
}
|
||||
setPlayerStatus(PlayerStatus.INDETERMINATE, null);
|
||||
}
|
||||
}
|
||||
|
||||
this.media = playable;
|
||||
remoteMedia = remoteVersion(playable);
|
||||
this.mediaType = media.getMediaType();
|
||||
this.startWhenPrepared.set(startWhenPrepared);
|
||||
setPlayerStatus(PlayerStatus.INITIALIZING, media);
|
||||
callback.ensureMediaInfoLoaded(media);
|
||||
callback.onMediaChanged(true);
|
||||
setPlayerStatus(PlayerStatus.INITIALIZED, media);
|
||||
if (prepareImmediately) {
|
||||
prepare();
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void resume() {
|
||||
int newPosition = RewindAfterPauseUtils.calculatePositionWithRewind(
|
||||
media.getPosition(),
|
||||
media.getLastPlayedTime());
|
||||
seekTo(newPosition);
|
||||
remoteMediaClient.play();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void pause(boolean abandonFocus, boolean reinit) {
|
||||
remoteMediaClient.pause();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void prepare() {
|
||||
if (playerStatus == PlayerStatus.INITIALIZED) {
|
||||
Log.d(TAG, "Preparing media player");
|
||||
setPlayerStatus(PlayerStatus.PREPARING, media);
|
||||
int position = media.getPosition();
|
||||
if (position > 0) {
|
||||
position = RewindAfterPauseUtils.calculatePositionWithRewind(
|
||||
position,
|
||||
media.getLastPlayedTime());
|
||||
}
|
||||
remoteMediaClient.load(new MediaLoadRequestData.Builder()
|
||||
.setMediaInfo(remoteMedia)
|
||||
.setAutoplay(startWhenPrepared.get())
|
||||
.setCurrentTime(position).build());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void reinit() {
|
||||
Log.d(TAG, "reinit() called");
|
||||
if (media != null) {
|
||||
playMediaObject(media, true, false, startWhenPrepared.get(), false);
|
||||
} else {
|
||||
Log.d(TAG, "Call to reinit was ignored: media was null");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void seekTo(int t) {
|
||||
new Exception("Seeking to " + t).printStackTrace();
|
||||
remoteMediaClient.seek(new MediaSeekOptions.Builder()
|
||||
.setPosition(t).build());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void seekDelta(int d) {
|
||||
int position = getPosition();
|
||||
if (position != Playable.INVALID_TIME) {
|
||||
seekTo(position + d);
|
||||
} else {
|
||||
Log.e(TAG, "getPosition() returned INVALID_TIME in seekDelta");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getDuration() {
|
||||
int retVal = (int) remoteMediaClient.getStreamDuration();
|
||||
if (retVal == Playable.INVALID_TIME && media != null && media.getDuration() > 0) {
|
||||
retVal = media.getDuration();
|
||||
}
|
||||
return retVal;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getPosition() {
|
||||
int retVal = (int) remoteMediaClient.getApproximateStreamPosition();
|
||||
if (retVal <= 0 && media != null && media.getPosition() >= 0) {
|
||||
retVal = media.getPosition();
|
||||
}
|
||||
return retVal;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isStartWhenPrepared() {
|
||||
return startWhenPrepared.get();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setStartWhenPrepared(boolean startWhenPrepared) {
|
||||
this.startWhenPrepared.set(startWhenPrepared);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setPlaybackParams(float speed, boolean skipSilence) {
|
||||
double playbackRate = (float) Math.max(MediaLoadOptions.PLAYBACK_RATE_MIN,
|
||||
Math.min(MediaLoadOptions.PLAYBACK_RATE_MAX, speed));
|
||||
remoteMediaClient.setPlaybackRate(playbackRate);
|
||||
}
|
||||
|
||||
@Override
|
||||
public float getPlaybackSpeed() {
|
||||
MediaStatus status = remoteMediaClient.getMediaStatus();
|
||||
return status != null ? (float) status.getPlaybackRate() : 1.0f;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setVolume(float volumeLeft, float volumeRight) {
|
||||
Log.d(TAG, "Setting the Stream volume on Remote Media Player");
|
||||
remoteMediaClient.setStreamVolume(volumeLeft);
|
||||
}
|
||||
|
||||
@Override
|
||||
public MediaType getCurrentMediaType() {
|
||||
return mediaType;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isStreaming() {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void shutdown() {
|
||||
remoteMediaClient.unregisterCallback(remoteMediaClientCallback);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setVideoSurface(SurfaceHolder surface) {
|
||||
throw new UnsupportedOperationException("Setting Video Surface unsupported in Remote Media Player");
|
||||
}
|
||||
|
||||
@Override
|
||||
public void resetVideoSurface() {
|
||||
Log.e(TAG, "Resetting Video Surface unsupported in Remote Media Player");
|
||||
}
|
||||
|
||||
@Override
|
||||
public Pair<Integer, Integer> getVideoSize() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Playable getPlayable() {
|
||||
return media;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void setPlayable(Playable playable) {
|
||||
if (playable != media) {
|
||||
media = playable;
|
||||
remoteMedia = remoteVersion(playable);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public List<String> getAudioTracks() {
|
||||
return Collections.emptyList();
|
||||
}
|
||||
|
||||
public void setAudioTrack(int track) {
|
||||
|
||||
}
|
||||
|
||||
public int getSelectedAudioTrack() {
|
||||
return -1;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void endPlayback(boolean hasEnded, boolean wasSkipped, boolean shouldContinue,
|
||||
boolean toStoppedState) {
|
||||
Log.d(TAG, "endPlayback() called");
|
||||
boolean isPlaying = playerStatus == PlayerStatus.PLAYING;
|
||||
if (playerStatus != PlayerStatus.INDETERMINATE) {
|
||||
setPlayerStatus(PlayerStatus.INDETERMINATE, media);
|
||||
}
|
||||
if (media != null && wasSkipped) {
|
||||
// current position only really matters when we skip
|
||||
int position = getPosition();
|
||||
if (position >= 0) {
|
||||
media.setPosition(position);
|
||||
}
|
||||
}
|
||||
final Playable currentMedia = media;
|
||||
Playable nextMedia = null;
|
||||
if (shouldContinue) {
|
||||
nextMedia = callback.getNextInQueue(currentMedia);
|
||||
|
||||
boolean playNextEpisode = isPlaying && nextMedia != null;
|
||||
if (playNextEpisode) {
|
||||
Log.d(TAG, "Playback of next episode will start immediately.");
|
||||
} else if (nextMedia == null) {
|
||||
Log.d(TAG, "No more episodes available to play");
|
||||
} else {
|
||||
Log.d(TAG, "Loading next episode, but not playing automatically.");
|
||||
}
|
||||
|
||||
if (nextMedia != null) {
|
||||
callback.onPlaybackEnded(nextMedia.getMediaType(), !playNextEpisode);
|
||||
// setting media to null signals to playMediaObject() that we're taking care of post-playback processing
|
||||
media = null;
|
||||
playMediaObject(nextMedia, false, true, playNextEpisode, playNextEpisode);
|
||||
}
|
||||
}
|
||||
if (shouldContinue || toStoppedState) {
|
||||
if (nextMedia == null) {
|
||||
remoteMediaClient.stop();
|
||||
// Otherwise we rely on the chromecast callback to tell us the playback has stopped.
|
||||
callback.onPostPlayback(currentMedia, hasEnded, wasSkipped, false);
|
||||
} else {
|
||||
callback.onPostPlayback(currentMedia, hasEnded, wasSkipped, true);
|
||||
}
|
||||
} else if (isPlaying) {
|
||||
callback.onPlaybackPause(currentMedia,
|
||||
currentMedia != null ? currentMedia.getPosition() : Playable.INVALID_TIME);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean shouldLockWifi() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isCasting() {
|
||||
return true;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,511 @@
|
|||
package ac.mdiq.podvinci.playback.cast
|
||||
|
||||
import ac.mdiq.podvinci.event.PlayerErrorEvent
|
||||
import ac.mdiq.podvinci.event.playback.BufferUpdateEvent
|
||||
import ac.mdiq.podvinci.model.feed.FeedMedia
|
||||
import ac.mdiq.podvinci.model.playback.MediaType
|
||||
import ac.mdiq.podvinci.model.playback.Playable
|
||||
import ac.mdiq.podvinci.model.playback.RemoteMedia
|
||||
import ac.mdiq.podvinci.playback.base.PlaybackServiceMediaPlayer
|
||||
import ac.mdiq.podvinci.playback.base.PlayerStatus
|
||||
import ac.mdiq.podvinci.playback.base.RewindAfterPauseUtils.calculatePositionWithRewind
|
||||
import android.annotation.SuppressLint
|
||||
import android.content.Context
|
||||
import android.util.Log
|
||||
import android.util.Pair
|
||||
import android.view.SurfaceHolder
|
||||
import com.google.android.gms.cast.*
|
||||
import com.google.android.gms.cast.framework.CastContext
|
||||
import com.google.android.gms.cast.framework.CastState
|
||||
import com.google.android.gms.cast.framework.media.RemoteMediaClient
|
||||
import com.google.android.gms.common.ConnectionResult
|
||||
import com.google.android.gms.common.GoogleApiAvailability
|
||||
import org.greenrobot.eventbus.EventBus
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
import kotlin.concurrent.Volatile
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
|
||||
/**
|
||||
* Implementation of PlaybackServiceMediaPlayer suitable for remote playback on Cast Devices.
|
||||
*/
|
||||
@SuppressLint("VisibleForTests")
|
||||
class CastPsmp(context: Context, callback: PSMPCallback) : PlaybackServiceMediaPlayer(context, callback) {
|
||||
@Volatile
|
||||
private var media: Playable?
|
||||
|
||||
@Volatile
|
||||
private var mediaType: MediaType?
|
||||
|
||||
@Volatile
|
||||
private var remoteMedia: MediaInfo? = null
|
||||
|
||||
@Volatile
|
||||
private var remoteState: Int
|
||||
private val castContext = CastContext.getSharedInstance(context)
|
||||
private val remoteMediaClient = castContext.sessionManager.currentCastSession!!.remoteMediaClient
|
||||
|
||||
private val isBuffering: AtomicBoolean
|
||||
|
||||
private val startWhenPrepared: AtomicBoolean
|
||||
|
||||
private val remoteMediaClientCallback: RemoteMediaClient.Callback = object : RemoteMediaClient.Callback() {
|
||||
override fun onMetadataUpdated() {
|
||||
super.onMetadataUpdated()
|
||||
onRemoteMediaPlayerStatusUpdated()
|
||||
}
|
||||
|
||||
override fun onPreloadStatusUpdated() {
|
||||
super.onPreloadStatusUpdated()
|
||||
onRemoteMediaPlayerStatusUpdated()
|
||||
}
|
||||
|
||||
override fun onStatusUpdated() {
|
||||
super.onStatusUpdated()
|
||||
onRemoteMediaPlayerStatusUpdated()
|
||||
}
|
||||
|
||||
override fun onMediaError(mediaError: MediaError) {
|
||||
EventBus.getDefault().post(PlayerErrorEvent(mediaError.reason!!))
|
||||
}
|
||||
}
|
||||
|
||||
init {
|
||||
remoteMediaClient!!.registerCallback(remoteMediaClientCallback)
|
||||
media = null
|
||||
mediaType = null
|
||||
startWhenPrepared = AtomicBoolean(false)
|
||||
isBuffering = AtomicBoolean(false)
|
||||
remoteState = MediaStatus.PLAYER_STATE_UNKNOWN
|
||||
}
|
||||
|
||||
private fun setBuffering(buffering: Boolean) {
|
||||
if (buffering && isBuffering.compareAndSet(false, true)) {
|
||||
EventBus.getDefault().post(BufferUpdateEvent.started())
|
||||
} else if (!buffering && isBuffering.compareAndSet(true, false)) {
|
||||
EventBus.getDefault().post(BufferUpdateEvent.ended())
|
||||
}
|
||||
}
|
||||
|
||||
private fun localVersion(info: MediaInfo?): Playable? {
|
||||
if (info == null || info.metadata == null) {
|
||||
return null
|
||||
}
|
||||
if (CastUtils.matches(info, media)) {
|
||||
return media
|
||||
}
|
||||
val streamUrl = info.metadata!!.getString(CastUtils.KEY_STREAM_URL)
|
||||
return if (streamUrl == null) CastUtils.makeRemoteMedia(info) else callback.findMedia(streamUrl)
|
||||
}
|
||||
|
||||
private fun remoteVersion(playable: Playable?): MediaInfo? {
|
||||
if (playable == null) {
|
||||
return null
|
||||
}
|
||||
if (CastUtils.matches(remoteMedia, playable)) {
|
||||
return remoteMedia
|
||||
}
|
||||
if (playable is FeedMedia) {
|
||||
return MediaInfoCreator.from(playable)
|
||||
}
|
||||
if (playable is RemoteMedia) {
|
||||
return MediaInfoCreator.from(playable)
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
private fun onRemoteMediaPlayerStatusUpdated() {
|
||||
val status = remoteMediaClient!!.mediaStatus
|
||||
if (status == null) {
|
||||
Log.d(TAG, "Received null MediaStatus")
|
||||
return
|
||||
} else {
|
||||
Log.d(TAG, "Received remote status/media update. New state=" + status.playerState)
|
||||
}
|
||||
var state = status.playerState
|
||||
val oldState = remoteState
|
||||
remoteMedia = status.mediaInfo
|
||||
val mediaChanged = !CastUtils.matches(remoteMedia, media)
|
||||
var stateChanged = state != oldState
|
||||
if (!mediaChanged && !stateChanged) {
|
||||
Log.d(TAG, "Both media and state haven't changed, so nothing to do")
|
||||
return
|
||||
}
|
||||
val currentMedia = if (mediaChanged) localVersion(remoteMedia) else media
|
||||
val oldMedia = media
|
||||
val position = status.streamPosition.toInt()
|
||||
// check for incompatible states
|
||||
if ((state == MediaStatus.PLAYER_STATE_PLAYING || state == MediaStatus.PLAYER_STATE_PAUSED)
|
||||
&& currentMedia == null) {
|
||||
Log.w(TAG, "RemoteMediaPlayer returned playing or pausing state, but with no media")
|
||||
state = MediaStatus.PLAYER_STATE_UNKNOWN
|
||||
stateChanged = oldState != MediaStatus.PLAYER_STATE_UNKNOWN
|
||||
}
|
||||
|
||||
if (stateChanged) {
|
||||
remoteState = state
|
||||
}
|
||||
|
||||
if (mediaChanged && stateChanged && oldState == MediaStatus.PLAYER_STATE_PLAYING && state != MediaStatus.PLAYER_STATE_IDLE) {
|
||||
callback.onPlaybackPause(null, Playable.INVALID_TIME)
|
||||
// We don't want setPlayerStatus to handle the onPlaybackPause callback
|
||||
setPlayerStatus(PlayerStatus.INDETERMINATE, currentMedia)
|
||||
}
|
||||
|
||||
setBuffering(state == MediaStatus.PLAYER_STATE_BUFFERING)
|
||||
|
||||
when (state) {
|
||||
MediaStatus.PLAYER_STATE_PLAYING -> {
|
||||
if (!stateChanged) {
|
||||
//These steps are necessary because they won't be performed by setPlayerStatus()
|
||||
if (position >= 0) {
|
||||
currentMedia!!.setPosition(position)
|
||||
}
|
||||
currentMedia!!.onPlaybackStart()
|
||||
}
|
||||
setPlayerStatus(PlayerStatus.PLAYING, currentMedia, position)
|
||||
}
|
||||
MediaStatus.PLAYER_STATE_PAUSED -> setPlayerStatus(PlayerStatus.PAUSED, currentMedia, position)
|
||||
MediaStatus.PLAYER_STATE_BUFFERING -> setPlayerStatus(if ((mediaChanged || playerStatus == PlayerStatus.PREPARING)
|
||||
) PlayerStatus.PREPARING else PlayerStatus.SEEKING, currentMedia,
|
||||
currentMedia?.getPosition() ?: Playable.INVALID_TIME)
|
||||
MediaStatus.PLAYER_STATE_IDLE -> {
|
||||
val reason = status.idleReason
|
||||
when (reason) {
|
||||
MediaStatus.IDLE_REASON_CANCELED -> {
|
||||
// Essentially means stopped at the request of a user
|
||||
callback.onPlaybackEnded(null, true)
|
||||
setPlayerStatus(PlayerStatus.STOPPED, currentMedia)
|
||||
if (oldMedia != null) {
|
||||
if (position >= 0) {
|
||||
oldMedia.setPosition(position)
|
||||
}
|
||||
callback.onPostPlayback(oldMedia, false, false, false)
|
||||
}
|
||||
// onPlaybackEnded pretty much takes care of updating the UI
|
||||
return
|
||||
}
|
||||
MediaStatus.IDLE_REASON_INTERRUPTED -> {
|
||||
// Means that a request to load a different media was sent
|
||||
// Not sure if currentMedia already reflects the to be loaded one
|
||||
if (mediaChanged && oldState == MediaStatus.PLAYER_STATE_PLAYING) {
|
||||
callback.onPlaybackPause(null, Playable.INVALID_TIME)
|
||||
setPlayerStatus(PlayerStatus.INDETERMINATE, currentMedia)
|
||||
}
|
||||
setPlayerStatus(PlayerStatus.PREPARING, currentMedia)
|
||||
}
|
||||
MediaStatus.IDLE_REASON_NONE -> // This probably only happens when we connected but no command has been sent yet.
|
||||
setPlayerStatus(PlayerStatus.INITIALIZED, currentMedia)
|
||||
MediaStatus.IDLE_REASON_FINISHED -> {
|
||||
// This is our onCompletionListener...
|
||||
if (mediaChanged && currentMedia != null) {
|
||||
media = currentMedia
|
||||
}
|
||||
endPlayback(true, false, true, true)
|
||||
return
|
||||
}
|
||||
MediaStatus.IDLE_REASON_ERROR -> {
|
||||
Log.w(TAG, "Got an error status from the Chromecast. "
|
||||
+ "Skipping, if possible, to the next episode...")
|
||||
EventBus.getDefault().post(PlayerErrorEvent("Chromecast error code 1"))
|
||||
endPlayback(false, false, true, true)
|
||||
return
|
||||
}
|
||||
else -> return
|
||||
}
|
||||
}
|
||||
MediaStatus.PLAYER_STATE_UNKNOWN -> if (playerStatus != PlayerStatus.INDETERMINATE || media !== currentMedia) {
|
||||
setPlayerStatus(PlayerStatus.INDETERMINATE, currentMedia)
|
||||
}
|
||||
else -> Log.w(TAG, "Remote media state undetermined!")
|
||||
}
|
||||
if (mediaChanged) {
|
||||
callback.onMediaChanged(true)
|
||||
if (oldMedia != null) {
|
||||
callback.onPostPlayback(oldMedia, false, false, currentMedia != null)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun playMediaObject(playable: Playable, stream: Boolean,
|
||||
startWhenPrepared: Boolean, prepareImmediately: Boolean
|
||||
) {
|
||||
Log.d(TAG, "playMediaObject() called")
|
||||
playMediaObject(playable, false, stream, startWhenPrepared, prepareImmediately)
|
||||
}
|
||||
|
||||
/**
|
||||
* Internal implementation of playMediaObject. This method has an additional parameter that
|
||||
* allows the caller to force a media player reset even if
|
||||
* the given playable parameter is the same object as the currently playing media.
|
||||
*
|
||||
* @see .playMediaObject
|
||||
*/
|
||||
private fun playMediaObject(playable: Playable, forceReset: Boolean,
|
||||
stream: Boolean, startWhenPrepared: Boolean, prepareImmediately: Boolean
|
||||
) {
|
||||
if (!CastUtils.isCastable(playable, castContext.sessionManager.currentCastSession)) {
|
||||
Log.d(TAG, "media provided is not compatible with cast device")
|
||||
EventBus.getDefault().post(PlayerErrorEvent("Media not compatible with cast device"))
|
||||
var nextPlayable: Playable? = playable
|
||||
do {
|
||||
nextPlayable = callback.getNextInQueue(nextPlayable)
|
||||
} while (nextPlayable != null && !CastUtils.isCastable(nextPlayable,
|
||||
castContext.sessionManager.currentCastSession))
|
||||
if (nextPlayable != null) {
|
||||
playMediaObject(nextPlayable, forceReset, stream, startWhenPrepared, prepareImmediately)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if (media != null) {
|
||||
if (!forceReset && media!!.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 {
|
||||
// set temporarily to pause in order to update list with current position
|
||||
val isPlaying = remoteMediaClient!!.isPlaying
|
||||
val position = remoteMediaClient.approximateStreamPosition.toInt()
|
||||
if (isPlaying) {
|
||||
callback.onPlaybackPause(media, position)
|
||||
}
|
||||
if (media != null && media?.getIdentifier() != playable.getIdentifier()) {
|
||||
val oldMedia: Playable = media!!
|
||||
callback.onPostPlayback(oldMedia, false, false, true)
|
||||
}
|
||||
setPlayerStatus(PlayerStatus.INDETERMINATE, null)
|
||||
}
|
||||
}
|
||||
|
||||
this.media = playable
|
||||
remoteMedia = remoteVersion(playable)
|
||||
this.mediaType = media!!.getMediaType()
|
||||
this.startWhenPrepared.set(startWhenPrepared)
|
||||
setPlayerStatus(PlayerStatus.INITIALIZING, media)
|
||||
callback.ensureMediaInfoLoaded(media!!)
|
||||
callback.onMediaChanged(true)
|
||||
setPlayerStatus(PlayerStatus.INITIALIZED, media)
|
||||
if (prepareImmediately) {
|
||||
prepare()
|
||||
}
|
||||
}
|
||||
|
||||
override fun resume() {
|
||||
val newPosition = calculatePositionWithRewind(
|
||||
media!!.getPosition(),
|
||||
media!!.getLastPlayedTime())
|
||||
seekTo(newPosition)
|
||||
remoteMediaClient!!.play()
|
||||
}
|
||||
|
||||
override fun pause(abandonFocus: Boolean, reinit: Boolean) {
|
||||
remoteMediaClient!!.pause()
|
||||
}
|
||||
|
||||
override fun prepare() {
|
||||
if (playerStatus == PlayerStatus.INITIALIZED) {
|
||||
Log.d(TAG, "Preparing media player")
|
||||
setPlayerStatus(PlayerStatus.PREPARING, media)
|
||||
var position = media!!.getPosition()
|
||||
if (position > 0) {
|
||||
position = calculatePositionWithRewind(
|
||||
position,
|
||||
media!!.getLastPlayedTime())
|
||||
}
|
||||
remoteMediaClient!!.load(MediaLoadRequestData.Builder()
|
||||
.setMediaInfo(remoteMedia)
|
||||
.setAutoplay(startWhenPrepared.get())
|
||||
.setCurrentTime(position.toLong()).build())
|
||||
}
|
||||
}
|
||||
|
||||
override fun reinit() {
|
||||
Log.d(TAG, "reinit() called")
|
||||
if (media != null) {
|
||||
playMediaObject(media!!, true, false, startWhenPrepared.get(), false)
|
||||
} else {
|
||||
Log.d(TAG, "Call to reinit was ignored: media was null")
|
||||
}
|
||||
}
|
||||
|
||||
override fun seekTo(t: Int) {
|
||||
Exception("Seeking to $t").printStackTrace()
|
||||
remoteMediaClient!!.seek(MediaSeekOptions.Builder()
|
||||
.setPosition(t.toLong()).build())
|
||||
}
|
||||
|
||||
override fun seekDelta(d: Int) {
|
||||
val position = getPosition()
|
||||
if (position != Playable.INVALID_TIME) {
|
||||
seekTo(position + d)
|
||||
} else {
|
||||
Log.e(TAG, "getPosition() returned INVALID_TIME in seekDelta")
|
||||
}
|
||||
}
|
||||
|
||||
override fun getDuration(): Int {
|
||||
var retVal = remoteMediaClient!!.streamDuration.toInt()
|
||||
if (retVal == Playable.INVALID_TIME && media != null && media!!.getDuration() > 0) {
|
||||
retVal = media!!.getDuration()
|
||||
}
|
||||
return retVal
|
||||
}
|
||||
|
||||
override fun getPosition(): Int {
|
||||
var retVal = remoteMediaClient!!.approximateStreamPosition.toInt()
|
||||
if (retVal <= 0 && media != null && media!!.getPosition() >= 0) {
|
||||
retVal = media!!.getPosition()
|
||||
}
|
||||
return retVal
|
||||
}
|
||||
|
||||
override fun isStartWhenPrepared(): Boolean {
|
||||
return startWhenPrepared.get()
|
||||
}
|
||||
|
||||
override fun setStartWhenPrepared(startWhenPrepared: Boolean) {
|
||||
this.startWhenPrepared.set(startWhenPrepared)
|
||||
}
|
||||
|
||||
override fun setPlaybackParams(speed: Float, skipSilence: Boolean) {
|
||||
val playbackRate = max(MediaLoadOptions.PLAYBACK_RATE_MIN,
|
||||
min(MediaLoadOptions.PLAYBACK_RATE_MAX, speed.toDouble())).toFloat().toDouble()
|
||||
remoteMediaClient!!.setPlaybackRate(playbackRate)
|
||||
}
|
||||
|
||||
override fun getPlaybackSpeed(): Float {
|
||||
val status = remoteMediaClient!!.mediaStatus
|
||||
return status?.playbackRate?.toFloat() ?: 1.0f
|
||||
}
|
||||
|
||||
override fun setVolume(volumeLeft: Float, volumeRight: Float) {
|
||||
Log.d(TAG, "Setting the Stream volume on Remote Media Player")
|
||||
remoteMediaClient!!.setStreamVolume(volumeLeft.toDouble())
|
||||
}
|
||||
|
||||
override fun getCurrentMediaType(): MediaType? {
|
||||
return mediaType
|
||||
}
|
||||
|
||||
override fun isStreaming(): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
override fun shutdown() {
|
||||
remoteMediaClient!!.unregisterCallback(remoteMediaClientCallback)
|
||||
}
|
||||
|
||||
override fun setVideoSurface(surface: SurfaceHolder?) {
|
||||
throw UnsupportedOperationException("Setting Video Surface unsupported in Remote Media Player")
|
||||
}
|
||||
|
||||
override fun resetVideoSurface() {
|
||||
Log.e(TAG, "Resetting Video Surface unsupported in Remote Media Player")
|
||||
}
|
||||
|
||||
override fun getVideoSize(): Pair<Int, Int>? {
|
||||
return null
|
||||
}
|
||||
|
||||
override fun getPlayable(): Playable? {
|
||||
return media
|
||||
}
|
||||
|
||||
override fun setPlayable(playable: Playable?) {
|
||||
if (playable !== media) {
|
||||
media = playable
|
||||
remoteMedia = remoteVersion(playable)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getAudioTracks(): List<String> {
|
||||
return emptyList<String>()
|
||||
}
|
||||
|
||||
override fun setAudioTrack(track: Int) {
|
||||
}
|
||||
|
||||
override fun getSelectedAudioTrack(): Int {
|
||||
return -1
|
||||
}
|
||||
|
||||
override fun endPlayback(hasEnded: Boolean, wasSkipped: Boolean, shouldContinue: Boolean,
|
||||
toStoppedState: Boolean
|
||||
) {
|
||||
Log.d(TAG, "endPlayback() called")
|
||||
val isPlaying = playerStatus == PlayerStatus.PLAYING
|
||||
if (playerStatus != PlayerStatus.INDETERMINATE) {
|
||||
setPlayerStatus(PlayerStatus.INDETERMINATE, media)
|
||||
}
|
||||
if (media != null && wasSkipped) {
|
||||
// current position only really matters when we skip
|
||||
val position = getPosition()
|
||||
if (position >= 0) {
|
||||
media!!.setPosition(position)
|
||||
}
|
||||
}
|
||||
val currentMedia = media
|
||||
var nextMedia: Playable? = null
|
||||
if (shouldContinue) {
|
||||
nextMedia = callback.getNextInQueue(currentMedia)
|
||||
|
||||
val playNextEpisode = isPlaying && nextMedia != null
|
||||
if (playNextEpisode) {
|
||||
Log.d(TAG, "Playback of next episode will start immediately.")
|
||||
} else if (nextMedia == null) {
|
||||
Log.d(TAG, "No more episodes available to play")
|
||||
} else {
|
||||
Log.d(TAG, "Loading next episode, but not playing automatically.")
|
||||
}
|
||||
|
||||
if (nextMedia != null) {
|
||||
callback.onPlaybackEnded(nextMedia.getMediaType(), !playNextEpisode)
|
||||
// setting media to null signals to playMediaObject() that we're taking care of post-playback processing
|
||||
media = null
|
||||
playMediaObject(nextMedia, false, true, playNextEpisode, playNextEpisode)
|
||||
}
|
||||
}
|
||||
if (shouldContinue || toStoppedState) {
|
||||
if (nextMedia == null) {
|
||||
remoteMediaClient!!.stop()
|
||||
// Otherwise we rely on the chromecast callback to tell us the playback has stopped.
|
||||
callback.onPostPlayback(currentMedia!!, hasEnded, wasSkipped, false)
|
||||
} else {
|
||||
callback.onPostPlayback(currentMedia!!, hasEnded, wasSkipped, true)
|
||||
}
|
||||
} else if (isPlaying) {
|
||||
callback.onPlaybackPause(currentMedia,
|
||||
currentMedia?.getPosition() ?: Playable.INVALID_TIME)
|
||||
}
|
||||
}
|
||||
|
||||
override fun shouldLockWifi(): Boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
override fun isCasting(): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
companion object {
|
||||
const val TAG: String = "CastPSMP"
|
||||
|
||||
fun getInstanceIfConnected(context: Context,
|
||||
callback: PSMPCallback
|
||||
): PlaybackServiceMediaPlayer? {
|
||||
if (GoogleApiAvailability.getInstance()
|
||||
.isGooglePlayServicesAvailable(context) != ConnectionResult.SUCCESS) {
|
||||
return null
|
||||
}
|
||||
try {
|
||||
if (CastContext.getSharedInstance(context).castState == CastState.CONNECTED) {
|
||||
return CastPsmp(context, callback)
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
}
|
||||
return null
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,76 +0,0 @@
|
|||
package ac.mdiq.podvinci.playback.cast;
|
||||
|
||||
import android.content.Context;
|
||||
import androidx.annotation.NonNull;
|
||||
import com.google.android.gms.cast.framework.CastContext;
|
||||
import com.google.android.gms.cast.framework.CastSession;
|
||||
import com.google.android.gms.cast.framework.SessionManagerListener;
|
||||
import com.google.android.gms.common.ConnectionResult;
|
||||
import com.google.android.gms.common.GoogleApiAvailability;
|
||||
|
||||
public class CastStateListener implements SessionManagerListener<CastSession> {
|
||||
private final CastContext castContext;
|
||||
|
||||
public CastStateListener(Context context) {
|
||||
if (GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(context) != ConnectionResult.SUCCESS) {
|
||||
castContext = null;
|
||||
return;
|
||||
}
|
||||
CastContext castCtx;
|
||||
try {
|
||||
castCtx = CastContext.getSharedInstance(context);
|
||||
castCtx.getSessionManager().addSessionManagerListener(this, CastSession.class);
|
||||
} catch (Exception e) {
|
||||
e.printStackTrace();
|
||||
castCtx = null;
|
||||
}
|
||||
castContext = castCtx;
|
||||
}
|
||||
|
||||
public void destroy() {
|
||||
if (castContext != null) {
|
||||
castContext.getSessionManager().removeSessionManagerListener(this, CastSession.class);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSessionStarting(@NonNull CastSession castSession) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSessionStarted(@NonNull CastSession session, @NonNull String sessionId) {
|
||||
onSessionStartedOrEnded();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSessionStartFailed(@NonNull CastSession castSession, int i) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSessionEnding(@NonNull CastSession castSession) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSessionResumed(@NonNull CastSession session, boolean wasSuspended) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSessionResumeFailed(@NonNull CastSession castSession, int i) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSessionSuspended(@NonNull CastSession castSession, int i) {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSessionEnded(@NonNull CastSession session, int error) {
|
||||
onSessionStartedOrEnded();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onSessionResuming(@NonNull CastSession castSession, @NonNull String s) {
|
||||
}
|
||||
|
||||
public void onSessionStartedOrEnded() {
|
||||
}
|
||||
}
|
|
@ -0,0 +1,66 @@
|
|||
package ac.mdiq.podvinci.playback.cast
|
||||
|
||||
import android.content.Context
|
||||
import com.google.android.gms.cast.framework.CastContext
|
||||
import com.google.android.gms.cast.framework.CastSession
|
||||
import com.google.android.gms.cast.framework.SessionManagerListener
|
||||
import com.google.android.gms.common.ConnectionResult
|
||||
import com.google.android.gms.common.GoogleApiAvailability
|
||||
|
||||
open class CastStateListener(context: Context?) : SessionManagerListener<CastSession> {
|
||||
private var castContext: CastContext?
|
||||
|
||||
init {
|
||||
if (GoogleApiAvailability.getInstance().isGooglePlayServicesAvailable(context!!) != ConnectionResult.SUCCESS) {
|
||||
castContext = null
|
||||
} else {
|
||||
var castCtx: CastContext?
|
||||
try {
|
||||
castCtx = CastContext.getSharedInstance(context)
|
||||
castCtx.sessionManager.addSessionManagerListener(this, CastSession::class.java)
|
||||
} catch (e: Exception) {
|
||||
e.printStackTrace()
|
||||
castCtx = null
|
||||
}
|
||||
castContext = castCtx
|
||||
}
|
||||
}
|
||||
|
||||
fun destroy() {
|
||||
if (castContext != null) {
|
||||
castContext!!.sessionManager.removeSessionManagerListener(this, CastSession::class.java)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onSessionStarting(castSession: CastSession) {
|
||||
}
|
||||
|
||||
override fun onSessionStarted(session: CastSession, sessionId: String) {
|
||||
onSessionStartedOrEnded()
|
||||
}
|
||||
|
||||
override fun onSessionStartFailed(castSession: CastSession, i: Int) {
|
||||
}
|
||||
|
||||
override fun onSessionEnding(castSession: CastSession) {
|
||||
}
|
||||
|
||||
override fun onSessionResumed(session: CastSession, wasSuspended: Boolean) {
|
||||
}
|
||||
|
||||
override fun onSessionResumeFailed(castSession: CastSession, i: Int) {
|
||||
}
|
||||
|
||||
override fun onSessionSuspended(castSession: CastSession, i: Int) {
|
||||
}
|
||||
|
||||
override fun onSessionEnded(session: CastSession, error: Int) {
|
||||
onSessionStartedOrEnded()
|
||||
}
|
||||
|
||||
override fun onSessionResuming(castSession: CastSession, s: String) {
|
||||
}
|
||||
|
||||
open fun onSessionStartedOrEnded() {
|
||||
}
|
||||
}
|
|
@ -1,181 +0,0 @@
|
|||
package ac.mdiq.podvinci.playback.cast;
|
||||
|
||||
import android.content.ContentResolver;
|
||||
import android.util.Log;
|
||||
import android.text.TextUtils;
|
||||
import com.google.android.gms.cast.CastDevice;
|
||||
import com.google.android.gms.cast.MediaInfo;
|
||||
import com.google.android.gms.cast.MediaMetadata;
|
||||
import com.google.android.gms.cast.framework.CastSession;
|
||||
import com.google.android.gms.common.images.WebImage;
|
||||
import ac.mdiq.podvinci.model.feed.Feed;
|
||||
import ac.mdiq.podvinci.model.feed.FeedItem;
|
||||
import ac.mdiq.podvinci.model.feed.FeedMedia;
|
||||
import ac.mdiq.podvinci.model.playback.Playable;
|
||||
import ac.mdiq.podvinci.model.playback.RemoteMedia;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Helper functions for Cast support.
|
||||
*/
|
||||
public class CastUtils {
|
||||
private CastUtils() {
|
||||
}
|
||||
|
||||
private static final String TAG = "CastUtils";
|
||||
|
||||
public static final String KEY_MEDIA_ID = "ac.mdiq.podvinci.core.cast.MediaId";
|
||||
|
||||
public static final String KEY_EPISODE_IDENTIFIER = "ac.mdiq.podvinci.core.cast.EpisodeId";
|
||||
public static final String KEY_EPISODE_LINK = "ac.mdiq.podvinci.core.cast.EpisodeLink";
|
||||
public static final String KEY_STREAM_URL = "ac.mdiq.podvinci.core.cast.StreamUrl";
|
||||
public static final String KEY_FEED_URL = "ac.mdiq.podvinci.core.cast.FeedUrl";
|
||||
public static final String KEY_FEED_WEBSITE = "ac.mdiq.podvinci.core.cast.FeedWebsite";
|
||||
public static final String KEY_EPISODE_NOTES = "ac.mdiq.podvinci.core.cast.EpisodeNotes";
|
||||
|
||||
/**
|
||||
* The field <code>PodVinci.FormatVersion</code> specifies which version of MediaMetaData
|
||||
* fields we're using. Future implementations should try to be backwards compatible with earlier
|
||||
* versions, and earlier versions should be forward compatible until the version indicated by
|
||||
* <code>MAX_VERSION_FORWARD_COMPATIBILITY</code>. If an update makes the format unreadable for
|
||||
* an earlier version, then its version number should be greater than the
|
||||
* <code>MAX_VERSION_FORWARD_COMPATIBILITY</code> value set on the earlier one, so that it
|
||||
* doesn't try to parse the object.
|
||||
*/
|
||||
public static final String KEY_FORMAT_VERSION = "ac.mdiq.podvinci.core.cast.FormatVersion";
|
||||
public static final int FORMAT_VERSION_VALUE = 1;
|
||||
public static final int MAX_VERSION_FORWARD_COMPATIBILITY = 9999;
|
||||
|
||||
public static boolean isCastable(Playable media, CastSession castSession) {
|
||||
if (media == null || castSession == null || castSession.getCastDevice() == null) {
|
||||
return false;
|
||||
}
|
||||
if (media instanceof FeedMedia || media instanceof RemoteMedia) {
|
||||
String url = media.getStreamUrl();
|
||||
if (url == null || url.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
if (url.startsWith(ContentResolver.SCHEME_CONTENT)) {
|
||||
return false; // Local feed
|
||||
}
|
||||
switch (media.getMediaType()) {
|
||||
case AUDIO:
|
||||
return castSession.getCastDevice().hasCapability(CastDevice.CAPABILITY_AUDIO_OUT);
|
||||
case VIDEO:
|
||||
return castSession.getCastDevice().hasCapability(CastDevice.CAPABILITY_VIDEO_OUT);
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts {@link MediaInfo} objects into the appropriate implementation of {@link Playable}.
|
||||
* @return {@link Playable} object in a format proper for casting.
|
||||
*/
|
||||
public static Playable makeRemoteMedia(MediaInfo media) {
|
||||
MediaMetadata metadata = media.getMetadata();
|
||||
int version = metadata.getInt(KEY_FORMAT_VERSION);
|
||||
if (version <= 0 || version > MAX_VERSION_FORWARD_COMPATIBILITY) {
|
||||
Log.w(TAG, "MediaInfo object obtained from the cast device is not compatible with this"
|
||||
+ "version of PodVinci CastUtils, curVer=" + FORMAT_VERSION_VALUE
|
||||
+ ", object version=" + version);
|
||||
return null;
|
||||
}
|
||||
List<WebImage> imageList = metadata.getImages();
|
||||
String imageUrl = null;
|
||||
if (!imageList.isEmpty()) {
|
||||
imageUrl = imageList.get(0).getUrl().toString();
|
||||
}
|
||||
String notes = metadata.getString(KEY_EPISODE_NOTES);
|
||||
RemoteMedia result = new RemoteMedia(media.getContentId(),
|
||||
metadata.getString(KEY_EPISODE_IDENTIFIER),
|
||||
metadata.getString(KEY_FEED_URL),
|
||||
metadata.getString(MediaMetadata.KEY_SUBTITLE),
|
||||
metadata.getString(MediaMetadata.KEY_TITLE),
|
||||
metadata.getString(KEY_EPISODE_LINK),
|
||||
metadata.getString(MediaMetadata.KEY_ARTIST),
|
||||
imageUrl,
|
||||
metadata.getString(KEY_FEED_WEBSITE),
|
||||
media.getContentType(),
|
||||
metadata.getDate(MediaMetadata.KEY_RELEASE_DATE).getTime(),
|
||||
notes);
|
||||
if (result.getDuration() == 0 && media.getStreamDuration() > 0) {
|
||||
result.setDuration((int) media.getStreamDuration());
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
/**
|
||||
* Compares a {@link MediaInfo} instance with a {@link FeedMedia} one and evaluates whether they
|
||||
* represent the same podcast episode.
|
||||
*
|
||||
* @param info the {@link MediaInfo} object to be compared.
|
||||
* @param media the {@link FeedMedia} object to be compared.
|
||||
* @return <true>true</true> if there's a match, <code>false</code> otherwise.
|
||||
*
|
||||
* @see RemoteMedia#equals(Object)
|
||||
*/
|
||||
public static boolean matches(MediaInfo info, FeedMedia media) {
|
||||
if (info == null || media == null) {
|
||||
return false;
|
||||
}
|
||||
if (!TextUtils.equals(info.getContentId(), media.getStreamUrl())) {
|
||||
return false;
|
||||
}
|
||||
MediaMetadata metadata = info.getMetadata();
|
||||
FeedItem fi = media.getItem();
|
||||
if (fi == null || metadata == null
|
||||
|| !TextUtils.equals(metadata.getString(KEY_EPISODE_IDENTIFIER), fi.getItemIdentifier())) {
|
||||
return false;
|
||||
}
|
||||
Feed feed = fi.getFeed();
|
||||
return feed != null && TextUtils.equals(metadata.getString(KEY_FEED_URL), feed.getDownload_url());
|
||||
}
|
||||
|
||||
/**
|
||||
* Compares a {@link MediaInfo} instance with a {@link RemoteMedia} one and evaluates whether they
|
||||
* represent the same podcast episode.
|
||||
*
|
||||
* @param info the {@link MediaInfo} object to be compared.
|
||||
* @param media the {@link RemoteMedia} object to be compared.
|
||||
* @return <true>true</true> if there's a match, <code>false</code> otherwise.
|
||||
*
|
||||
* @see RemoteMedia#equals(Object)
|
||||
*/
|
||||
public static boolean matches(MediaInfo info, RemoteMedia media) {
|
||||
if (info == null || media == null) {
|
||||
return false;
|
||||
}
|
||||
if (!TextUtils.equals(info.getContentId(), media.getStreamUrl())) {
|
||||
return false;
|
||||
}
|
||||
MediaMetadata metadata = info.getMetadata();
|
||||
return metadata != null
|
||||
&& TextUtils.equals(metadata.getString(KEY_EPISODE_IDENTIFIER), media.getEpisodeIdentifier())
|
||||
&& TextUtils.equals(metadata.getString(KEY_FEED_URL), media.getFeedUrl());
|
||||
}
|
||||
|
||||
/**
|
||||
* Compares a {@link MediaInfo} instance with a {@link Playable} and evaluates whether they
|
||||
* represent the same podcast episode. Useful every time we get a MediaInfo from the Cast Device
|
||||
* and want to avoid unnecessary conversions.
|
||||
*
|
||||
* @param info the {@link MediaInfo} object to be compared.
|
||||
* @param media the {@link Playable} object to be compared.
|
||||
* @return <true>true</true> if there's a match, <code>false</code> otherwise.
|
||||
*
|
||||
* @see RemoteMedia#equals(Object)
|
||||
*/
|
||||
public static boolean matches(MediaInfo info, Playable media) {
|
||||
if (info == null || media == null) {
|
||||
return false;
|
||||
}
|
||||
if (media instanceof RemoteMedia) {
|
||||
return matches(info, (RemoteMedia) media);
|
||||
}
|
||||
return media instanceof FeedMedia && matches(info, (FeedMedia) media);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,172 @@
|
|||
package ac.mdiq.podvinci.playback.cast
|
||||
|
||||
import ac.mdiq.podvinci.model.feed.Feed
|
||||
import ac.mdiq.podvinci.model.feed.FeedMedia
|
||||
import ac.mdiq.podvinci.model.playback.MediaType
|
||||
import ac.mdiq.podvinci.model.playback.Playable
|
||||
import ac.mdiq.podvinci.model.playback.RemoteMedia
|
||||
import android.content.ContentResolver
|
||||
import android.text.TextUtils
|
||||
import android.util.Log
|
||||
import com.google.android.gms.cast.CastDevice
|
||||
import com.google.android.gms.cast.MediaInfo
|
||||
import com.google.android.gms.cast.MediaMetadata
|
||||
import com.google.android.gms.cast.framework.CastSession
|
||||
|
||||
/**
|
||||
* Helper functions for Cast support.
|
||||
*/
|
||||
object CastUtils {
|
||||
private const val TAG = "CastUtils"
|
||||
|
||||
const val KEY_MEDIA_ID: String = "ac.mdiq.podvinci.core.cast.MediaId"
|
||||
|
||||
const val KEY_EPISODE_IDENTIFIER: String = "ac.mdiq.podvinci.core.cast.EpisodeId"
|
||||
const val KEY_EPISODE_LINK: String = "ac.mdiq.podvinci.core.cast.EpisodeLink"
|
||||
const val KEY_STREAM_URL: String = "ac.mdiq.podvinci.core.cast.StreamUrl"
|
||||
const val KEY_FEED_URL: String = "ac.mdiq.podvinci.core.cast.FeedUrl"
|
||||
const val KEY_FEED_WEBSITE: String = "ac.mdiq.podvinci.core.cast.FeedWebsite"
|
||||
const val KEY_EPISODE_NOTES: String = "ac.mdiq.podvinci.core.cast.EpisodeNotes"
|
||||
|
||||
/**
|
||||
* The field `PodVinci.FormatVersion` specifies which version of MediaMetaData
|
||||
* fields we're using. Future implementations should try to be backwards compatible with earlier
|
||||
* versions, and earlier versions should be forward compatible until the version indicated by
|
||||
* `MAX_VERSION_FORWARD_COMPATIBILITY`. If an update makes the format unreadable for
|
||||
* an earlier version, then its version number should be greater than the
|
||||
* `MAX_VERSION_FORWARD_COMPATIBILITY` value set on the earlier one, so that it
|
||||
* doesn't try to parse the object.
|
||||
*/
|
||||
const val KEY_FORMAT_VERSION: String = "ac.mdiq.podvinci.core.cast.FormatVersion"
|
||||
const val FORMAT_VERSION_VALUE: Int = 1
|
||||
const val MAX_VERSION_FORWARD_COMPATIBILITY: Int = 9999
|
||||
|
||||
fun isCastable(media: Playable?, castSession: CastSession?): Boolean {
|
||||
if (media == null || castSession == null || castSession.castDevice == null) {
|
||||
return false
|
||||
}
|
||||
if (media is FeedMedia || media is RemoteMedia) {
|
||||
val url = media.getStreamUrl()
|
||||
if (url.isNullOrEmpty()) {
|
||||
return false
|
||||
}
|
||||
if (url.startsWith(ContentResolver.SCHEME_CONTENT)) {
|
||||
return false // Local feed
|
||||
}
|
||||
return when (media.getMediaType()) {
|
||||
MediaType.AUDIO -> castSession.castDevice!!
|
||||
.hasCapability(CastDevice.CAPABILITY_AUDIO_OUT)
|
||||
MediaType.VIDEO -> castSession.castDevice!!.hasCapability(CastDevice.CAPABILITY_VIDEO_OUT)
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts [MediaInfo] objects into the appropriate implementation of [Playable].
|
||||
* @return [Playable] object in a format proper for casting.
|
||||
*/
|
||||
fun makeRemoteMedia(media: MediaInfo): Playable? {
|
||||
val metadata = media.metadata
|
||||
val version = metadata!!.getInt(KEY_FORMAT_VERSION)
|
||||
if (version <= 0 || version > MAX_VERSION_FORWARD_COMPATIBILITY) {
|
||||
Log.w(TAG, "MediaInfo object obtained from the cast device is not compatible with this"
|
||||
+ "version of PodVinci CastUtils, curVer=" + FORMAT_VERSION_VALUE
|
||||
+ ", object version=" + version)
|
||||
return null
|
||||
}
|
||||
val imageList = metadata.images
|
||||
var imageUrl: String? = null
|
||||
if (imageList.isNotEmpty()) {
|
||||
imageUrl = imageList[0].url.toString()
|
||||
}
|
||||
val notes = metadata.getString(KEY_EPISODE_NOTES)
|
||||
val result = RemoteMedia(media.contentId,
|
||||
metadata.getString(KEY_EPISODE_IDENTIFIER),
|
||||
metadata.getString(KEY_FEED_URL),
|
||||
metadata.getString(MediaMetadata.KEY_SUBTITLE),
|
||||
metadata.getString(MediaMetadata.KEY_TITLE),
|
||||
metadata.getString(KEY_EPISODE_LINK),
|
||||
metadata.getString(MediaMetadata.KEY_ARTIST),
|
||||
imageUrl,
|
||||
metadata.getString(KEY_FEED_WEBSITE),
|
||||
media.contentType,
|
||||
metadata.getDate(MediaMetadata.KEY_RELEASE_DATE)!!.time,
|
||||
notes)
|
||||
if (result.getDuration() == 0 && media.streamDuration > 0) {
|
||||
result.setDuration(media.streamDuration.toInt())
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Compares a [MediaInfo] instance with a [FeedMedia] one and evaluates whether they
|
||||
* represent the same podcast episode.
|
||||
*
|
||||
* @param info the [MediaInfo] object to be compared.
|
||||
* @param media the [FeedMedia] object to be compared.
|
||||
* @return <true>true</true> if there's a match, `false` otherwise.
|
||||
*
|
||||
* @see RemoteMedia.equals
|
||||
*/
|
||||
fun matches(info: MediaInfo?, media: FeedMedia?): Boolean {
|
||||
if (info == null || media == null) {
|
||||
return false
|
||||
}
|
||||
if (!TextUtils.equals(info.contentId, media.getStreamUrl())) {
|
||||
return false
|
||||
}
|
||||
val metadata = info.metadata
|
||||
val fi = media.getItem()
|
||||
if (fi == null || metadata == null || !TextUtils.equals(metadata.getString(KEY_EPISODE_IDENTIFIER), fi.itemIdentifier)) {
|
||||
return false
|
||||
}
|
||||
val feed: Feed? = fi.feed
|
||||
return feed != null && TextUtils.equals(metadata.getString(KEY_FEED_URL), feed.download_url)
|
||||
}
|
||||
|
||||
/**
|
||||
* Compares a [MediaInfo] instance with a [RemoteMedia] one and evaluates whether they
|
||||
* represent the same podcast episode.
|
||||
*
|
||||
* @param info the [MediaInfo] object to be compared.
|
||||
* @param media the [RemoteMedia] object to be compared.
|
||||
* @return <true>true</true> if there's a match, `false` otherwise.
|
||||
*
|
||||
* @see RemoteMedia.equals
|
||||
*/
|
||||
fun matches(info: MediaInfo?, media: RemoteMedia?): Boolean {
|
||||
if (info == null || media == null) {
|
||||
return false
|
||||
}
|
||||
if (!TextUtils.equals(info.contentId, media.getStreamUrl())) {
|
||||
return false
|
||||
}
|
||||
val metadata = info.metadata
|
||||
return (metadata != null && TextUtils.equals(metadata.getString(KEY_EPISODE_IDENTIFIER),
|
||||
media.getEpisodeIdentifier())
|
||||
&& TextUtils.equals(metadata.getString(KEY_FEED_URL), media.feedUrl))
|
||||
}
|
||||
|
||||
/**
|
||||
* Compares a [MediaInfo] instance with a [Playable] and evaluates whether they
|
||||
* represent the same podcast episode. Useful every time we get a MediaInfo from the Cast Device
|
||||
* and want to avoid unnecessary conversions.
|
||||
*
|
||||
* @param info the [MediaInfo] object to be compared.
|
||||
* @param media the [Playable] object to be compared.
|
||||
* @return <true>true</true> if there's a match, `false` otherwise.
|
||||
*
|
||||
* @see RemoteMedia.equals
|
||||
*/
|
||||
fun matches(info: MediaInfo?, media: Playable?): Boolean {
|
||||
if (info == null || media == null) {
|
||||
return false
|
||||
}
|
||||
if (media is RemoteMedia) {
|
||||
return matches(info, media as RemoteMedia?)
|
||||
}
|
||||
return media is FeedMedia && matches(info, media as FeedMedia?)
|
||||
}
|
||||
}
|
|
@ -1,135 +0,0 @@
|
|||
package ac.mdiq.podvinci.playback.cast;
|
||||
|
||||
import android.net.Uri;
|
||||
import android.text.TextUtils;
|
||||
import androidx.annotation.Nullable;
|
||||
import com.google.android.gms.cast.MediaInfo;
|
||||
import com.google.android.gms.cast.MediaMetadata;
|
||||
import com.google.android.gms.common.images.WebImage;
|
||||
import ac.mdiq.podvinci.model.feed.Feed;
|
||||
import ac.mdiq.podvinci.model.feed.FeedItem;
|
||||
import ac.mdiq.podvinci.model.feed.FeedMedia;
|
||||
import ac.mdiq.podvinci.model.playback.RemoteMedia;
|
||||
import java.util.Calendar;
|
||||
|
||||
public class MediaInfoCreator {
|
||||
public static MediaInfo from(RemoteMedia media) {
|
||||
MediaMetadata metadata = new MediaMetadata(MediaMetadata.MEDIA_TYPE_GENERIC);
|
||||
|
||||
metadata.putString(MediaMetadata.KEY_TITLE, media.getEpisodeTitle());
|
||||
metadata.putString(MediaMetadata.KEY_SUBTITLE, media.getFeedTitle());
|
||||
if (!TextUtils.isEmpty(media.getImageLocation())) {
|
||||
metadata.addImage(new WebImage(Uri.parse(media.getImageLocation())));
|
||||
}
|
||||
Calendar calendar = Calendar.getInstance();
|
||||
calendar.setTime(media.getPubDate());
|
||||
metadata.putDate(MediaMetadata.KEY_RELEASE_DATE, calendar);
|
||||
if (!TextUtils.isEmpty(media.getFeedAuthor())) {
|
||||
metadata.putString(MediaMetadata.KEY_ARTIST, media.getFeedAuthor());
|
||||
}
|
||||
if (!TextUtils.isEmpty(media.getFeedUrl())) {
|
||||
metadata.putString(CastUtils.KEY_FEED_URL, media.getFeedUrl());
|
||||
}
|
||||
if (!TextUtils.isEmpty(media.getFeedLink())) {
|
||||
metadata.putString(CastUtils.KEY_FEED_WEBSITE, media.getFeedLink());
|
||||
}
|
||||
if (!TextUtils.isEmpty(media.getEpisodeIdentifier())) {
|
||||
metadata.putString(CastUtils.KEY_EPISODE_IDENTIFIER, media.getEpisodeIdentifier());
|
||||
} else {
|
||||
metadata.putString(CastUtils.KEY_EPISODE_IDENTIFIER, media.getDownloadUrl());
|
||||
}
|
||||
if (!TextUtils.isEmpty(media.getEpisodeLink())) {
|
||||
metadata.putString(CastUtils.KEY_EPISODE_LINK, media.getEpisodeLink());
|
||||
}
|
||||
String notes = media.getNotes();
|
||||
if (notes != null) {
|
||||
metadata.putString(CastUtils.KEY_EPISODE_NOTES, notes);
|
||||
}
|
||||
// Default id value
|
||||
metadata.putInt(CastUtils.KEY_MEDIA_ID, 0);
|
||||
metadata.putInt(CastUtils.KEY_FORMAT_VERSION, CastUtils.FORMAT_VERSION_VALUE);
|
||||
metadata.putString(CastUtils.KEY_STREAM_URL, media.getStreamUrl());
|
||||
|
||||
MediaInfo.Builder builder = new MediaInfo.Builder(media.getDownloadUrl())
|
||||
.setContentType(media.getMimeType())
|
||||
.setStreamType(MediaInfo.STREAM_TYPE_BUFFERED)
|
||||
.setMetadata(metadata);
|
||||
if (media.getDuration() > 0) {
|
||||
builder.setStreamDuration(media.getDuration());
|
||||
}
|
||||
return builder.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts {@link FeedMedia} objects into a format suitable for sending to a Cast Device.
|
||||
* Before using this method, one should make sure isCastable(Playable) returns
|
||||
* {@code true}. This method should not run on the main thread.
|
||||
*
|
||||
* @param media The {@link FeedMedia} object to be converted.
|
||||
* @return {@link MediaInfo} object in a format proper for casting.
|
||||
*/
|
||||
public static MediaInfo from(FeedMedia media) {
|
||||
if (media == null) {
|
||||
return null;
|
||||
}
|
||||
MediaMetadata metadata = new MediaMetadata(MediaMetadata.MEDIA_TYPE_GENERIC);
|
||||
if (media.getItem() == null) {
|
||||
throw new IllegalStateException("item is null");
|
||||
}
|
||||
FeedItem feedItem = media.getItem();
|
||||
if (feedItem != null) {
|
||||
metadata.putString(MediaMetadata.KEY_TITLE, media.getEpisodeTitle());
|
||||
String subtitle = media.getFeedTitle();
|
||||
if (subtitle != null) {
|
||||
metadata.putString(MediaMetadata.KEY_SUBTITLE, subtitle);
|
||||
}
|
||||
|
||||
final @Nullable Feed feed = feedItem.getFeed();
|
||||
// Manual because cast does not support embedded images
|
||||
String url = (feedItem.getImageUrl() == null && feed != null) ? feed.getImageUrl() : feedItem.getImageUrl();
|
||||
if (!TextUtils.isEmpty(url)) {
|
||||
metadata.addImage(new WebImage(Uri.parse(url)));
|
||||
}
|
||||
Calendar calendar = Calendar.getInstance();
|
||||
calendar.setTime(media.getItem().getPubDate());
|
||||
metadata.putDate(MediaMetadata.KEY_RELEASE_DATE, calendar);
|
||||
if (feed != null) {
|
||||
if (!TextUtils.isEmpty(feed.getAuthor())) {
|
||||
metadata.putString(MediaMetadata.KEY_ARTIST, feed.getAuthor());
|
||||
}
|
||||
if (!TextUtils.isEmpty(feed.getDownload_url())) {
|
||||
metadata.putString(CastUtils.KEY_FEED_URL, feed.getDownload_url());
|
||||
}
|
||||
if (!TextUtils.isEmpty(feed.getLink())) {
|
||||
metadata.putString(CastUtils.KEY_FEED_WEBSITE, feed.getLink());
|
||||
}
|
||||
}
|
||||
if (!TextUtils.isEmpty(feedItem.getItemIdentifier())) {
|
||||
metadata.putString(CastUtils.KEY_EPISODE_IDENTIFIER, feedItem.getItemIdentifier());
|
||||
} else {
|
||||
metadata.putString(CastUtils.KEY_EPISODE_IDENTIFIER, media.getStreamUrl());
|
||||
}
|
||||
if (!TextUtils.isEmpty(feedItem.getLink())) {
|
||||
metadata.putString(CastUtils.KEY_EPISODE_LINK, feedItem.getLink());
|
||||
}
|
||||
}
|
||||
// This field only identifies the id on the device that has the original version.
|
||||
// Idea is to perhaps, on a first approach, check if the version on the local DB with the
|
||||
// same id matches the remote object, and if not then search for episode and feed identifiers.
|
||||
// This at least should make media recognition for a single device much quicker.
|
||||
metadata.putInt(CastUtils.KEY_MEDIA_ID, ((Long) media.getIdentifier()).intValue());
|
||||
// A way to identify different casting media formats in case we change it in the future and
|
||||
// senders with different versions share a casting device.
|
||||
metadata.putInt(CastUtils.KEY_FORMAT_VERSION, CastUtils.FORMAT_VERSION_VALUE);
|
||||
metadata.putString(CastUtils.KEY_STREAM_URL, media.getStreamUrl());
|
||||
|
||||
MediaInfo.Builder builder = new MediaInfo.Builder(media.getStreamUrl())
|
||||
.setContentType(media.getMime_type())
|
||||
.setStreamType(MediaInfo.STREAM_TYPE_BUFFERED)
|
||||
.setMetadata(metadata);
|
||||
if (media.getDuration() > 0) {
|
||||
builder.setStreamDuration(media.getDuration());
|
||||
}
|
||||
return builder.build();
|
||||
}
|
||||
}
|
|
@ -0,0 +1,130 @@
|
|||
package ac.mdiq.podvinci.playback.cast
|
||||
|
||||
import ac.mdiq.podvinci.model.feed.Feed
|
||||
import ac.mdiq.podvinci.model.feed.FeedMedia
|
||||
import ac.mdiq.podvinci.model.playback.RemoteMedia
|
||||
import android.net.Uri
|
||||
import android.text.TextUtils
|
||||
import com.google.android.gms.cast.MediaInfo
|
||||
import com.google.android.gms.cast.MediaMetadata
|
||||
import com.google.android.gms.common.images.WebImage
|
||||
import java.util.*
|
||||
|
||||
object MediaInfoCreator {
|
||||
fun from(media: RemoteMedia): MediaInfo {
|
||||
val metadata = MediaMetadata(MediaMetadata.MEDIA_TYPE_GENERIC)
|
||||
|
||||
metadata.putString(MediaMetadata.KEY_TITLE, media.getEpisodeTitle())
|
||||
metadata.putString(MediaMetadata.KEY_SUBTITLE, media.getFeedTitle())
|
||||
if (!TextUtils.isEmpty(media.getImageLocation())) {
|
||||
metadata.addImage(WebImage(Uri.parse(media.getImageLocation())))
|
||||
}
|
||||
val calendar = Calendar.getInstance()
|
||||
calendar.time = media.getPubDate()
|
||||
metadata.putDate(MediaMetadata.KEY_RELEASE_DATE, calendar)
|
||||
if (!TextUtils.isEmpty(media.getFeedAuthor())) {
|
||||
metadata.putString(MediaMetadata.KEY_ARTIST, media.getFeedAuthor())
|
||||
}
|
||||
if (!TextUtils.isEmpty(media.feedUrl)) {
|
||||
metadata.putString(CastUtils.KEY_FEED_URL, media.feedUrl!!)
|
||||
}
|
||||
if (!TextUtils.isEmpty(media.feedLink)) {
|
||||
metadata.putString(CastUtils.KEY_FEED_WEBSITE, media.feedLink!!)
|
||||
}
|
||||
if (!TextUtils.isEmpty(media.getEpisodeIdentifier())) {
|
||||
metadata.putString(CastUtils.KEY_EPISODE_IDENTIFIER, media.getEpisodeIdentifier()!!)
|
||||
} else {
|
||||
if (media.getStreamUrl() != null) metadata.putString(CastUtils.KEY_EPISODE_IDENTIFIER, media.getStreamUrl()!!)
|
||||
}
|
||||
if (!TextUtils.isEmpty(media.episodeLink)) {
|
||||
metadata.putString(CastUtils.KEY_EPISODE_LINK, media.episodeLink!!)
|
||||
}
|
||||
val notes: String? = media.getDescription()
|
||||
if (notes != null) {
|
||||
metadata.putString(CastUtils.KEY_EPISODE_NOTES, notes)
|
||||
}
|
||||
// Default id value
|
||||
metadata.putInt(CastUtils.KEY_MEDIA_ID, 0)
|
||||
metadata.putInt(CastUtils.KEY_FORMAT_VERSION, CastUtils.FORMAT_VERSION_VALUE)
|
||||
metadata.putString(CastUtils.KEY_STREAM_URL, media.getStreamUrl()!!)
|
||||
|
||||
val builder = MediaInfo.Builder(media.getStreamUrl()?:"")
|
||||
.setContentType(media.getMimeType())
|
||||
.setStreamType(MediaInfo.STREAM_TYPE_BUFFERED)
|
||||
.setMetadata(metadata)
|
||||
if (media.getDuration() > 0) {
|
||||
builder.setStreamDuration(media.getDuration().toLong())
|
||||
}
|
||||
return builder.build()
|
||||
}
|
||||
|
||||
/**
|
||||
* Converts [FeedMedia] objects into a format suitable for sending to a Cast Device.
|
||||
* Before using this method, one should make sure isCastable(Playable) returns
|
||||
* `true`. This method should not run on the main thread.
|
||||
*
|
||||
* @param media The [FeedMedia] object to be converted.
|
||||
* @return [MediaInfo] object in a format proper for casting.
|
||||
*/
|
||||
fun from(media: FeedMedia?): MediaInfo? {
|
||||
if (media == null) {
|
||||
return null
|
||||
}
|
||||
val metadata = MediaMetadata(MediaMetadata.MEDIA_TYPE_GENERIC)
|
||||
checkNotNull(media.getItem()) { "item is null" }
|
||||
val feedItem = media.getItem()
|
||||
if (feedItem != null) {
|
||||
metadata.putString(MediaMetadata.KEY_TITLE, media.getEpisodeTitle())
|
||||
val subtitle = media.getFeedTitle()
|
||||
metadata.putString(MediaMetadata.KEY_SUBTITLE, subtitle)
|
||||
|
||||
|
||||
val feed: Feed? = feedItem.feed
|
||||
// Manual because cast does not support embedded images
|
||||
val url: String = if (feedItem.imageUrl == null && feed != null) feed.imageUrl?:"" else feedItem.imageUrl?:""
|
||||
if (!TextUtils.isEmpty(url)) {
|
||||
metadata.addImage(WebImage(Uri.parse(url)))
|
||||
}
|
||||
val calendar = Calendar.getInstance()
|
||||
if (media.getItem()?.getPubDate() != null) calendar.time = media.getItem()!!.getPubDate()!!
|
||||
metadata.putDate(MediaMetadata.KEY_RELEASE_DATE, calendar)
|
||||
if (feed != null) {
|
||||
if (!feed.author.isNullOrEmpty()) {
|
||||
metadata.putString(MediaMetadata.KEY_ARTIST, feed.author!!)
|
||||
}
|
||||
if (!feed.download_url.isNullOrEmpty()) {
|
||||
metadata.putString(CastUtils.KEY_FEED_URL, feed.download_url!!)
|
||||
}
|
||||
if (!feed.link.isNullOrEmpty()) {
|
||||
metadata.putString(CastUtils.KEY_FEED_WEBSITE, feed.link!!)
|
||||
}
|
||||
}
|
||||
if (!feedItem.itemIdentifier.isNullOrEmpty()) {
|
||||
metadata.putString(CastUtils.KEY_EPISODE_IDENTIFIER, feedItem.itemIdentifier!!)
|
||||
} else {
|
||||
metadata.putString(CastUtils.KEY_EPISODE_IDENTIFIER, media.getStreamUrl()?:"")
|
||||
}
|
||||
if (!feedItem.link.isNullOrEmpty()) {
|
||||
metadata.putString(CastUtils.KEY_EPISODE_LINK, feedItem.link!!)
|
||||
}
|
||||
}
|
||||
// This field only identifies the id on the device that has the original version.
|
||||
// Idea is to perhaps, on a first approach, check if the version on the local DB with the
|
||||
// same id matches the remote object, and if not then search for episode and feed identifiers.
|
||||
// This at least should make media recognition for a single device much quicker.
|
||||
metadata.putInt(CastUtils.KEY_MEDIA_ID, (media.getIdentifier() as Long).toInt())
|
||||
// A way to identify different casting media formats in case we change it in the future and
|
||||
// senders with different versions share a casting device.
|
||||
metadata.putInt(CastUtils.KEY_FORMAT_VERSION, CastUtils.FORMAT_VERSION_VALUE)
|
||||
metadata.putString(CastUtils.KEY_STREAM_URL, media.getStreamUrl()!!)
|
||||
|
||||
val builder = MediaInfo.Builder(media.getStreamUrl()!!)
|
||||
.setContentType(media.mime_type)
|
||||
.setStreamType(MediaInfo.STREAM_TYPE_BUFFERED)
|
||||
.setMetadata(metadata)
|
||||
if (media.getDuration() > 0) {
|
||||
builder.setStreamDuration(media.getDuration().toLong())
|
||||
}
|
||||
return builder.build()
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue