Hosting Slices

For a SliceProvider to be useful, something need to be able to request and display a slice served by that provider. In this chapter, we will explore what it takes to do just that: display in your app a slice obtained from some other app’s SliceProvider.

What You Need to Know

Hosting slices is tied very closely to LiveData from the Architecture Components. In a nutshell, LiveData is a way for you to subscribe to updates to some source of data, akin to subscribing to an RxJava Observable. LiveData has the added benefit of being aware of the Android lifecycle, so it can cleanly unsubscribe from the data source when the hosting activity or fragment is destroyed. You can learn more about LiveData from CommonsWare’s book, Android’s Architecture Components.

The Slices/Inspector sample project shown in this chapter happens to be written in Kotlin. If you are not familiar with Kotlin, focus less on the syntax shown in the examples and more on the concepts outlined in the prose.

Why?

Just because somebody needs to host slices does not mean that you have to host slices in your app. So, why would you bother writing code to display other apps’ slices?

Depending on how the slice ecosystem evolves, there could be many good reasons to host slices. And, the good news is that for simple scenarios, hosting slices is not very difficult.

How?

You have two main approaches for hosting a slice. Most developers will take the simple approach, using some Google-supplied library code to display and update the slice. If needed, though, you can get at the raw information about the slice and build your own custom rendered edition of that information.

In both cases, for the moment, we will assume that we know the Uri of the SliceProvider. In the Inspector sample app, to keep it simple, this value is hard-coded in gradle.properties to be the SamplerX example from the chapter on publishing slices:

SLICE_URI=content://com.commonsware.android.slice.dice.provider
(from Slices/Inspector/gradle.properties)

You can test using the Inspector with any other slice provider simply by changing that line to provide the Uri that you wish to test. That Uri is then hoisted into BuildConfig via a buildConfigField statement in app/build.gradle:

    buildConfigField "String", "SLICE_URI", '"' + SLICE_URI + '"'
(from Slices/Inspector/app/build.gradle)

So, the app’s code can identify our SliceProvider via BuildConfig.SLICE_URI.

The Simple Way: SliceView

Only two classes are really needed to render a slice given the Uri:

The sample app has a ViewPager with four tabs. The first three tabs each show a slice from the provider identified by SLICE_URI. Those tabs each use a SliceView, centered inside of a ConstraintLayout:

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
  xmlns:app="http://schemas.android.com/apk/res-auto"
  android:layout_width="match_parent"
  android:layout_height="match_parent"
  android:background="@android:color/darker_gray">

  <androidx.slice.widget.SliceView
    android:id="@+id/slice"
    android:layout_width="wrap_content"
    android:layout_height="wrap_content"
    android:layout_margin="8dp"
    android:background="@android:color/white"
    app:layout_constraintBottom_toBottomOf="parent"
    app:layout_constraintEnd_toEndOf="parent"
    app:layout_constraintStart_toStartOf="parent"
    app:layout_constraintTop_toTopOf="parent" />
</androidx.constraintlayout.widget.ConstraintLayout>
(from Slices/Inspector/app/src/main/res/layout/fragment_slice.xml)

A SliceView has no intrinsic background. If you do nothing else, it will render text and images with no background. In this layout, we set the ConstraintLayout to have a gray background and the SliceView to have a white background, so you can see the bounds the SliceView.

The sample app has a TabAdapter serving as our PagerAdapter for the ViewPager. For those first three tabs, it creates instances of a SliceFragment:

package com.commonsware.android.slice.inspector

import androidx.fragment.app.Fragment
import androidx.fragment.app.FragmentManager
import androidx.fragment.app.FragmentPagerAdapter
import androidx.slice.widget.SliceView

class TabAdapter(fm: FragmentManager, private val titles: Array<String>) : FragmentPagerAdapter(fm) {
  override fun getCount() = 4

  override fun getPageTitle(position: Int) = titles[position]

  override fun getItem(position: Int): Fragment =
      when (position) {
        0 -> SliceFragment.newInstance(SliceView.MODE_SHORTCUT)
        1 -> SliceFragment.newInstance(SliceView.MODE_SMALL)
        2 -> SliceFragment.newInstance(SliceView.MODE_LARGE)
        else -> InspectorFragment()
      }
}
(from Slices/Inspector/app/src/main/java/com/commonsware/android/slice/inspector/TabAdapter.kt)

The newInstance() factory method on SliceFragment takes a SliceView “mode” value, indicating whether we want a shortcut, a small view, or a large view. These modes were describe in the chapter on publishing slices.

In onCreateView(), the SliceFragment fills the mode into the SliceView via setMode() (via the mode property syntax in Kotlin), defaulting to large mode if for some reason we did not get a mode provided to us:

package com.commonsware.android.slice.inspector

