Skip to content

4. SDK: Creating a Complication

Kieron Quinn edited this page Nov 16, 2023 · 3 revisions

Creating a Complication

Smartspacer's Complications are small snippets of information, consisting of an icon and a short amount of content. Complications are displayed alongside Targets, with any remaining Complications displayed in additional pages after the Targets.

Setup

Firstly, create a new project or open an existing one. It does not matter if the project uses Compose or Views, since Complications use neither.

Adding the SDK

Add the Plugin SDK:

implementation 'com.kieronquinn.smartspacer:sdk-plugin:version'

You can find the latest version here

The Complication Class

Complications at their core are Content Providers, but the Smartspacer SDK handles most of the logic required for a Provider to function.

The basic Complication class is as follows:

class ExampleComplication: SmartspacerComplicationProvider() {

    override fun getSmartspaceActions(smartspacerId: String): List<SmartspaceAction> {
        //Return a list of SmartspaceActions
    }

    override fun getConfig(smartspacerId: String?): Config {
        //Return your configuration
    }

    override fun onProviderRemoved(smartspacerId: String) {
        //Handle removal of the Complication (optional)
    }

}

Note: Since SmartspaceAction is also used in the core of SmartspaceTarget, the name has been kept to match the system. In the case of Complications, SmartspaceAction represents a single Complication.

Declaring in the Manifest

Since a Complication is based on ContentProvider, it must be specified as one, with a specific action and permission:

<provider
	android:name=".ExampleComplication"
	android:authorities="${applicationId}.complication.example"
	android:permission="com.kieronquinn.app.smartspacer.permission.ACCESS_SMARTSPACER_COMPLICATIONS"
	android:exported="true">
	<intent-filter>
		<action android:name="com.kieronquinn.app.smartspacer.COMPLICATION" />
	</intent-filter>
</provider>

Your Complication must be exported, and must have the specific action and permission shown above. Without this, they will not work in Smartspacer.

Configuration

The getConfig method needs to return some basic information for Smartspacer about your Complication. The available options are:

Config(
	label = // Your Complication's label to be shown in Smartspacer's settings UI
	description = // A short description for your Complication to be shown in the same UI
	icon = // An Icon (android.graphics.drawable.Icon) to show in the same UI
	allowAddingMoreThanOnce = // Optional: Whether the Complication should be able to be added multiple times at once (defaults to false)
	configActivity = // Optional: An Intent given as an option to the user in the settings UI after adding
	setupActivity = // Optional: An Intent of an Activity presented to the user during setup (see below)
	refreshPeriodMinutes = // Optional: How often the refresh broadcast for this Complication should be called, in minutes (defaults to 0, never)
	refreshIfNotVisible = // Optional: Whether to call the refresh broadcast if the Complication did not last return any Complications (defaults to false)
	compatibilityState = // Optional: A CompatibilityState object representing whether this Complication is compatible with the device or not (defaults to always compatible)
	widgetProvider = // Optional: The authority of a Widget Provider attached to this Complication (see Widget Providers page)
	notificationProvider = // Optional: The authority of a Notification Provider attached to this Complication (see Notification Providers page)
	broadcastProvider = // Optional: The authority of a Broadcast Provider attached to this Complication (see Broadcast Providers page)
)

As a bare minimum, you must specify a label, description and icon. It's also recommended you consider specifying a Compatibility State, checking whether the device is able to add your Complication or not. For example, you may require another app to load data from, in which case you would return CompatibilityState.Incompatible("App X is not installed") if the app is not installed, or CompatibilityState.Compatible if it is.

The setup and configuration activities work much the same as those used for App Widgets by Android. Specify an Intent, with any extras you wish, and Smartspacer will open it during the adding of a Complication or when the user selects the settings option from the UI respectively. These Intents will also automatically be provided with the extra SmartspacerConstants.EXTRA_SMARTSPACER_ID, a String containing the unique ID given to this Complication during setup.

The setup activity must call Activity.setResult(Activity.RESULT_OK) before finishing to tell Smartspacer setup was successful and to continue adding the Complication. Otherwise, the Complication will not be added and the provider's onProviderRemovedMethod will be called.

The configuration activity may call Activity.setResult(Activity.RESULT_OK) before finishing to tell Smartspacer to send a change notification to the Complication, though this is optional.

