Home Android & Kotlin Books Android Apprentice

25
Podcast Subscriptions, Part Two Written by Fuad Kamal

Heads up... You're reading this book for free, with parts of this chapter shown beyond this point as scrambled text.

You can unlock the rest of this book, and our entire catalogue of books and videos, with a raywenderlich.com Professional subscription.

Now that the user can subscribe to podcasts, it’s helpful to notify them when new episodes are available. In this chapter, you’ll update the app to periodically check for new episodes in the background and post a notification if any are found.

Getting started

If you’re following along with your own project, the starter project for this chapter includes an additional icon that you’ll need to complete the section. Open your project and then copy the following resources from the provided starter project into yours:

  • src/main/res/drawable-hdpi/ic_episode_icon.png
  • src/main/res/drawable-mdpi/ic_episode_icon.png
  • src/main/res/drawable-xhdpi/ic_episode_icon.png
  • src/main/res/drawable-xxhdpi/ic_episode_icon.png

When you’re done, the res\drawable folder in Android Studio will look like this:

If you don’t have your own project, don’t worry. Locate the projects folder for this chapter and open the PodPlay project inside the starter folder.

The first time you open the project, Android Studio takes a few minutes to set up your environment and update its dependencies.

Background methods

Checking for new episodes should happen automatically at regular intervals whether the app is running or not. There are several methods available for an application to perform tasks when it’s not running. It’s important to choose the correct one so that it doesn’t affect the performance of other running applications.

Alarms

You can use AlarmManager to wake up the app at a specified time so it can perform operations. An Intent is sent to the application to wake it up, and then it can perform the work.

Broadcasts

You can register to receive broadcasts from the system for certain events and then perform tasks. This option is highly restricted to a limited number of broadcasts in apps that target API level 26 or higher.

Services

Android provides foreground and background services.

Scheduled jobs

This is the approach Google recommends for most background operations. You can specify detailed criteria about when the job will run. Android intelligently determines the best time and takes advantage of system idle time.

WorkManager

WorkManager provides a way to schedule background tasks that are considered deferrable. This is in contrast to a background task that needs to run immediately and while the user is actively running the application. It also guarantees that the task will run even if the app is closed or the device is rebooted.

Episode update logic

To keep with the current architecture of using the repo for updating podcast data, you need to add a new method in the repo to handle the episode update logic.

@Query("SELECT * FROM Podcast ORDER BY FeedTitle")
fun loadPodcastsStatic(): List<Podcast>
private suspend fun getNewEpisodes(localPodcast: Podcast): List<Episode> {
  // 1
  val response = feedService.getFeed(localPodcast.feedUrl)
  if (response != null) {
    // 2
    val remotePodcast = rssResponseToPodcast(localPodcast.feedUrl, localPodcast.imageUrl, response)
    remotePodcast?.let {
      // 3
      val localEpisodes = podcastDao.loadEpisodes(localPodcast.id!!)
      // 4
      return remotePodcast.episodes.filter { episode ->
        localEpisodes.find { episode.guid == it.guid } == null
      }
  }
  }
  // 5
  return listOf()
}
private fun saveNewEpisodes(podcastId: Long, episodes: List<Episode>) {
  GlobalScope.launch {
    for (episode in episodes) {
      episode.podcastId = podcastId
      podcastDao.insertEpisode(episode)
    }
  }
}
class PodcastUpdateInfo(
    val feedUrl: String, 
    val name: String, 
    val newCount: Int
)
suspend fun updatePodcastEpisodes() : MutableList<PodcastUpdateInfo> {
  // 1
  val updatedPodcasts: MutableList<PodcastUpdateInfo> = mutableListOf()
  // 2
  val podcasts = podcastDao.loadPodcastsStatic()
    // 3
    for (podcast in podcasts) {
      // 4
      val newEpisodes = getNewEpisodes(podcast)
      // 5
      if (newEpisodes.count() > 0) {
        podcast.id?.let {
          saveNewEpisodes(it, newEpisodes)
          updatedPodcasts.add(PodcastUpdateInfo(
                    podcast.feedUrl, podcast.feedTitle, newEpisodes.count()))
        }
      }
    }
    // 6
    return updatedPodcasts
}

WorkManager

Now that all of the support code is in place to update podcast episodes, you can turn your attention back to job scheduling.

Worker

You must add the WorkManager library to the project first.

implementation "androidx.work:work-runtime-ktx:2.5.0"
class EpisodeUpdateWorker(context: Context, params: WorkerParameters) : CoroutineWorker(context, params) {

  override suspend fun doWork(): Result = coroutineScope {
  Result.success()
  }
}

Notifications

Notifications are Android’s way of letting you display information outside of your application. The notifications appear as icons in the notification display area at the top of the screen as shown here:

