properly setup NotificationRecommendations

This commit is contained in:
Felix 2020-04-12 22:37:51 +02:00
parent 2077e1ed54
commit 158d4b129b
14 changed files with 177 additions and 179 deletions

View file

@ -18,7 +18,7 @@ class ChaosflixPreferenceManager(private val sharedPref: SharedPreferences) {
var autoselectStream: Boolean by BooleanPreferencesDelegate(keyAutoselectStream, false)
var recommendationsGenerated: Boolean by BooleanPreferencesDelegate(RECOMMENDATIONS_GENERATED, false)
var recommendationsEnabled: Boolean by BooleanPreferencesDelegate(RECOMMENDATIONS_ENABLED, true)
private inner class BooleanPreferencesDelegate(key: String, default: Boolean) :
PreferencesDelegate<Boolean>(key, default, SharedPreferences::getBoolean, SharedPreferences.Editor::putBoolean)
@ -55,6 +55,6 @@ class ChaosflixPreferenceManager(private val sharedPref: SharedPreferences) {
private const val keyAnalyticsDisabled = "disable_analytics"
private const val keyDownloadFolder = "download_folder"
private const val CHANNEL_ID = "channelId"
private const val RECOMMENDATIONS_GENERATED = "recommendation_generated"
private const val RECOMMENDATIONS_ENABLED = "recommendation_enabled"
}
}

View file

