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
PreferenceController
s 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
PreferenceController
s 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.
Use DashboardFragment
Move 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
- Find the preference XML files for the original page and destination
page. You can find this information from the page's
getPreferenceScreenResId()
method. - Remove the preference from the original page's XML.
- Add the preference to the destination page's XML.
- Remove the
PreferenceController
for this preference from the original page's Java implementation. Usually it is increatePreferenceControllers()
. The controller might be declared in XML directly.Note: The preference might not have a
PreferenceController
. - Instantiate the
PreferenceController
in the destination page'screatePreferenceControllers()
. If thePreferenceController
is defined in XML in the old page, define it in XML for the new page also.
Dynamic move in Android 9
- Find which category the original and destination page hosts. You can
find this information in
DashboardFragmentRegistry
. - Open the
AndroidManifest.xml
file that contains the setting you need to move and find the Activity entry representing this setting. - 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
- Find the preference XML files for the original page and destination page. You can find this information from the page's
- Remove the preference in the original page's XML.
- Add the preference to destination page's XML.
- Remove the
PreferenceController
for this preference in the original page's Java implementation. Usually it's ingetPreferenceControllers()
. - Instantiate the
PreferenceController
in the destination page'sgetPreferenceControllers()
.
getPreferenceScreenResId()
method.
Note: It's possible the preference doesn't have a
PreferenceController
.
Dynamic move in Android 8.x releases
- Find which category the original and destination page hosts. You can find
this information in
DashboardFragmentRegistry
. - Open the
AndroidManifest.xml
file that contains the setting you need to move and find the Activity entry representing this setting. - Change the activity's metadata value for
com.android.settings.category
, set the value point to the new page's category key.
Create 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.
Create a static preference
- Find the preference XML files for the page. You can find this information from the page's getPreferenceScreenResId() method.
- Add a new Preference item in the XML. Make sure it has a unique
android:key
. -
Define a
PreferenceController
for this preference in the page'sgetPreferenceControllers()
method.- In Android 8.x and optionally in Android 9,
instantiate a
PreferenceController
for this preference in the page'screatePreferenceControllers()
method.If this preference already existed in other places, it's possible there is already a
PreferenceController
for it. You can reuse thePreferenceController
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"/>
- In Android 8.x and optionally in Android 9,
instantiate a
Create a dynamic preference
- Find which category the original and destination page hosts. You can find
this information in
DashboardFragmentRegistry
. - Create a new Activity in
AndroidManifest
- 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
- Create a new fragment, inheriting from
DashboardFragment
. - 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.
- 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.