Chapters

Hide chapters

Android Apprentice

Third Edition · Android 10 · Kotlin 1.3 · Android Studio 3.6

Before You Begin

Section 0: 4 chapters
Show chapters Hide chapters

Section II: Building a List App

Section 2: 7 chapters
Show chapters Hide chapters

Section III: Creating Map-Based Apps

Section 3: 7 chapters
Show chapters Hide chapters

25. Podcast Subscriptions, Part Two
Written by Tom Blankenship

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Heads up... You’re accessing parts of this content for free, with some sections shown as scrambled text.

Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now

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 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 tasks 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 fun getNewEpisodes(localPodcast: Podcast, callBack: (List<Episode>) -> Unit) {
// 1
    feedService.getFeed(localPodcast.feedUrl) { response ->
      if (response != null) {
// 2
        val remotePodcast = rssResponseToPodcast(localPodcast.feedUrl, localPodcast.imageUrl, response)
        remotePodcast?.let {
// 3
          val localEpisodes = podcastDao.loadEpisodes(localPodcast.id!!)
// 4
          val newEpisodes = remotePodcast.episodes.filter { episode ->
            localEpisodes.find {
              episode.guid == it.guid
            } == null
          }
// 5
          callBack(newEpisodes)
        }
      } else {
        callBack(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)
fun updatePodcastEpisodes(callback: (List<PodcastUpdateInfo>) -> Unit) {
// 1
  val updatedPodcasts: MutableList<PodcastUpdateInfo> = mutableListOf()
// 2
  val podcasts = podcastDao.loadPodcastsStatic()
// 3
  var processCount = podcasts.count()
// 4
  for (podcast in podcasts) {
// 5
    getNewEpisodes(podcast) { newEpisodes ->
// 6
        if (newEpisodes.count() > 0) {
            saveNewEpisodes(podcast.id!!, newEpisodes)
            updatedPodcasts.add(PodcastUpdateInfo(podcast.feedUrl, podcast.feedTitle, newEpisodes.count()))
        }
// 7
        processCount--
        if (processCount == 0) {
// 8
            callback(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.3.4"
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()
  // 4
  val notificationManager = 
      applicationContext.getSystemService(NOTIFICATION_SERVICE)
        as NotificationManager
  // 5
  notificationManager.notify(podcastInfo.name, 0, notification)
}
  // 1
override suspend fun doWork(): Result = coroutineScope {
  // 2
  val job = async {
    // 3
    val db = PodPlayDatabase.getInstance(applicationContext)
    val repo = PodcastRepo(FeedService.instance,
        db.podcastDao())
    // 4
    repo.updatePodcastEpisodes { podcastUpdates ->
      // 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.

fun setActivePodcast(feedUrl: String, callback: (PodcastSummaryViewData?) -> Unit) {
  val repo = podcastRepo ?: return
  repo.getPodcast(feedUrl) {
    if (it == null) {
      callback(null)
    } else {
      activePodcastViewData = podcastToPodcastView(it)
      activePodcast = it
      callback(podcastToSummaryView(it))
    }
  }
}
val podcastFeedUrl = intent.getStringExtra(EpisodeUpdateWorker.EXTRA_FEED_URL)
if (podcastFeedUrl != null) {
  podcastViewModel.setActivePodcast(podcastFeedUrl) {
    it?.let { podcastSummaryView -> onShowDetails(podcastSummaryView) }
  }
}
it.episodes = it.episodes.drop(1)

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.
© 2024 Kodeco Inc.

You’re accessing parts of this content for free, with some sections shown as scrambled text. Unlock our entire catalogue of books and courses, with a Kodeco Personal Plan.

Unlock now