companion object {
  const val EPISODE_CHANNEL_ID = "podplay_episodes_channel"
}
// 1
@RequiresApi(Build.VERSION_CODES.O)
private fun createNotificationChannel() {
  // 2
  val notificationManager = applicationContext
    .getSystemService(NOTIFICATION_SERVICE) as NotificationManager
  // 3
  if (notificationManager.getNotificationChannel(EPISODE_CHANNEL_ID) == null) {
    // 4
    val channel = NotificationChannel(EPISODE_CHANNEL_ID,
        "Episodes", NotificationManager.IMPORTANCE_DEFAULT)
    notificationManager.createNotificationChannel(channel)
  }
}
import android.content.Context.NOTIFICATION_SERVICE
<string name="episode_notification_title">New episodes</string>
<string name="episode_notification_text">%1$d new episode(s) for %2$s</string>
const val EXTRA_FEED_URL = "PodcastFeedUrl"
private fun displayNotification(podcastInfo: PodcastRepo.PodcastUpdateInfo) {
  // 1
  val contentIntent = Intent(applicationContext, PodcastActivity::class.java)  
  contentIntent.putExtra(EXTRA_FEED_URL, podcastInfo.feedUrl)
  val pendingContentIntent = 
      PendingIntent.getActivity(applicationContext, 0, 
        contentIntent, PendingIntent.FLAG_UPDATE_CURRENT)
  // 2
  val notification = 
      NotificationCompat
      .Builder(applicationContext, EPISODE_CHANNEL_ID)
        .setSmallIcon(R.drawable.ic_episode_icon)
        .setContentTitle(applicationContext.getString(
          R.string.episode_notification_title))
        .setContentText(applicationContext.getString(
          R.string.episode_notification_text,
          podcastInfo.newCount, podcastInfo.name))
        .setNumber(podcastInfo.newCount)
        .setAutoCancel(true)
        .setContentIntent(pendingContentIntent)
        .build()
  // 3
  val notificationManager = applicationContext
    .getSystemService(NOTIFICATION_SERVICE) as NotificationManager
  // 4
  notificationManager.notify(podcastInfo.name, 0, notification)
}
// 1
override suspend fun doWork(): Result = coroutineScope {
    // 2
    val job = async {
        // 3
        val db = PodPlayDatabase.getInstance(applicationContext, this)
        val repo = PodcastRepo(RssFeedService.instance, db.podcastDao())
        // 4
        val podcastUpdates = repo.updatePodcastEpisodes()
        // 5
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
            createNotificationChannel()
        }
        // 6
        for (podcastUpdate in podcastUpdates) {
            displayNotification(podcastUpdate)
        }
    }
    // 7
    job.await()
    // 8
    Result.success()
}

WorkManager scheduling

Now that EpisodeUpdateWorker is updating podcast episodes and notifying the user, you’ll finish up by using WorkManager to schedule the EpisodeUpdateWorker.

private const val TAG_EPISODE_UPDATE_JOB = 
   "com.raywenderlich.podplay.episodes"
private fun scheduleJobs() {
  // 1
  val constraints: Constraints = Constraints.Builder().apply {
    setRequiredNetworkType(NetworkType.CONNECTED)
    setRequiresCharging(true)
  }.build()
  // 2
  val request = PeriodicWorkRequestBuilder<EpisodeUpdateWorker>(
          1, TimeUnit.HOURS)
          .setConstraints(constraints)
          .build()
  // 3
  WorkManager.getInstance(this).enqueueUniquePeriodicWork(
     TAG_EPISODE_UPDATE_JOB,
     ExistingPeriodicWorkPolicy.REPLACE, request)
}
scheduleJobs()

Notification Intent

At this point, the episode worker runs, and the notifications work. If the user taps the notification, it activates the PodcastActivity. The only thing left is to handle the notification intent and use it to display the podcast details.

suspend fun setActivePodcast(feedUrl: String): PodcastSummaryViewData? {
  val repo = podcastRepo ?: return null
  val podcast = repo.getPodcast(feedUrl)
  if (podcast == null) {
    return null
  } else {
    _podcastLiveData.value = podcastToPodcastView(podcast)
    activePodcast = podcast
    return podcastToSummaryView(podcast)
  }
}
val podcastFeedUrl = intent.getStringExtra(EpisodeUpdateWorker.EXTRA_FEED_URL)
if (podcastFeedUrl != null) {
  podcastViewModel.viewModelScope.launch {
      val podcastSummaryViewData = podcastViewModel.setActivePodcast(podcastFeedUrl)
      podcastSummaryViewData?.let { podcastSummaryView -> onShowDetails(podcastSummaryView) }
    }
}
it.episodes = it.episodes.drop(1)

Key Points

  • JobScheduler is the recommended approach for adding background operations to your app.
  • WorkManager is part of Android Jetpack. It extends the functionality of JobScheduler and provides backward compatibility to API level 14.
  • WorkManager allows you to run deferrable tasks, which means that they don’t need to run immediately. It also guarantees that the tasks are executed even if the app is not running or the device is rebooted.
  • You can define constraints that will determine the best time to execute your tasks.
  • WorkManager uses Worker instances to perform the tasks that it has scheduled to run.

Where to go from here?

After testing, don’t forget to remove the temporary code you added to drop the first podcast when subscribing, and put back in the original repeat interval time.

Have a technical question? Want to report a bug? You can ask questions and report bugs to the book authors in our official book forum here.

Have feedback to share about the online reading experience? If you have feedback about the UI, UX, highlighting, or other features of our online readers, you can send them to the design team with the form below:

© 2021 Razeware LLC

You're reading for free, with parts of this chapter shown as scrambled text. Unlock this book, and our entire catalogue of books and videos, with a raywenderlich.com Professional subscription.

Unlock Now

To highlight or take notes, you’ll need to own this book in a subscription or purchased by itself.