Muzei 3.0 and the new API

Ian Lake
muzei
Published in
9 min readJul 11, 2018

Muzei, besides being a capable live wallpaper in its own right, has continued to be successful due to its plugin architecture which allowed many, many other developers to build their own wallpaper source, ensuring that users always have a wide variety of images from whatever source they could imagine.

As previously discussed in my The Muzei Plugin API and Android’s Evolution post, a lot has changed in Android since Muzei was first released by Roman Nurik back in the times of Android 4.4.

This has meant a full rewrite of both Muzei and its API has been necessary to ensure that it is compatible with Doze and App Standby as well as Background Execution Limits.

I’m happy to announce that alongside the first alpha of Muzei 3.0, an alpha of the new Muzei API is available that brings the API in alignment with the latest best practices and offers some significant improvements to the overall user experience for Muzei.

Edit: the Muzei 3.0 API is now available in beta and is expected to be forward compatible with Muzei 3.0 as we move towards a full production release.

No more unresponsive Next buttons

Rewriting an entire app and plugin ecosystem is not something to be taken lightly so any new API needed to offer a significantly better user experience, both to end users and to developers.

One of the main complaints of Muzei is that you’d hit the ‘Next’ button in the app and nothing would happen. That’s a bad user experience in all regards that the new API seeks to remove entirely even as a possibility.

Supporting multiple artwork

Unlike the previous MuzeiArtSource based API that had just a single publishArtwork method and a single current artwork, the new API is now based on a MuzeiArtProvider class — a special subclass of ContentProvider.

This new structure allows your MuzeiArtProvider to add multiple artwork at once. For example, if your network call to your backend returns 30 images, you no longer need to randomly pick one and throw the other 29 away. Instead, you can call addArtwork 30 times and Muzei will automatically go through all 30 in the order you add them before asking you to load more artwork. Besides significantly lowering the amount of requests to your backend, this allows Muzei’s UI to only show the Next button if you actually have more artwork ready.

This fundamental change also means the entry point for your MuzeiArtProvider is significantly changed. Rather than a call to onUpdate or onTryUpdate every time the wallpaper changes, you now get a callback to onLoadRequested only when Muzei loads the last new image. This should be your cue to load more artwork!

Note: not every source has an endless backend of new artwork. Don’t worry about it. Even if you don’t load new artwork in response to onLoadRequested, Muzei will loop back through the rest of the artwork you have, loading older artwork randomly (while avoiding selecting recently shown artwork) without you having to do any extra work.

Pre-loading artwork

The time between hitting the Next button and the next wallpaper displaying is the absolutely most nerve racking time for users — it has to be quick and reliable every time.

With this in mind, Muzei will now pre-load the next artwork before it is displayed, ensuring there is already a locally cached copy available before the user can even hit the Next button. This ensures that even if the user is offline, if they can see a Next button, it will go to a new wallpaper. It also means transitions are near instantaneous, even on slow internet connections!

Downloading the artwork itself

Another significant change with the new API is that downloading artwork happens on the plugin side. Instead of Muzei using the call to publishArtwork as a sign to kick off its own download process, the downloading now happens via a call to your MuzeiArtProvider’s openFile method, which returns an InputStream to your image.

The default implementation of openFile handles much of the same cases as the previous API such as content:// or android.resource:// Uris as well as basic, unauthenticated http:// or https:// Uris.

Note: Muzei enforced TLS support on Android platform versions that required it to specifically be enabled. You now need to do this work yourself. An example of this can be found in the SourceArtProvider.

Since openFile is being called within your plugin’s process, there’s no longer any need to use a FileProvider or similar solution for loading local images that you have access to — a regular old file:// URL generated from Uri.fromFile actually works fine.

This flexibility also opens up the possibility to download images from authenticated servers or support custom protocols outside of http and https all completely transparently to Muzei itself.

When Muzei updates

One consistent feature request we’ve gotten for Muzei is better control over when Muzei transitions to the next wallpaper. Different plugins would add different settings on when to publish new artwork or whether to only update when you are on Wi-Fi.

With the new API, the settings controlling when wallpaper is loaded have been centralized within Muzei through a new set of options called Auto Advance.

Auto Advance settings float above the Sources screen

For sources built on the MuzeiArtProvider API, users will be able to configure at what interval they want to automatically advance to the next artwork and whether that should only happen over Wi-Fi. Individual plugins no longer need to specify or control these settings themselves — as long as you provide artwork when onLoadRequested is called, Muzei will handle everything else.