import android.net.Uri
import android.os.Bundle
import android.view.LayoutInflater
import android.view.View
import android.view.ViewGroup
import androidx.fragment.app.Fragment
import androidx.slice.widget.SliceLiveData
import androidx.slice.widget.SliceView

private const val ARG_MODE = "mode"

class SliceFragment : Fragment() {
  companion object {
    fun newInstance(mode: Int) =
        SliceFragment().apply {
          arguments = Bundle().apply { putInt(ARG_MODE, mode) }
        }
  }

  override fun onCreateView(inflater: LayoutInflater, container: ViewGroup?, state: Bundle?): View? {
    val result = inflater.inflate(R.layout.fragment_slice, container, false)
    val sliceView = result.findViewById<SliceView>(R.id.slice)

    sliceView.mode = arguments?.getInt(ARG_MODE, SliceView.MODE_LARGE) ?: SliceView.MODE_LARGE

    SliceLiveData
        .fromUri(activity!!, Uri.parse(BuildConfig.SLICE_URI))
        .observe(this, sliceView)

    return result
  }
}
(from Slices/Inspector/app/src/main/java/com/commonsware/android/slice/inspector/SliceFragment.kt)

Then, we:

And… that’s it.

At this point, SliceLiveData and SliceView take over to render the slice in the requested mode:

Slice Inspector, Showing Shortcut Mode
Figure 720: Slice Inspector, Showing Shortcut Mode

Slice Inspector, Showing Small Mode
Figure 721: Slice Inspector, Showing Small Mode

Slice Inspector, Showing Large Mode
Figure 722: Slice Inspector, Showing Large Mode

All of the actions included in the slice are handled by SliceView. In this case, all of the SamplerX actions work, each showing a Toast with the appropriate message.

Also, so long as you SliceLiveData has an active observer, it will receive updates to the slice content if the SliceProvider publishes updates. So, for example, if you inspect a slice that needs to perform network I/O — such as the weather slice profiled in the chapter on publishing slices — you will first get whatever the “loading” slice content is, then you will get the real content. SliceLiveData simply pushes new slices to the SliceView, which updates its content accordingly.

The Fully-Custom Way: Slice

You do not need a SliceView to display the contents of a slice, if you want to decide how to display those contents yourself. Instead, have the SliceLiveData route to your own code. It will hand you Slice objects, which you can inspect and use for setting up your own UI.

Unfortunately, this is largely undocumented.

The fourth tab in the Slice Inspector brings up a graph showing the various pieces of a slice:

Slice Inspector, Showing Graph of Slice Elements
Figure 723: Slice Inspector, Showing Graph of Slice Elements

This is handled by a separate InspectorFragment, using a TreeView from this library.

A Slice is a tree structure, created using the various builders inside of the SliceProvider. The root Slice will contain one or more SliceItem objects, obtained via the getItems() method (or the items property in Kotlin). Each SliceItem has a getFormat() method indicating what sort of item it is and what it contains:

The sample app’s graph simply puts the format value into each of the nodes, since formats happen to be human-readable strings. A app that wanted to do its own rendering of a slice — such as a screen reader wanting to use text-to-speech — would need to make sense of what is all in these slices and how to use them.

Most such apps will leverage SliceMetadata to help. You can wrap a Slice in a SliceMetadata via the SliceMetadata.from() method. SliceMetadata will give you higher-order information about an individual slice, such as:

The Catch: Discovering Slices

The Slice Inspector sample app cheats. It already knows the Uri of the slice provider, courtesy of that SLICE_URI value in gradle.properties.

In some cases, your app will be able to cheat as well. If you want to show slices from some particular app, you can “bake” the Uri to its provider into your code by one means or another. You can use PackageManager and queryContentProviders() to determine if that provider exists, and if it does, you can arrange to display its slices.

But what if you do not know what slice providers to use?

Eventually, the slice ecosystem will work out options for discovering slice providers. There is already a partial mechanism in place, involving Intent objects, but it is largely undocumented as of July 2018. Plus, it is fairly abstract, moving the discovery question from “how do I get the Uri?” to “how do I create the correct Intent?”.

Slices… from the Web?

Most of the focus on slices assume that the Uri in question is tied to a SliceProvider.

However, there is code in Google’s Slice Viewer app that supports slice Uri values having http or https as a scheme, instead of content. This implies that slices might come directly from Web servers, in addition to from local slice providers.

A slice host will not really care about the difference, other than perhaps needing the INTERNET permission where it might not be required otherwise. SliceLiveData is already asynchronous, so whether the slice data is coming from another app on the device or is coming from a server is an implementation detail for SliceLiveData, not for the hosting app.

Right now, there is no documentation for how one would do this. It is entirely possible that this bit of code was from some experiments that will never be fully supported by the slice libraries. However, if you are implementing a slice host, you may wish to watch for any signs that slices are being served by Web sites — if nothing else, they would represent additional scenarios for your app’s test suite.