com.twofortyfouram:android-plugin-api-for-locale

Plugin API for Locale


Keywords
android
Licenses
Apache-2.0/libpng-2.0

Documentation

Overview

This repository contains multiple open source libraries. Because some of the libraries are interdependent, putting them in a single repository (monorepo) makes maintaining these libraries and releasing them easier.

Although details of each library are below, the high level is:

  • Annotation — Simple annotations to better document code.
  • Assertion — Runtime assertions.
  • Memento — A SQLite-based Content Provider implementation, with both an API and implementation layer.
  • Plugin SDKs for Locale — A combination of API specification, client SDK, and host SDK.
  • Spackle — A hodgepodge of utilities, mostly intended to be internal support the other modules.
  • Test — Fills in some gaps in the Android test framework.

Versioning

Because of interdependencies, any modification to a single library causes all libraries to be republished in lockstep with a new version number. The new version number is semantic, based on whether the change was a bug fix, feature, or incompatible change.

Consider an example: memento depends on spackle. Both are at version 1.0.0. If a minor bug fix is made to spackle, both libraries will be released with version 1.0.1. In other words, memento will be re-released although it didn't change (only its transitive dependency changed).

Consider another example: memento depends on spackle. Both are at version 1.0.0. If a minor bug fix is made to memento, both libraries will be released with version 1.0.1. In other words, spackle will be re-released although it didn't change, and none of its dependencies changed.

Although this pattern may change in the future, this seems to be a reasonable balance given the need to continuosly improve these libraries.

Building

The entire repo can be easily checked out and built locally by running ./gradlew assemble.

Contributing

Contributions are welcome, although contributors will need to sign a Contributor License Agreement (CLA). Please contact up@244.am.

Annotation

Assertion

Memento

Overview

Memento makes it easy to persistently store data for your Android app in a SQLite database via the ContentProvider interface.

A ContentProvider is just an abstraction to some underlying data storage mechanism. The ContentProvider API combines REST and SQL, where data is accessed via URIs (REST-like) and arguments (SQL-like). Although any storage mechanism could be used, SQLite is common. ContentProvider is a great abstraction because Android provides many features on top of the ContentProvider interface such as inter-process communication, observer design patterns, permissions, syncing, and multi-threading facilities.

The reason to use this library, rather than roll your own, is that Memento dramatically simplifies the process of implementing and testing a ContentProvider that is DRY, thread-safe, and well tested. Android ContentProvider design patterns usually combine the business logic and the data model, such that an implementation may have a UriMatcher and a repetitive switch statement within each of query(), insert(), update(), and delete(). Within those methods, there are concerns not only about the data model but also SQL injection, permissions, transactions, thread-safety, observer notification, and so on. Memento, on the other hand, takes care of implementing the business logic so that the developer only has to worry about implementing the data model. In addition, Memento provides facilities to simplify data model creation. Memento ensures minimal lock-in: because the app talks to the ContentProvider via the ContentResolver, that layer of indirection makes it easy to remove the dependency on the Memento library in the future.

Additional features include:

Usage

Step by step

  1. Define the contract classes for the ContentProvider. (A contract usually class more or less represents a database table.)
  2. Subclass MementoContentProvider, providing implementations for:
    1. SqliteOpenHelper: Opens and creates the database tables. To simplify database table creation, consider using the helper classes SqliteTableBuilder and SqliteColumnBuilder. Advanced users might also create indexes for improved performance via SqliteIndexBuilder.
    2. SqliteUriMatcher: Takes Uris and converts them into SqliteUriMatch objects that the ContentProvider uses whenever an operation (query, insert, update, delete, etc.) occurs.
  3. Create an AndroidManifest entry for the ContentProvider.

Example

An example implementation exists as part of the test suite.

Further reading

The official Android ContentProvider Developer Guide.

Compatibility

The library is compatible and optimized for Android API Level 19 and above.

Plugin SDKs for Locale

Overview

Locale allows developers to create plug-in conditions and settings through the Locale Developer Platform.

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.

pluginHostSdkLib

Creating a Host

The Locale host SDK offers a simplified way to implement an application that can host plug-ins. The host has three primary responsibilities with regards to plug-ins: 1. discovering plug-ins, 2. editing plug-in instances, and 3. executing plug-in instances.

A plug-in app is represented by Plugin. The data that the plug-in saved after editing is represented by PluginInstanceData.

Discover

The PluginRegistry scans for installed plug-ins, which are represented by Plugin. A host UI would most likely interact with the PluginRegistry through the PluginRegistryLoader or SupportPluginRegistryLoader.

Edit

The host's UI displays to the user the Plugins that were discovered, using Plugin.getActivityLabel() and Plugin.getActivityIcon() to get human-readable information about the plug-in.

To edit a Plugin, the host instantiates a subclass of AbstractPluginEditFragment or AbstractSupportPluginEditFragment. AbstractPluginEditFragment takes care of launching the plug-in's "edit" Activity, processing the Activity result, serializing the plug-in's Bundle into a persistent form, and delivering a callback to subclasses via handleSave(PluginInstanceData).

Execute

The host fires PluginInstanceData to a plug-in, via one of the two controller objects Condition or Setting.

PluginInstanceData has getters for getType() and getRegistryName(). Those two getters provide the keys necessary to look up the associated Plugin from the PluginRegistry, enabling the host to instantiate Condition or Setting.

Compatibility