@ -17,18 +17,20 @@ import de.nicidienase.chaosflix.common.mediadata.entities.recording.persistence.
import de.nicidienase.chaosflix.common.mediadata.entities.recording.persistence.RelatedEvent
import de.nicidienase.chaosflix.common.mediadata.entities.recording.persistence.RelatedEventDao
import de.nicidienase.chaosflix.common.mediadata.network.RecordingService
import de.nicidienase.chaosflix.common.userdata.entities.progress.PlaybackProgressDao
import de.nicidienase.chaosflix.common.userdata.entities.progress.ProgressEventView
import de.nicidienase.chaosflix.common.userdata.entities.watchlist.WatchlistItem
import de.nicidienase.chaosflix.common.userdata.entities.watchlist.WatchlistItemDao
import de.nicidienase.chaosflix.common.util.ConferenceUtil
import de.nicidienase.chaosflix.common.util.LiveEvent
import de.nicidienase.chaosflix.common.util.SingleLiveEvent
import java.io.IOException
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.launch
import kotlinx.coroutines.withContext
import retrofit2.Response
import java.io.IOException
class MediaRepository(
private val recordingApi: RecordingService,
@ -44,6 +46,7 @@ class MediaRepository(
private val recordingDao: RecordingDao by lazy { database.recordingDao() }
private val relatedEventDao: RelatedEventDao by lazy { database.relatedEventDao() }
private val watchlistItemDao: WatchlistItemDao by lazy { database.watchlistItemDao() }
private val playbackProgressDao: PlaybackProgressDao by lazy { database.playbackProgressDao() }
suspend fun getEventSync(eventId: Long) = eventDao.findEventByIdSync(eventId)
@ -319,11 +322,18 @@ class MediaRepository(
return recordingDao.findRecordingByEvent(eventId)
}
suspend fun getRecommendations(): MutableList<Event> {
return mutableListOf<Event>().apply {
this.addAll(database.eventDao().findBookmarkedEventsSync())
this.addAll(database.eventDao().findPromotedEventsSync())
}
suspend fun getEventsInProgress(): List<ProgressEventView> {
val progress = playbackProgressDao.getAllWithEventSync()
progress.forEach { it.event?.progress = it.progress.progress }
return progress
}
suspend fun getBookmarkedEvents(): List<Event> = eventDao.findBookmarkedEventsSync()
suspend fun getHomescreenRecommendations(): List<Event> {
val bookmarks = eventDao.findBookmarkedEventsSync()
val promoted = eventDao.findPromotedEventsSync()
return listOf(bookmarks, promoted).flatten()
}
data class SearchResponse(val events: List<Event>, val total: Int, val links: Map<String, String>) {

View file

@ -52,46 +52,7 @@ data class EventDto(
}
}
fun getExtendedDescription(): String = "$description\n\nreleased at: $releaseDate\n\nTags: ${tags?.joinToString(", ")}"
fun getSpeakerString(): String? = persons?.joinToString(", ")
override fun compareTo(other: EventDto): Int {
return slug.compareTo(other.slug)
}
override fun equals(other: Any?): Boolean {
return if (other is EventDto) {
guid == other.guid
} else {
super.equals(other)
}
}
override fun hashCode(): Int {
var result = guid.hashCode()
result = 31 * result + title.hashCode()
result = 31 * result + (subtitle?.hashCode() ?: 0)
result = 31 * result + slug.hashCode()
result = 31 * result + (link?.hashCode() ?: 0)
result = 31 * result + (description?.hashCode() ?: 0)
result = 31 * result + originalLanguage.hashCode()
result = 31 * result + (persons?.contentHashCode() ?: 0)
result = 31 * result + (tags?.contentHashCode() ?: 0)
result = 31 * result + (date?.hashCode() ?: 0)
result = 31 * result + releaseDate.hashCode()
result = 31 * result + updatedAt.hashCode()
result = 31 * result + length.hashCode()
result = 31 * result + thumbUrl.hashCode()
result = 31 * result + posterUrl.hashCode()
result = 31 * result + (frontendLink?.hashCode() ?: 0)
result = 31 * result + url.hashCode()
result = 31 * result + conferenceUrl.hashCode()
result = 31 * result + (recordings?.hashCode() ?: 0)
result = 31 * result + (related?.hashCode() ?: 0)
result = 31 * result + isPromoted.hashCode()
result = 31 * result + eventID.hashCode()
result = 31 * result + viewCount
return result
}
}

View file

@ -22,6 +22,9 @@ interface PlaybackProgressDao {
@Query("SELECT * FROM playback_progress ORDER BY watch_date DESC")
fun getAllWithEvent(): LiveData<List<ProgressEventView>>
@Query("SELECT * FROM playback_progress ORDER BY watch_date DESC")
suspend fun getAllWithEventSync(): List<ProgressEventView>
@Query("SELECT * FROM playback_progress")
fun getAllSync(): List<PlaybackProgress>

View file

@ -52,9 +52,18 @@
android:name=".settings.ChaosflixSettingsActivity"
android:theme="@style/ChaosflixLeanbackPreferences" />
<service android:name=".ChaosRecommendationsService"
<service android:name=".recommendations.ChaosRecommendationsService"
android:enabled="true"/>
<receiver
android:name=".recommendations.RecommendationBroadcastReceiver"
android:enabled="true"
android:exported="false">
<intent-filter>
<action android:name="android.intent.action.BOOT_COMPLETED" />
</intent-filter>
</receiver>
</application>
<uses-feature

View file

@ -1,96 +0,0 @@
package de.nicidienase.chaosflix.leanback
import android.app.IntentService
import android.app.NotificationManager
import android.content.Context
import android.content.Intent
import android.graphics.Bitmap
import android.preference.PreferenceManager
import android.util.Log
import androidx.recommendation.app.ContentRecommendation
import com.bumptech.glide.Glide
import de.nicidienase.chaosflix.common.ChaosflixPreferenceManager
import de.nicidienase.chaosflix.common.mediadata.entities.recording.persistence.Event
import de.nicidienase.chaosflix.common.viewmodel.ViewModelFactory
import de.nicidienase.chaosflix.leanback.detail.DetailsActivity
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import java.io.IOException
class ChaosRecommendationsService : IntentService("ChaosRecommendationService") {
private val ioScope = CoroutineScope(Dispatchers.IO + Job())
override fun onHandleIntent(intent: Intent?) {
Log.d(TAG, "Updating Recommendation")
val mediaRepository = ViewModelFactory.getInstance(this).mediaRepository
val preferenceManager = ChaosflixPreferenceManager(PreferenceManager.getDefaultSharedPreferences(applicationContext))
// if(preferenceManager.recommendationsGenerated){
// Log.d(TAG, "already generated, returning")
// Toast.makeText(this, "already generated, returning", Toast.LENGTH_SHORT).show()
// return
// }
ioScope.launch {
val recommendations = mediaRepository.getRecommendations()
var count = 0
val cardWidth: Int = resources.getDimensionPixelSize(R.dimen.conference_card_width)
val cardHeight: Int = resources.getDimensionPixelSize(R.dimen.conference_card_height)
try {
// val builder = RecommendationBuilder()
val builder = ContentRecommendation.Builder().setBadgeIcon(R.drawable.chaosflix_icon)
val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as? NotificationManager
for (event in recommendations) {
Log.d(TAG, "Recommendation - " + event.title)
val bitmap: Bitmap = Glide.with(getApplication())
.asBitmap()
.load(event.thumbUrl)
.submit(cardWidth, cardHeight) // Only use for synchronous .get()
.get()
val id = event.id.toInt()
val contentRecommendation = builder.setIdTag("Event-$id")
.setTitle(event.title)
.setText(event.subtitle)
.setContentImage(bitmap)
.setContentIntentData(ContentRecommendation.INTENT_TYPE_ACTIVITY, buildPendingIntent(event), 0, null)
.build()
val notification = contentRecommendation.getNotificationObject(applicationContext)
notificationManager?.notify(id, notification)
Log.d(TAG, "Added notification for ${event.title}")
if (++ count >= MAX_RECOMMENDATIONS) {
break
}
}
} catch (e: IOException) {
Log.e(TAG, "Unable to update recommendation", e)
}
Log.d(TAG, "done generating recommendations")
preferenceManager.recommendationsGenerated = false
}
}
private fun buildPendingIntent(event: Event): Intent {
val detailsIntent = Intent(this, DetailsActivity::class.java).apply {
putExtra(DetailsActivity.EVENT, event)
}
return detailsIntent
// val builder = TaskStackBuilder.create(this)
// .addParentStack(ConferencesActivity::class.java)
// .addNextIntent(detailsIntent)
// detailsIntent.action = event.guid
// return builder.getPendingIntent(0, PendingIntent.FLAG_UPDATE_CURRENT)
}
companion object {
private val TAG = ChaosRecommendationsService::class.java.simpleName
private const val MAX_RECOMMENDATIONS = 6
}
}

View file

@ -18,6 +18,7 @@ import de.nicidienase.chaosflix.leanback.detail.DetailsActivity
import de.nicidienase.chaosflix.leanback.detail.DetailsActivity.Companion.start
import de.nicidienase.chaosflix.leanback.events.EventsActivity
import de.nicidienase.chaosflix.leanback.events.EventsActivity.Companion.start
import de.nicidienase.chaosflix.leanback.recommendations.ChaosRecommendationsService
import de.nicidienase.chaosflix.leanback.settings.ChaosflixSettingsActivity
class ItemViewClickedListener(private val fragment: Fragment, private val streamUpdater: (() -> Unit)? = null) : OnItemViewClickedListener {

View file

@ -6,6 +6,6 @@ enum class SelectableContentItem(val title: String, @DrawableRes val icon: Int,
Settings("Settings", R.drawable.ic_settings),
About("About", R.drawable.ic_info),
LeakCanary("Leak Canary", R.drawable.ic_warning),
AddRecommendations("Recommendations", R.drawable.ic_refresh),
AddRecommendations("Update Recommendations", R.drawable.ic_refresh),
UpdateStreams("Update Streams", R.drawable.ic_refresh)
}

View file

@ -1,15 +1,7 @@
package de.nicidienase.chaosflix.leanback.conferences
import android.os.Bundle
import androidx.lifecycle.ViewModelProvider
import androidx.lifecycle.lifecycleScope
import androidx.preference.PreferenceManager
import de.nicidienase.chaosflix.common.ChaosflixPreferenceManager
import de.nicidienase.chaosflix.common.viewmodel.BrowseViewModel
import de.nicidienase.chaosflix.common.viewmodel.ViewModelFactory
import de.nicidienase.chaosflix.leanback.ChannelManager
import de.nicidienase.chaosflix.leanback.R
import kotlinx.coroutines.launch
class ConferencesActivity : androidx.fragment.app.FragmentActivity() {
@ -18,26 +10,6 @@ class ConferencesActivity : androidx.fragment.app.FragmentActivity() {
setContentView(R.layout.activity_conferences_browse)
}
override fun onStart() {
super.onStart()
val prefs = ChaosflixPreferenceManager(PreferenceManager.getDefaultSharedPreferences(applicationContext))
val viewmodel = ViewModelProvider(
this, ViewModelFactory.getInstance(this)).get(BrowseViewModel::class.java)
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
lifecycleScope.launch {
ChannelManager.setupChannels(this@ConferencesActivity, viewmodel, prefs)
}
} else {
setupRecommendations()
}
}
private fun setupRecommendations() {
// startService(Intent(this, ChaosRecommendationsService::class.java))
}
companion object {
private val TAG = ConferencesActivity::class.java.simpleName
}

View file

@ -1,4 +1,4 @@
package de.nicidienase.chaosflix.leanback
package de.nicidienase.chaosflix.leanback.recommendations
import android.content.ContentUris
import android.content.Context
@ -10,7 +10,8 @@ import androidx.tvprovider.media.tv.ChannelLogoUtils
import androidx.tvprovider.media.tv.PreviewProgram
import androidx.tvprovider.media.tv.TvContractCompat
import de.nicidienase.chaosflix.common.ChaosflixPreferenceManager
import de.nicidienase.chaosflix.common.viewmodel.BrowseViewModel
import de.nicidienase.chaosflix.common.mediadata.MediaRepository
import de.nicidienase.chaosflix.leanback.R
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.withContext
@ -20,7 +21,7 @@ object ChannelManager {
PROMOTED,
}
suspend fun setupChannels(context: Context, viewmodel: BrowseViewModel, prefs: ChaosflixPreferenceManager) {
suspend fun setupChannels(context: Context, mediaRepository: MediaRepository, prefs: ChaosflixPreferenceManager) {
withContext(Dispatchers.IO) {
if (prefs.channelId == 0L) {
val builder = Channel.Builder()
@ -42,10 +43,9 @@ object ChannelManager {
}
}
val promotedEvents = viewmodel.getPromoted()
val bookmarkedEvents = viewmodel.getBookmarks()
val recommendations = mediaRepository.getHomescreenRecommendations()
val programmIds = promotedEvents.toMutableList().apply { addAll(bookmarkedEvents) }.map {
val programmIds = recommendations.map {
val toContentValues = PreviewProgram.Builder()
.setChannelId(prefs.channelId)
.setType(TvContractCompat.PreviewPrograms.TYPE_EVENT)

View file

@ -0,0 +1,103 @@
package de.nicidienase.chaosflix.leanback.recommendations
import android.app.IntentService
import android.app.NotificationManager
import android.content.Context
import android.content.Intent
import android.graphics.Bitmap
import android.preference.PreferenceManager
import android.util.Log
import androidx.recommendation.app.ContentRecommendation
import com.bumptech.glide.Glide
import de.nicidienase.chaosflix.common.ChaosflixPreferenceManager
import de.nicidienase.chaosflix.common.mediadata.MediaRepository
import de.nicidienase.chaosflix.common.mediadata.entities.recording.persistence.Event
import de.nicidienase.chaosflix.common.viewmodel.ViewModelFactory
import de.nicidienase.chaosflix.leanback.R
import de.nicidienase.chaosflix.leanback.detail.DetailsActivity
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import java.io.IOException
class ChaosRecommendationsService : IntentService("ChaosRecommendationService") {
private val ioScope = CoroutineScope(Dispatchers.IO + Job())
override fun onHandleIntent(intent: Intent?) {
val preferenceManager = ChaosflixPreferenceManager(PreferenceManager.getDefaultSharedPreferences(applicationContext))
val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as? NotificationManager
val mediaRepository = ViewModelFactory.getInstance(this).mediaRepository
if(!preferenceManager.recommendationsEnabled){
Log.i(TAG, "recommendations are disabled, returning")
notificationManager?.cancelAll()
return
}
ioScope.launch {
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
Log.i(TAG, "updating recommendation channels")
ChannelManager.setupChannels(this@ChaosRecommendationsService, mediaRepository, preferenceManager)
} else {
Log.i(TAG, "updating recommendation notifications")
notificationManager?.let { setupRecommendationNotifications(mediaRepository, it) }
}
}
}
private suspend fun setupRecommendationNotifications(mediaRepository: MediaRepository, notificationManager: NotificationManager) {
val recommendations = mediaRepository.getHomescreenRecommendations()
val cardWidth: Int = resources.getDimensionPixelSize(R.dimen.recommendation_thumb_width)
val cardHeight: Int = resources.getDimensionPixelSize(R.dimen.recommendation_thumb_height)
try {
val builder = ContentRecommendation.Builder().setBadgeIcon(R.drawable.chaosflix_icon)
for (event in recommendations) {
Log.d(TAG, "Recommendation - " + event.title)
val bitmap: Bitmap = Glide.with(application)
.asBitmap()
.load(event.thumbUrl)
.submit(cardWidth, cardHeight)
.get()
val id = event.id.toInt()
val contentRecommendation = builder.setIdTag("Event-$id")
.setTitle(event.title)
.setText(event.subtitle)
.setContentImage(bitmap)
.setContentIntentData(ContentRecommendation.INTENT_TYPE_ACTIVITY, buildPendingIntent(event), 0, null)
.setRunningTime(event.length)
.setContentTypes(arrayOf(ContentRecommendation.CONTENT_TYPE_VIDEO))
// .setBackgroundImageUri(event.posterUrl) // not supported on fireTV
.build()
val notification = contentRecommendation.getNotificationObject(applicationContext)
notification.extras.putString("com.amazon.extra.DISPLAY_NAME", "Chaosflix")
notification.extras.putString("com.amazon.extra.PREVIEW_URL", event.posterUrl)
notification.extras.putString("com.amazon.extra.LONG_DESCRIPTION", event.description)
notification.extras.putInt("com.amazon.extra.LIVE_CONTENT", 0)
// notification.extras.putInt("com.amazon.extra.CONTENT_CUSTOMER_RATING", 9)
notification.extras.putInt("com.amazon.extra.CONTENT_CUSTOMER_RATING_COUNT", event.viewCount)
notification.extras.putIntArray("com.amazon.extra.ACTION_OPTION", intArrayOf(2))
notificationManager.notify(id, notification)
Log.d(TAG, "Added notification for ${event.title}")
}
} catch (e: IOException) {
Log.e(TAG, "Unable to update recommendation", e)
}
Log.d(TAG, "done generating recommendations")
}
private fun buildPendingIntent(event: Event): Intent {
return Intent(this, DetailsActivity::class.java).apply {
putExtra(DetailsActivity.EVENT, event)
}
}
companion object {
private val TAG = ChaosRecommendationsService::class.java.simpleName
}
}

View file

@ -0,0 +1,27 @@
package de.nicidienase.chaosflix.leanback.recommendations
import android.app.AlarmManager
import android.app.PendingIntent
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
class RecommendationBroadcastReceiver: BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
if (intent?.action == Intent.ACTION_BOOT_COMPLETED) {
setupRecommendationUpdates(context)
}
}
private fun setupRecommendationUpdates(context: Context?) {
val alarmManager = context?.getSystemService(Context.ALARM_SERVICE) as? AlarmManager
val intent = Intent(context, ChaosRecommendationsService::class.java)
val pendingIntent = PendingIntent.getService(context, 0, intent, 0)
alarmManager?.setInexactRepeating(AlarmManager.ELAPSED_REALTIME, DELAY, AlarmManager.INTERVAL_HALF_HOUR, pendingIntent)
}
companion object {
private const val DELAY = 5_000L
}
}

View file

@ -9,4 +9,7 @@
<dimen name="spinner_width">120dp</dimen>
<dimen name="spinner_height">120dp</dimen>
<dimen name="settings_icon_size">96dp</dimen>
<dimen name="recommendation_thumb_width">240dp</dimen>
<dimen name="recommendation_thumb_height">135dp</dimen>
</resources>

View file

@ -12,6 +12,11 @@
android:summary="@string/pref_autoselect_recording"
android:title="@string/settings_choose_recording"/>
<SwitchPreference
android:defaultValue="true"
android:key="recommendation_enabled"
android:title="Homescreen recommendations"/>
<SwitchPreference
android:defaultValue="false"
android:key="auto_external_player"