When specifying a refresh period, please be mindful of battery use. Refreshes are also not guaranteed, the value is a limit to how often the refresh call will be made, not a guaranteed call. Refreshes also are not guaranteed to happen at the top of the minute (:00), so may not be reliable for time-sensitive refreshes such as calendar events or reminders. Enabling the refreshIfNotVisible option means the refresh call will also be made when your Complication is not currently in Smartspace, which may be useful if you check for an update to something periodically. Enabling this option can have a severe effect on battery life if combined with refreshing often, so please be extremely careful.

Returning a Basic Complication

The getSmartspaceActions method expects a list of SmartspaceActions in return, this can be empty up to containing as many Complications as you wish. The ID provided to this method is the same unique ID provided during setup, so you can use it to retreive local data to display in a Complication, but please do not block the thread. Very simple data loading (eg. reading a row from a database or a JSON file) is usually fine, but making a network request at this state may result in failed loads and your Complication not updating.

Smartspacer's SDK comes with a template you can use from to show data in a Complication. Unlike Targets, only one type of Complication Template is currently supported: Basic.

ComplicationTemplate.Basic(
	id = "example_$smartspacerId",
	icon = Icon(android.graphics.drawable.Icon.createWithResource(provideContext(), R.drawable.ic_example)),
	content = Text("Example"),
	onClick = TapAction(
		intent = Intent(provideContext(), MainActivity::class.java)
	)
).create().apply { 
	//Apply additional options here (optional)
}

This example Basic Complication Template has all the required fields filled as follows:

  • id: The unique ID for this SmartspaceAction, you don't need to consider other plugins in this ID, but it should be unique across your app. Consider using the provided Smartspacer ID to aid with this. The ID is also used throughout Smartspacer for efficiency and animations, so the same SmartspaceAction (for example a weather forecast for a location) should keep the same ID.
  • icon: The Icon object representing this SmartspaceAction's icon. This is a model provided by the SDK, containing an android.graphics.drawable.Icon object, as well as options for whether to automatically tint the icon and an accessibility content description. Due to the duplication of class names, it's recommended to use import aliases to make this easier to understand. Note here how provideContext() is used - this is the same as requireContext(), but backwards compatible down to API 29.
  • content: The Text object representing this SmartspaceAction's content. While just content is required, you can also specify justification here. The content is also a CharSequence and thus supports basic Parcelable formatting - colour and style - though both this and justification are not supported by OEM Smartspace. This content is limited to 12 characters.
  • onClick: The TapAction object representing this SmartspaceAction's action to be invoked on click. It contains:
    • intent: An Intent to be invoked on tap (must be an Activity)
    • pendingIntent: A PendingIntent to be be invoked on tap (can be any, takes priority over Intent)
    • shouldShowOnLockScreen: If set, Smartspacer will not try to unlock the device when the action is run. If you start an Activity that can display over the lock screen, or send a PendingIntent which starts a service or sends a broadcast, this can mean no interruption to the user.

Additional Options

After creating a SmartspaceAction, some fields can be set to modify Smartspacer behaviour. You can set these on a SmartspaceAction object:

  • limitToSurfaces: An optional set of UI Surfaces (HOMESCREEN and LOCKSCREEN) on which the Complication should be shown. If not set, it will be shown on all.
  • weatherData: An optional WeatherData object containing a weather data represented by the Complication. This allows weather to be displayed on the AoD when using the special "Weather" style AoD on Pixels on Android 14+, which has its own limited set of weather icons. It's not used anywhere else, and only the first Complication with this data is used.

Complication Removal

When the user removes a Complication from Smartspacer, the onProviderRemoved method will be called with your unique Smartspacer ID. You should perform cleanup actions here, such as removing any data from the database related to the Complication.

Please note that this method will not be called if Smartspacer is uninstalled.

Complication Configuration

In the settings UI for your Complication within Smartspacer, the user can be presented with a link to your app's settings. Some customisation is available, depending on how you declare the Activity your Intent is linking to in your manifest:

<activity
	android:name=".ConfigurationActivity"
	android:description="A short description to be shown to the user of what they can configure"
	android:exported="true"
	android:icon="@drawable/ic_complication_settings"
	android:label="Example Settings" />

Requesting an update to your Complication

If you wish to notify Smartspacer of a change to your Complication, and thus a call to getSmartspaceActions should be made, you can call one of the two static notifyChange methods in SmartspacerComplicationProvider from anywhere you have a context. These are:

  • SmartspacerComplicationProvider.notifyChange(Context, Authority, Smartspacer ID (optional)): Updates a Complication with a given provider Authority. If provided, only that with a given Smartspacer ID will be updated (only useful if you are allowing multiple of the same Complication to be added)

  • SmartspacerComplicationProvider.notifyChange(Context, Class<out SmartspacerComplicationProvider>, Smartspacer ID (optional)): Updates a Complication with a given Complication class. If provided, only that with a given Smartspacer ID will be updated (only useful if you are allowing multiple of the same Complication to be added)