The SDKs are compatible and optimized for Android API Level 19 and above. Note, however, consuming the SDKs requires at least Android Gradle Plugin 1.3.0. (This is due to a manifest placeholder in the AAR's manifest.)

pluginClientSdkLib

This SDK is the middle layer of the Locale Developer Platform.

Although there are multiple ways to approach building a plug-in host or plug-in client, we do not recommend starting with this SDK layer. Instead we strongly recommend starting with the main Locale Developer Platform documentation.

Creating a Plug-in

Fundamentals

A plug-in implementation consists of two things:

  1. Activity: for the ACTION_EDIT_CONDITION or ACTION_EDIT_SETTING Intent action.
  2. BroadcastReceiver: for the 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 again.

Step by Step

  1. Add dependencies to build.gradle as described in the Usage section above.
  2. Architect the contents of the plug-in's EXTRA_BUNDLE. We recommend implementing a "BundleManager" object with static methods to verify the Bundle is correct, generate a new Bundle, and extract values from the Bundle.
  3. Implement the "Edit" Activity:
    1. Subclass AbstractPluginActivity (or AbstractFragmentPluginActivity for android-support-v4 compatibility) and provide implementations for:
      1. isBundleValid(android.os.Bundle): Determines whether a Bundle is valid.
      2. onPostCreateWithPreviousResult(android.os.Bundle previousBundle, java.lang.String previousBlurb): If the user is editing an old instance of the plug-in, this allows the Activity to restore state from that old plug-in configuration.
      3. getResultBundle(): When the Activity is finishing, this method will return the Bundle that represents the plug-in's state. This Bundle will eventually be sent to the BroadcastReceiver when the plug-in is queried.
      4. getResultBlurb(android.os.Bundle bundle): When the Activity is finishing, this method will return a concise, human-readable description of the plug-in's state that may be displayed in the host's UI.
    2. Add the AndroidManifest entry for the Activity. Note: It is very important that plug-in conditions and settings have a stable Activity class name. The package and class names for the edit Activity are a plug-in's public API. If they do not remain consistent, then saved instances of the plug-in created previously will be orphaned. For more information, see Dianne Hackborn's blog post Things That Cannot Change. To make maintaining a stable Activity class name easier, we recommend using an activity-alias for exposing the plug-in's edit Activity. (It is permitted for the plug-in's BroadcastReceiver class name to change.)
      1. Add an Intent filter for ACTION_EDIT_CONDITION or ACTION_EDIT_SETTING Intent action.

      2. Add an Activity icon: This icon will be shown in the host's UI. The ldpi version of the icon should be 27x27 pixels, the mdpi version should be 36x36 pixels, the hdpi version of the icon should be 48x48 pixels, the xhdpi version of the icon should be 72x72 pixels, and the xxhdpi version of the icon should be 108x108 pixels. Note: THIS ICON IS SMALLER THAN THE LAUCHER ICON. Providing a correctly scaled icon will improve performance when the host displays the plug-in's icon.

      3. Add an Activity label: The label is the name that will be displayed in the host's UI.

         <!-- This is the real Activity implementation but it is not exposed directly. -->
         <activity
                 android:name=".ui.activity.PluginActivityImpl"
                 android:exported="false"
                 android:label="@string/plugin_name"
                 android:uiOptions="splitActionBarWhenNarrow"
                 android:windowSoftInputMode="adjustResize">
         </activity>
         <!-- This is the activity-alias, which the host perceives as being the plug-in's Edit Activity.
              This layer of indirection helps ensure the public API for the plug-in is stable.  -->
         <activity-alias
                 android:name=".ui.activity.PluginActivity"
                 android:exported="true"
                 android:icon="@drawable/ic_plugin"
                 android:label="@string/plugin_name"
                 android:targetActivity=".ui.activity.PluginActivityImpl">
             <intent-filter>
                 <!-- For a plug-in setting, use EDIT_SETTING instead. -->
                 <action android:name="com.twofortyfouram.locale.intent.action.EDIT_CONDITION"/>
             </intent-filter>
         </activity-alias>
        
  4. Implement the BroadcastReceiver:
  5. Add the AndroidManifest entry for the BroadcastReceiver
    • Condition: Register the BroadcastReceiver with an Intent-filter for ACTION_QUERY_CONDITION:

            <receiver
                    android:name=".receiver.PluginConditionReceiverImpl"
                    android:exported="true">
                <intent-filter>
                    <action android:name="com.twofortyfouram.locale.intent.action.QUERY_CONDITION"/>
                </intent-filter>
            </receiver>
      
    • Setting: Register the BroadcastReceiver with an Intent-filter for ACTION_FIRE_SETTING:

            <receiver
                    android:name=".receiver.PluginSettingReceiverImpl"
                    android:exported="true">
                <intent-filter>
                    <action android:name="com.twofortyfouram.locale.intent.action.FIRE_SETTING"/>
                </intent-filter>
            </receiver>
      

Compatibility

The library is compatible and optimized for Android API Level 19 and above.

Spackle

Spackle smooths things over and fills in the cracks. This is a hodgepodge of utility classes that we reused several times and bundled them together. We don't anticipate others to rely on this much directly, as our purpose in publishing it was more as an internal dependency for our other open source projects. Put another way, we anticipate more churn in this library.

Compatibility

The library is compatible and optimized for Android API Level 19 and above.

Test

Test implements a variety of classes to fill in gaps in the Android test framework. This library is intended only to be used for test purposes, so should only be used in an androidTestCompile context.

Compatibility

The library is compatible and optimized for Android API Level 19 and above.