Locale® contains an advanced plug-in architecture, allowing developers to create new conditions and settings. In fact, there are hundreds of plug-ins currently available for Locale! This page contains code samples, documentation, and tips for implementing a plug-in.
Interaction between Locale (host) and plug-ins (client) occurs via an Intent-based API. The plug-in architecture is implemented in three different layers (API, SDK, and Example), with each subsequent layer becoming more abstract and easier to work with. These layers are open sourced on GitHub.
- API: android-plugin-api-for-locale defines the raw Intent-based API. We do not recommend developers start at this layer.
- SDK: android-plugin-client-sdk-for-locale contains a set of classes that abstract the Intent-based API, allowing plug-in developers to focus on the novel behavior of their own plug-ins. (For completeness, the android-plugin-host-sdk-for-locale is also available to demonstrate Locale's internal implementation for interacting with plug-ins).
- Example: Real working plug-ins that demonstrate how to use the SDK layer. For all plug-in developers, we highly recommend starting at this layer. Simply fork one of these Github projects and modify the behavior for your plug-in.
A plug-in implementation consists of two things:
- Activity: for the ACTION_EDIT_CONDITION or ACTION_EDIT_SETTING Intent action. The Activity icon and label will be shown in the host UI, identifying the plug-in to the user.
- BroadcastReceiver: for ACTION_QUERY_CONDITION or ACTION_FIRE_SETTING Intent action.
At runtime the host launches the plug-in's Activity, the plug-in's Activity returns a Bundle to the host, and the host will send that Bundle back to the plug-in's BroadcastReceiver when it is time to query/fire the plug-in. The host may also pass the Bundle back to the Activity in the future, if the user wishes to edit the plug-in's configuration.
Activity for Editing
Once the user has completed configuring the plug-in, the Activity result Intent back to the host must contain the following:
- Result Code: Tells the host whether the plug-in should be saved or not. RESULT_OK indicates that changes should be saved and blurb and Bundle are required. RESULT_CANCELED indicates that changes should not be saved and no blurb or Bundle are required.
- Blurb Extra: Constant key value EXTRA_STRING_BLURB maps to a String extra that represents a concise, human-readable status of the plug-in instance. For example, a volume setting might have a blurb that says, "50%."
- Bundle Extra: Constant key value EXTRA_BUNDLE maps to a Bundle that represents the entire state of the plug-in. This Bundle is stored by by the host and forwarded back to the plug-in whenever the plug-in is edited again or eventually queried/fired. The host guarantees that the Bundle will not be modified in any way. Practically anything can be stored in the Bundle, with the following limitations: 1. The Bundle cannot contain Parcelable objects, because these cannot be stored long-term. Use Serializable instead. 2. The Bundle can only contain objects that are part of the standard Android platform, because the host's class loader will need to serialize and deserialize these objects. 3. The Bundle MUST be less than 25-kilobytes (base-10) in size, and SHOULD be significantly smaller than that.
BroadcastReceiver for Querying a Condition
When the host queries a plug-in condition, the host will send an explicit ordered Intent with the action ACTION_QUERY_CONDITION to the BroadcastReceiver exported by the plug-in's AndroidManifest. This Intent will contain the EXTRA_BUNDLE extra that was previously saved by the edit Activity. There will be one broadcast for each plug-in instance. Using the Display Condition example, let's say the user has created two situations in Locale, each with a single Display condition. Locale will broadcast two ACTION_QUERY_CONDITION Intents, each with a different EXTRA_BUNDLE. Plug-ins should not keep state about what instances have been created, as the user could delete these from within the host at any time. A plug-in should simply process each ordered broadcast as it comes.
The host makes no guarantees as to how often or if the plug-in will be queried. Typically, a plug-in will be queried once when the host first starts up, and additional queries may happen at any time. For example, if a situation in Locale contains multiple conditions, the order in which conditions are queried is unspecified. Locale may choose to stop evaluating conditions in a situation if one condition is not satisfied, at which point a plug-in may never be queried. Similarly, a plug-in will no longer be queried if the host is uninstalled or its Service is stopped.
In addition to waiting for the host to query the plug-in, a plug-in can signal to the host that it is ready to be queried via the ACTION_REQUEST_QUERY Intent. If a plug-in instance does not receive a response to a query request within one minute, the plug-in should stop requesting requeries as no response indicates that the host is not currently interested in that plug-in instance. For example, all instances of that plug-in could have been deleted within the host, the host's service could have been stopped, or the host could have been uninstalled. When the host becomes interested in the plug-in again, the plug-in will receive ACTION_QUERY_CONDITION.
It may not be possible to implement all conceivable plug-in conditions with this interface. Plug-ins that operate asynchronously (e.g. they receive actions like ACTION_POWER_CONNECTED and then broadcast ACTION_REQUEST_QUERY to the host) or plug-ins that can respond to a query Intent immediately, such as a calendar plug-in, will work best. Plug-ins that require a Service to operate are not recommended, as each running Service consumes additional RAM which is limited resource on an Android device.
BroadcastReceiver for Firing a Setting
When it comes time to apply a plug-in setting, the host will send an explicit Intent with the action ACTION_FIRE_SETTING to the BroadcastReceiver exported by the plug-in's AndroidManifest.
Locale prints logcat messages to help plug-in developers. Specifically, Locale prints log messages during the plug-in discovery process and will note any errors detected in the plug-in. In addition, Locale prints log messages when starting and receiving results from the plug-in's edit Activity. Finally, Locale's Service also prints log messages when querying plug-in conditions or firing plug-in settings.
Although the host should keep its configuration private, plug-ins MUST NOT pass sensitive information such as login credentials via EXTRA_BUNDLE. Doing so would constitute a serious security flaw because Android allows any application with the GET_TASKS permission to read the Intent sent by the host to a plug-in Activity through ActivityManager.getRecentTasks(int, int). Although this security flaw is fixed in Android 5.0 and later, there are many devices still running vulnerable versions of Android.
If a plug-in needs to store login credentials, there are more secure implementations. Remember that each app on Android with a unique digital signature will run in its own sandbox. To improve security of private data, such as usernames and passwords, only minimal information needs to be passed to the host via EXTRA_BUNDLE. For example, consider a hypothetical plug-in setting that posts a Tweet to Twitter. It could store OAuth credentials in a SharedPreference file private to the plug-in and only returns the non-private Tweet message via EXTRA_BUNDLE.
Backup/restore with login credentials
Consider what happens to a plug-in if its application data is cleared but the instances within the host are not. For example, a hypothetical plug-in might post a Tweet to Twitter. This plug-in stores only the Tweet message within its EXTRA_BUNDLE to the host. The Tweet plug-in stores its OAuth login credentials in a SharedPreference file that is private to the Tweet plug-in. When the Tweet plug-in receives a "fire" Intent from the host but doesn't have any login credentials stored, it displays a notification in order to let the user login again. This ensures that the Tweet plug-in doesn't fail mysteriously or silently when it is reinstalled. For security reasons, a plug-in should not store login credentials within the EXTRA_BUNDLE Bundle nor should it back them up via the Android Backup Manager.
Backup/restore with hardware changes
Locale supports backup and restore. Consider what happens to a plug-in if it depends on certain hardware or software characteristics that are different across Android devices. For example, the HTC Nexus One ringer volume can range from 0 to 7, while the Motorola Droid X ringer volume can range from 0 to 15. Locale's built-in Volume setting stores volumes as a relative value (e.g. 100%) rather than an absolute value (e.g. 15) in order to ensure portability across different devices during backup and restore.
Every Android application should contain the targetSdkVersion attribute in its Android Manifest. This should be set to the highest value that the application has been tested against. By not keeping this value up to date with the latest versions of Android, the application will be forced to run in Android's backwards compatibility mode. Locale will print a warning via logcat for all plug-ins whose targetSdkVersion is less than Locale's.
Android 3.0 (API level 11) and later provide support for hardware acceleration of an app's UI. When hardware acceleration is enabled, an app's RAM memory usage is significantly increased for the lifetime of the process as soon as any UI is displayed. For this reason, we recommend one of two solutions: 1. Disable hardware acceleration for the plug-in. 2. Put the plug-in's BroadcastReceiver (and any Services) in a separate process from the UI so that background processing will use less memory.
Long requests (e.g. Internet requests) and WakeLocks
A BroadcastReceiver is supposed to do its work quickly. In fact, a BroadcastReceiver has only ten seconds to complete before Android kills it. Longer-running tasks, such as making a request over the Internet, will require an Android Service. When launching a Service, plug-ins also also need to acquire a WakeLock in the BroadcastReceiver to ensure that the device doesn't fall asleep until the task is completed because the host may send broadcasts to a plug-in's BroadcastReceiver when the device is asleep. The Display plug-in condition exemplifies how to do this.
Selecting multiple items
In general, a condition or setting within Locale should only choose a single item because Locale can add multiple conditions or settings of the same type to a situation. For example, the built-in Shortcut setting only chooses a single app to launch. In order to launch multiple apps, the user adds another instance of the Shortcut setting to the situation.
In general, avoid creating plug-ins that duplicate functionality built-in to Locale. For example, Locale's built-in Volume setting controls ringer and notification volumes. If a hypothetical third-party volume plug-in were to also control ringer and notification volumes, it would be possible for a user to configure both the built-in and plug-in settings within Locale at the same time and the two settings would conflict with each other. In this case, it would be best if the plug-in omitted ringer and notification volumes and only controlled voice and alarm volumes, which Locale does not natively control at this time.
If a plug-in is not a standalone application and does not provide a launcher screen Activity, then it should implement an Info Activity that gives users an entrypoint to the plug-in from Google Play. The example plug-ins provide InfoActivity for this purpose.
Launcher Activity for undo of plug-in settings
Some plug-ins may change settings that are not normally exposed by Android to the user. For such plug-ins, a Launcher Activity should be provided so that the user can undo such changes even if the host isn't installed. For example, consider APNDroid: APNDroid provides a Launcher Activity and a plug-in. Even if the user were unable to open Locale, the user could still restore APN settings from the APNDroid Launcher Activity. By providing a Launcher Activity, it is possible for a user to undo anything that the plug-in has set.
Test plug-ins for standalone apps
Many plug-ins are part of a larger standalone app, such as Seal. When creating such plug-ins, be sure to test what happens if the user tries launching the plug-in before launching the standalone app.
If the blurb cannot represent the plug-in concisely, then consider reducing the complexity of the plug-in. For example, the Locale Volume setting does not control media, alarm, and voice volumes specifically because all those values cannot be represented in a single blurb. Instead, it would be better to break the different volumes into separate settings. With that said, this is simply a guideline and best judgment should be used when creating a blurb.
Run from internal memory
Locale is a background service. Therefore Locale will only run from internal memory in order to meet Android's guidelines for app install locations. Because plug-ins similarly run in the background, they must also be configured to run from internal memory only.
Locale is currently localized to Czech (cs), German (de), English (en), Spanish (es), French (fr), Italian (it), Japanese (jp), Korean (ko), Norwegian (nb), Dutch (nl), Brazilian Portuguese (pt-rBR), Russian (ru), Swedish (sv), and Vietnamese (vi). Plug-in developers should consider localizing plug-ins in order to provide a seamless experience for users.