Implementing onLoadRequested

We’ve talked about how important onLoadRequested is, so it deserves some extra attention to explain exactly how to implement it. We’ll be using the example-unsplash project as our example project. It implements a straightforward plugin that downloads the latest popular photos from Unsplash.

Our UnsplashExampleMuzeiArtProvider's onLoadRequested looks like:

override fun onLoadRequested(initial: Boolean) {
UnsplashExampleWorker.enqueueLoad()
}

Yep, that’s it. While onLoadRequested is called on a background thread (so it is safe to do database or shared preference calls), it is strongly, strongly recommended to push the actual loading to WorkManager, JobScheduler, or JobIntentService.

Personally, I strongly prefer WorkManager for this kind of work since it will start execution immediately if conditions are met (say, you’re connected to the internet) and offers control over retrying. Muzei has been using it in production for all of the source loading as of Muzei 2.6.0.

The UnsplashExampleWorker is no more complicated. Its core doWork method looks like:

override fun doWork(): Result {
val photos = try {
UnsplashService.popularPhotos()
} catch (e: IOException) {
Log.w(TAG, "Error reading Unsplash response", e)
return Result.RETRY
}
if (photos.isEmpty()) {
Log.w(TAG, "No photos returned from API.")
return Result.FAILURE
}
val attributionString =
applicationContext.getString(R.string.attribution)
photos.map { photo ->
Artwork().apply {
token = photo.id
title = photo.description ?: attributionString
byline = photo.user.name
attribution =
if (photo.description != null) attributionString else null
persistentUri = photo.urls.full.toUri()
webUri = photo.links.webUri
metadata = photo.user.links.webUri.toString()
}
}.forEach { artwork ->
ProviderContract.Artwork.addArtwork(applicationContext,
UnsplashExampleArtProvider::class.java,
artwork)
}
return Result.SUCCESS
}

We load from a Retrofit service, create a new Artwork object for each photo returned from the API, then call ProviderContract.Artwork.addArtwork to add the artwork to the correct MuzeiArtProvider.

Unlike the previous API where all calls to publishArtwork needed to be within the MuzeiArtSource itself (forcing you to start the service, and your own action handling in onStartCommand, etc), the ProviderContract.Artwork class has static methods for common operations, such as getting the last artwork or adding new artwork, that can be called from anywhere in your application.

As a convenience, each of the static methods in ProviderContract.Artwork also has a non-static equivalent in MuzeiArtProvider, so calling addArtwork within the MuzeiArtProvider is as straightforward as it gets.

Note: as a MuzeiArtProvider is a ContentProvider itself, you can also use all of the ContentResolver based APIs such as query and delete using the base content URI returned by ProviderContract.Artwork.getContentUri and the column names in ProviderContract.Artwork .

Adding custom commands

Muzei’s API has always offered the ability to add custom commands to your plugin, giving users additional plugin specific actions they can take.

As the concept of the ‘Next Artwork’ is now controlled entirely by the existence of more valid artwork being added to your MuzeiArtProvider, the commands you return from the getCommands method should now all be custom commands (the default behavior is to return an empty list).

The Unsplash Example returns two custom commands:

override fun getCommands(artwork: Artwork) = listOf(
UserCommand(COMMAND_ID_VIEW_PROFILE,
context.getString(R.string.action_view_profile,
artwork.byline)),
UserCommand(COMMAND_ID_VISIT_UNSPLASH,
context.getString(R.string.action_visit_unsplash)))

You’ll note that we’re given the Artwork object that these commands should be associated with. This allows you to customize what commands are supported on an artwork by artwork basis or, like this example, customize the strings used for your actions with information from your artwork.

Handling commands is then done from the onCommand method, which also gets the Artwork object and the id of the action that was selected.

The Unsplash Example implements this with a simple when statement:

override fun onCommand(artwork: Artwork, id: Int) {
when (id) {
COMMAND_ID_VIEW_PROFILE -> {
val profileUri = artwork.metadata?.toUri() ?: return
context.startActivity(Intent(Intent.ACTION_VIEW, profileUri))
}
COMMAND_ID_VISIT_UNSPLASH -> {
val unsplashUri = context.getString(R.string.unsplash_link) +
ATTRIBUTION_QUERY_PARAMETERS
context.startActivity(Intent(Intent.ACTION_VIEW,
unsplashUri.toUri()))
}
}
}

