Information architecture

Android 8.0 introduced a new information architecture for the Settings app to simplify the way settings are organized and make it easier for users to quickly find settings to customize their Android devices. Android 9 introduced some improvements to provide more Settings functionality and easier implementation.

Examples and source

Most pages in Settings are currently implemented using the new framework. A good example is DisplaySettings: packages/apps/Settings/src/com/android/settings/DisplaySettings.java

Files paths for important components are listed below:

  • CategoryKey: packages/SettingsLib/src/com/android/settingslib/drawer/CategoryKey.java
  • DashboardFragmentRegistry: packages/apps/Settings/src/com/android/settings/dashboard/DashboardFragmentRegistry.java
  • DashboardFragment: packages/apps/Settings/src/com/android/settings/dashboard/DashboardFragment.java
  • AbstractPreferenceController: frameworks/base/packages/SettingsLib/src/com/android/settingslib/core/AbstractPreferenceController.java
  • BasePreferenceController (introduced in Android 9): packages/apps/Settings/src/com/android/settings/core/BasePreferenceController.java

Implementation

Device manufacturers are encouraged to adapt the existing Settings information architecture and insert additional settings pages as needed to accommodate partner-specific features. Moving preferences from legacy page (implemented as SettingsPreferencePage) to a new page (implemented using DashboardFragment) can be complicated. The preference from the legacy page is likely not implemented with a PreferenceController.

So when moving a preferences from a legacy page to a new page, you need to create a PreferenceController and move the code into the controller before instantiating it in the new DashboardFragment. The APIs that PreferenceController requires are described in their name and documented in Javadoc.

It is highly recommended to add a unit test for each PreferenceController. If the change is submitted to AOSP, then a unit test is required. To get more information about how to write Robolectric based tests, see the readme file packages/apps/Settings/tests/robotests/README.md.

Plugin-style information architecture

Each settings item is implemented as a Preference. A Preference can easily be moved from one page to another.

To make it easier for multiple settings to be moved around, Android 8.0 introduced a plugin-style host fragment that contains settings items. Settings items are modeled as plugin-style controllers. Hence, a settings page is constructed by a single host fragment and multiple setting controllers.

DashboardFragment

DashboardFragment is the host of plugin-style preference controllers. The fragment inherits from PreferenceFragment and has hooks to expand and update both static preference lists and dynamic preference lists.

Static preferences

A static preference list is defined in XML using the <Preference> tag. A DashboardFragment implementation uses the getPreferenceScreenResId() method to define which XML file contains the static list of preferences to display.

Dynamic preferences

A dynamic item represents a tile with intent, leading to an external or internal Activity. Usually, the intent leads to a different setting page. For example, the "Google" setting item in the Settings homepage is a dynamic item. Dynamic items are defined in AndroidManifest (discussed below) and loaded through a FeatureProvider (defined as DashboardFeatureProvider).

Dynamic settings are more heavyweight than statically configured settings, so normally developers should implement the setting as a static one. However the dynamic setting can be useful when any of the following is true:

  • The setting is not directly implemented in the Settings app (such as injecting a setting implemented by OEM/Carrier apps).
  • The setting should appear on the Settings homepage.
  • You already have an Activity for the setting and do not want to implement the extra static config.

To configure an Activity as a dynamic setting, do the following:

  • Mark the activity as a dynamic setting by adding an intent-filter to the activity.
  • Tell the Settings app which category it belongs to. The category is a constant, defined in CategoryKey.
  • Optional: Add summary text when the setting is displayed.

Here is an example taken from Settings app for DisplaySettings.

<activity android:name="Settings$DisplaySettingsActivity"
                   android:label="@string/display_settings"
                   android:icon="@drawable/ic_settings_display">
             <!-- Mark the activity as a dynamic setting -->
              <intent-filter>
                     <action android:name="com.android.settings.action.IA_SETTINGS" />
              </intent-filter>
             <!-- Tell Settings app which category it belongs to -->
              <meta-data android:name="com.android.settings.category"
                     android:value="com.android.settings.category.ia.homepage" />
             <!-- Add a summary text when the setting is displayed -->
              <meta-data android:name="com.android.settings.summary"
                     android:resource="@string/display_dashboard_summary"/>
             </activity>

At render time, the fragment will ask for a list of Preferences from both static XML and dynamic settings defined in AndroidManifest. Whether the PreferenceControllers are defined in Java code or in XML, DashboardFragment manages the handling logic of each setting through PreferenceController (discussed below). Then they are displayed in the UI as a mixed list.

PreferenceController

There are differences between implementing PreferenceController in Android 9 and Android 8.x, as described in this section.

PreferenceController in Android 9 release

A PreferenceController contains all logic to interact with the preference, including displaying, updating, search indexing, etc.

The interface of PreferenceController is defined as BasePreferenceController. For example, see code in packages/apps/Settings/src/com/android/settings/core/ BasePreferenceController.java

There are several subclasses of BasePreferenceController, each mapping to a specific UI style that the Settings app supports by default. For example, TogglePreferenceController has an API that directly maps to how the user should interact with a toggle-based preference UI.