Receiving Complication Refresh Broadcasts

If you specified a refresh period in your config, detailed earlier, your app will receive periodic broadcasts. To receive them, set up a receiver extending from SmartspacerComplicationUpdateReceiver:

class ExampleComplicationUpdateReceiver: SmartspacerComplicationUpdateReceiver() {

    override fun onRequestSmartspaceComplicationUpdate(
        context: Context,
        requestComplications: List<RequestComplication>
    ) {
        //Handle updates to the requested complications
    }
}

You must also declare this in your manifest with the required action and permission, since it is a BroadcastReceiver:

<receiver
	android:name=".ExampleComplicationUpdateReceiver"
	android:exported="true"
	android:permission="com.kieronquinn.app.smartspacer.permission.SEND_UPDATE_BROADCAST">
	<intent-filter>
		<action android:name="com.kieronquinn.app.smartspacer.REQUEST_COMPLICATION_UPDATE" />
	</intent-filter>
</receiver>

Note: The RequestComplication object contains the authority and Smartspacer ID for each Complication an update has been requested for. Updates are batched, so if you have multiple Complications requesting an update at the same minute, the broadcast will only be received once, with multiple requestComplications

Smartspace Visibility

If you wish to receive a broadcast when Smartspace becomes visible, or stops being visible, you can add a receiver for this too. This could be used to pause background updates, or request an update to your data automatically when Smartspace becomes visible. Create a receiver extending from SmartspacerVisibilityChangedReceiver:

class SmartspaceVisibilityReceiver: SmartspacerVisibilityChangedReceiver() {

    override fun onSmartspaceVisibilityChanged(context: Context, visible: Boolean, timestamp: Long) {
	//Run an update?
    }

}

You must also declare this in your manifest with the required action and permission, since it is a BroadcastReceiver:

<receiver android:name=".SmartspaceVisibilityReceiver"
	android:exported="true"
	android:permission="com.kieronquinn.app.smartspacer.permission.SEND_UPDATE_BROADCAST">
	<intent-filter>
		<action android:name="com.kieronquinn.app.smartspacer.SMARTSPACE_VISIBILITY_CHANGED" />
	</intent-filter>
</receiver>

Important note: This option requires the user have Smartspacer v1.3 or above installed. You can use "minimum_smartspacer_version": 130 in your update.json file to enforce this.

Complication Backups

Smartspacer supports an integrated backup system, since it is not available on Google Play. The idea of this is all Targets, Complications and Requirements contribute to a single backup file, which when restored provides the same data back to the plugins for restoration. You should put basic information such as settings in here, but data such as images should not be backed up - and large data will be rejected due to Android's Binder limit of 1MB. Backups are entirely optional - if you do not wish to implement it, the user will simply be presented with your Complication during restoration and add it as if it were new.

During a backup, the createBackup method will be called, passing the standard Smartspacer ID:

override fun createBackup(smartspacerId: String): Backup {
	val settings = //load settings
	return Backup(settings.serialiseToString(), "A short description")
}

You should use the ID to load the settings of your Complication, and serialise it to a String (consider using GSON or similar libraries to flatten to JSON easily). A short description should also be provided specifying what was in the backup, which will be shown to the user in lieu of the Complication's description so they know what is being restored.

Complication Restoration

Similarly, the restoreBackup method is called during the restoration process:

override fun restoreBackup(smartspacerId: String, backup: Backup): Boolean {
	//Handle restoring your backup
	notifyChange(smartspacerId)
	return true
}

Important note: The Smartspacer ID here will not be the same as your one at the time of backup. Complication do not keep a static ID between different Smartspacer install instances

The Backup object here contains the data you sent during the backup process. You should deserialise the data, commit it however you wish, and then call notifyChange(smartspacerId) to update the Complication with this new data. Returning true tells Smartspacer the restoration was successful, and the setup activity (if specified) does not need to be shown for this Complication. If you cannot restore the Complication for whatever reason, return false and the setup activity will be shown.

Note: If your Complication requires certain permissions which will not persist across a reinstall, consider returning false even if the restore succeeds, so you can make sure the user grants the permission before the Complication finishes being added.