You’ll note that here we use the metadata property. This String property is not used by Muzei in any way so you can repurpose it for whatever extra information you want to attach to each Artwork. In this case, we attach the URL of the photographer’s profile.

Adding a MuzeiArtProvider to your Manifest

Just like any other ContentProvider or service, you must add your MuzeiArtProvider to your manifest:

<provider
android:name="com.example.CoolArtworkMuzeiArtProvider"
android:authorities="com.example.coolartwork"
android:label="@string/name"
android:description="@string/description"
android:exported="true"
android:permission=
"com.google.android.apps.muzei.api.ACCESS_PROVIDER">
<intent-filter>
<action android:name="com.google.android.apps.muzei.api.MuzeiArtProvider"/>
</intent-filter>
</provider>

You’ll that it is strongly recommended to add the android:permission to your MuzeiArtProvider. This ensures that only your app and Muzei can read your Artwork. (Of course, you could leave it off if you want any app to be able to read your Artwork — your choice.)

Similar to a MuzeiArtSource, you can use <metadata> tags for a settingsActivity and setupActivity . More information on this can be found in the documentation.

Migrating from MuzeiArtSource

For users using Muzei 3.0, sources using MuzeiArtSource will appear in a separate ‘Legacy Sources’ option within Muzei’s ‘Sources’ screen, clearly delineating them from the modern sources built using MuzeiArtProvider. This is absolutely an intentional change.

If your app targets API 26 or higher

With the Target API level requirement for late 2018 enforcing a targetSdkVersion of 26 or higher for all updates to existing apps starting in November 2018, you might be considering changing your targetSdkVersion now.

If you update your targetSdkVersion now, note that the MuzeiArtSource API will no longer work. At all. Muzei has been enforcing this for quite some time, making your source unselectable.

In this case, you can safely delete your MuzeiArtSource and add a new MuzeiArtProvider .

Supporting both the old and new API simultaneously

To give the best experience for Muzei users in the short term, it is strongly recommended to support both the old and new API simultaneously by keeping your MuzeiArtSource functional while also providing a MuzeiArtProvider implementation for users already on Muzei 3.0.

In order to avoid duplicate sources in Muzei’s UI and to automatically migrate users of your existing MuzeiArtSource to your new MuzeiArtProvider, you should add a replacement metadata element to your MuzeiArtSource :

<service
android:name="com.example.muzei.unsplash.UnsplashExampleArtSource"
android:label="@string/name"
android:description="@string/description"
android:icon="@drawable/ic_source"
tools:ignore="ExportedService">
<intent-filter>
<action android:name="com.google.android.apps.muzei.api.MuzeiArtSource"/>
</intent-filter>
<meta-data
android:name="color"
android:value="@color/colorPrimary"/>
<meta-data
android:name="replacement"
android:value="com.example.muzei.exampleunsplash"/>

</service>

Muzei will read that metadata attribute and ensure that only your MuzeiArtProvider is visible in the Muzei UI and any users are automatically migrated over to the MuzeiArtProvider with the given authority.

Timeline for Muzei 3.0 and beyond

Muzei 3.0 is in beta right now and it will have a longer alpha / beta period to give you developers a chance to exercise and give feedback on the new API. It will then to go production and all users will have access to sources built on the new API.

Before the November 2018 requirement to target API 26+, another Muzei release will be made, warning users that still have a legacy source selected that their legacy source will stop working on ~January 2019.

In ~January 2019, Muzei itself will upgrade to target API 26+ and all support for MuzeiArtSource built plugins will be dropped (it literally won’t work at all).

Get started today

Update: Muzei 3.0 is now available in the open beta!

Muzei 3.0 is available in alpha today. To join the alpha:

  1. Join the Muzei Google+ Community
  2. Opt in to alpha testing (you may need to leave the tester program and rejoin if you previously were beta testing Muzei)
  3. After ~15 minutes, you should see an update available via the Google Play Store

If you’d like to get started with the new API, update your dependency:

implementation "com.google.android.apps.muzei:muzei-api:3.0.0-beta02"

You can download the aar, sources.jar, or javadoc.jar from the Github release page.

Note: The Muzei 3.0 API is in beta and subject to minor bug fixes. The API as written now will be forward compatible with Muzei 3.0 releases and is safe to use in production.

Feedback is greatly appreciated. The best way to contact us through the Google+ community or by sending an email to support@muzei.co.

--

--

Ian Lake
muzei
Editor for

Android Framework Developer at Google and Runner