BasePreferenceController has APIs like getAvailabilityStatus(), displayPreference(), handlePreferenceTreeClicked(), etc. Detailed documentation for each API is in the interface class.

A restriction on implementing BasePreferenceController (and its subclasses such as TogglePreferenceController) is that the constructor signature must match either of the following:

  • public MyController(Context context, String key) {}
  • public MyController(Context context) {}

While installing a preference to the fragment, dashboard provides a method to attach a PreferenceController before display time. At install time, the controller is wired up to the fragment so all future relevant events are sent to the controller.

DashboardFragment keeps a list of PreferenceControllers in the screen. At the fragment's onCreate(), all controllers are invoked for the getAvailabilityStatus() method, and if it returns true, displayPreference() is invoked to process display logic. getAvailabilityStatus() is also important to tell the Settings framework which items are available during search.

PreferenceController in Android 8.x releases

A PreferenceController contains all logic to interact with the preference, including displaying, updating, search indexing. etc.

Corresponding to the preference interactions, the interface of PreferenceController has APIs isAvailable(), displayPreference(), handlePreferenceTreeClicked() etc. Detailed documentation on each API can be found in the interface class.

While installing a preference to the fragment, dashboard provides a method to attach a PreferenceController before display time. At install time, the controller is wired up to the fragment so all future relevant events are sent to the controller.

DashboardFragment keeps a list of PreferenceControllers in the screen. At the fragment's onCreate(), all controllers are invoked for the isAvailable() method, and if it returns true, displayPreference() is invoked to process display logic.

Using DashboardFragment

Moving a preference from page A to B

If the preference is statically listed in the original page's preference XML file, follow the Static move procedure for your Android release below. Otherwise, follow the Dynamic move procedure for your Android release.

Static move in Android 9

  1. Find the preference XML files for the original page and destination page. You can find this information from the page's getPreferenceScreenResId() method.
  2. Remove the preference from the original page's XML.
  3. Add the preference to the destination page's XML.
  4. Remove the PreferenceController for this preference from the original page's Java implementation. Usually it is in createPreferenceControllers(). The controller might be declared in XML directly.

    Note: The preference might not have a PreferenceController.

  5. Instantiate the PreferenceController in the destination page's createPreferenceControllers(). If the PreferenceController is defined in XML in the old page, define it in XML for the new page also.

Dynamic move in Android 9

  1. Find which category the original and destination page hosts. You can find this information in DashboardFragmentRegistry.
  2. Open the AndroidManifest.xml file that contains the setting you need to move and find the Activity entry representing this setting.
  3. Set the activity's metadata value for com.android.settings.category to the new page's category key.

Static move in Android 8.x releases

  1. Find the preference XML files for the original page and destination page.
  2. You can find this information from the page's getPreferenceScreenResId() method.
  3. Remove the preference in the original page's XML.
  4. Add the preference to destination page's XML.
  5. Remove the PreferenceController for this preference in the original page's Java implementation. Usually it's in getPreferenceControllers().
  6. Note: It is possible the preference does not have a PreferenceController.

  7. Instantiate the PreferenceController in the destination page's getPreferenceControllers().

Dynamic move in Android 8.x releases

  1. Find which category the original and destination page hosts. You can find this information in DashboardFragmentRegistry.
  2. Open the AndroidManifest.xml file that contains the setting you need to move and find the Activity entry representing this setting.
  3. Change the activity's metadata value for com.android.settings.category, set the value point to the new page's category key.

Creating a new preference in a page

If the preference is statically listed in the original page's preference XML file, follow the static procedure below. Otherwise follow the dynamic procedure.

Creating a static preference

  1. Find the preference XML files for the page. You can find this information from the page's getPreferenceScreenResId() method.
  2. Add a new Preference item in the XML. Make sure it has a unique android:key.
  3. Define a PreferenceController for this preference in the page's getPreferenceControllers() method.
    • In Android 8.x and optionally in Android 9, instantiate a PreferenceController for this preference in the page’s createPreferenceControllers() method.

      If this preference already existed in other places, it’s possible there is already a PreferenceController for it. You can reuse the PreferenceController without building a new one.

    • Starting in Android 9, you can choose to declare the PreferenceController in XML next to the preference. For example:
      <Preference
              android:key="reset_dashboard"
              android:title="@string/reset_dashboard_title"
              settings:controller="com.android.settings.system.ResetPreferenceController"/>
      

Creating a dynamic preference

  1. Find which category the original and destination page hosts. You can find this information in DashboardFragmentRegistry.
  2. Create a new Activity in AndroidManifest
  3. Add necessary metadata to the new Activity to define the setting. Set the metadata value for com.android.settings.category to the same value defined in step 1.

Create a new page

  1. Create a new fragment, inheriting from DashboardFragment.
  2. Define its category in DashboardFragmentRegistry.

    Note: This step is optional. If you do not need any dynamic preferences in this page, you don't need to provide a category key.

  3. Follow the steps for adding the settings needed for this page. For more information, see the Implementation section.

Validation

  • Run the robolectric tests in Settings. All existing and new tests should pass.
  • Build and install Settings, then manually open the page being modified. The page should update immediately.