APIs For Third-Party Widget Developers

This page explains the protocol that allows third-party widget developers to include their widget configurations in Smart Launcher backups and restores.

Introduction

Smart Launcher backups usually include the configuration of the launcher itself and its widgets. By using the protocol described here, your widget configuration will also be saved and restored when users create or restore a backup. This improves the user experience and ensures that widget data is retained after device resets or migrations.

How it works

The protocol relies on a custom ContentProvider that you implement in your app. This provider gives Smart Launcher a way to export and import your widget configuration data. Smart Launcher will:

  1. Call your provider to export widget configuration data whenever a backup is created.

  2. Call your provider to import widget configuration data whenever a user restores a backup.

What you need to do

  1. Implement the BackupContentProvider Create a class in your project extending BackupContentProvider (see code below) and implement the abstract methods exportWidgetConfig and importWidgetConfig. These methods handle the actual reading and writing of widget configuration data.

  2. Register the provider in your Manifest Add your BackupContentProvider to your AndroidManifest.xml. Make sure to use an authority that follows this pattern: <your_package_name>.widgetConfigProvider For example:

    <provider 
        android:name=".MyBackupContentProvider"
        android:authorities="${applicationId}.widgetConfigProvider"
        android:exported="true" />

  3. Implement the exportWidgetConfig method In exportWidgetConfig(appWidgetId: Int), read the current configuration of the specified widget and write it to a file (for example a temporary file in your app's files directory). You can use JSON, ZIP, or any format you like. Consider including a version number in the exported file so future updates can keep compatibility. If your configuration includes sensitive data, encrypt it before returning the file.

  4. Implement the importWidgetConfig method In importWidgetConfig(file: File, appWidgetId: Int), read back the configuration from the file created by the export step. Decrypt if necessary. Then apply the configuration to the widget identified by appWidgetId. This ensures that after the user restores their backup, the widget returns to the previous configuration.

  5. Update your code when configuration format changes Over time your widget configuration format might evolve. Keep the versioning of the exported data in mind and ensure your import method can still handle older versions. This provides a smoother experience for users restoring backups that were created with older versions of your app.

The BackupContentProvider class

Below is the BackupContentProvider base class. Add it to your project and extend it. You only need to override exportWidgetConfig and importWidgetConfig to handle your widget's data.

import android.content.ContentProvider
import android.content.ContentValues
import android.content.Context
import android.content.UriMatcher
import android.database.Cursor
import android.net.Uri
import android.os.ParcelFileDescriptor
import android.util.Log
import java.io.File
import java.io.IOException
import java.io.InputStream

/**
 * A ContentProvider that facilitates exporting and importing widget configuration files.
 * This abstract class provides the necessary framework and abstract methods that must be implemented
 * to perform export and import operations.
 */
abstract class BackupContentProvider : ContentProvider() {

    companion object {
        private const val TAG = "BackupContentProvider"

        private const val CODE_EXPORT = 1
        private const val CODE_IMPORT = 2

        private const val PARAM_APP_WIDGET_ID = "appWidgetId"
        private const val PARAM_URI = "uri"

        private const val PATH_EXPORT_WIDGET_CONFIG = "exportWidgetConfig"
        private const val PATH_IMPORT_WIDGET_CONFIG = "importWidgetConfig"

        private fun getUriMatcher(packageName: String) = UriMatcher(UriMatcher.NO_MATCH).apply {
            addURI(getAuthority(packageName), PATH_EXPORT_WIDGET_CONFIG, CODE_EXPORT)
            addURI(getAuthority(packageName), PATH_IMPORT_WIDGET_CONFIG, CODE_IMPORT)
        }

        private fun getAuthority(packageName: String): String = "$packageName.widgetConfigProvider"
    }

    override fun onCreate(): Boolean = true

    override fun query(
        uri: Uri,
        projection: Array<String>?,
        selection: String?,
        selectionArgs: Array<String>?,
        sortOrder: String?
    ): Cursor? {
        val context = context ?: return null
        val uriMatcher = getUriMatcher(context.packageName)
        return when (uriMatcher.match(uri)) {
            CODE_IMPORT -> handleImport(uri, context)
            else -> null
        }
    }

    private fun handleImport(uri: Uri, context: Context): Cursor? {
        val fileUriString = uri.getQueryParameter(PARAM_URI)
        val appWidgetIdString = uri.getQueryParameter(PARAM_APP_WIDGET_ID)
        if (fileUriString != null && appWidgetIdString != null) {
            val fileUri = Uri.parse(fileUriString)
            val appWidgetId = appWidgetIdString.toInt()
            loadWidgetConfigFile(context, fileUri)?.let { file ->
                try {
                    importWidgetConfig(file, appWidgetId)
                } finally {
                    file.delete()
                }
            }
        } else {
            Log.w(TAG, "Invalid parameters for import.")
        }
        return null
    }

    private fun loadWidgetConfigFile(context: Context, uri: Uri): File? {
        return try {
            context.contentResolver.openInputStream(uri)?.use { inputStream ->
                val tempFile = File.createTempFile("restored", ".zip")
                copyStreamToLocalFile(inputStream, tempFile)
            }
        } catch (e: IOException) {
            Log.w(TAG, "Failed to load widget config file for URI: $uri", e)
            null
        }
    }

    private fun copyStreamToLocalFile(inputStream: InputStream, localFile: File): File? {
        return try {
            localFile.outputStream().use { outputStream ->
                inputStream.copyTo(outputStream)
            }
            localFile
        } catch (e: IOException) {
            Log.e(TAG, "Error copying file to local storage", e)
            null
        }
    }

    override fun getType(uri: Uri): String? {
        val context = context ?: throw IllegalStateException("Context is null")
        return when (getUriMatcher(context.packageName).match(uri)) {
            CODE_EXPORT -> "application/octet-stream"
            else -> throw IllegalArgumentException("Unknown URI: $uri")
        }
    }

    override fun insert(uri: Uri, values: ContentValues?): Uri? = null

    override fun delete(uri: Uri, selection: String?, selectionArgs: Array<String>?): Int = 0

    override fun update(
        uri: Uri,
        values: ContentValues?,
        selection: String?,
        selectionArgs: Array<String>?
    ): Int = 0

    override fun openFile(uri: Uri, mode: String): ParcelFileDescriptor? {
        val context = context ?: throw IllegalStateException("Context is null")
        val uriMatcher = getUriMatcher(context.packageName)

        if (uriMatcher.match(uri) != CODE_EXPORT) {
            throw IllegalArgumentException("Unknown URI: $uri")
        }

        val appWidgetId = uri.getQueryParameter(PARAM_APP_WIDGET_ID)?.toIntOrNull()
        if (appWidgetId == null) {
            Log.e(TAG, "Invalid or missing appWidgetId in URI: $uri")
            return null
        }

        val backupFile = exportWidgetConfig(appWidgetId)
        return ParcelFileDescriptor.open(backupFile, ParcelFileDescriptor.MODE_READ_ONLY)
    }

    /**
     * Exports the widget configuration to a file.
     *
     * **Implementation Guidelines**:
     * - You can use any format for the export file, such as JSON, ZIP, or plain text.
     * - It is recommended to include versioning information in the export file format.
     *   This helps future-proof your backup system by allowing easier migration of data
     *   from older versions of your app.
     * - **Security Warning**: Remember that any app could potentially request this
     *   configuration file. If the backup file contains sensitive information, consider
     *   encrypting it to protect user data. You are responsible for choosing and implementing
     *   a secure encryption method.
     *
     * @param appWidgetId The ID of the widget whose configuration is to be exported.
     * @return A File object representing the exported configuration.
     */
    abstract fun exportWidgetConfig(appWidgetId: Int): File

    /**
     * Imports the widget configuration from a provided file.
     *
     * **Implementation Guidelines**:
     * - This method receives the exact file that was previously exported by the
     *   `exportWidgetConfig` method. It should reverse any processing applied during export,
     *   such as decrypting the file if it was encrypted.
     * - Ensure that this method accurately reconstructs the widget configuration from the file
     *   contents.
     * - Keep in mind that this method may require updates whenever the format of your widget
     *   configuration changes. This is crucial for maintaining backward compatibility and ensuring
     *   that configurations created with older versions of your app can still be restored correctly.
     *
     * @param file The file containing the configuration data to import.
     * @param appWidgetId The ID of the widget to which the configuration will be applied.
     */
    abstract fun importWidgetConfig(file: File, appWidgetId: Int)

}

Testing your implementation

  1. Install your app with the BackupContentProvider.

  2. Configure a widget and verify that it works properly.

  3. Launch a Smart Launcher backup and restore process.

  4. Confirm that after restoring the backup, the widget configuration is the same as before.

If everything is correct, users will never lose their widget configuration when they switch devices, reinstall the launcher, or reset their device.

Last updated

Was this helpful?