User preferences architecture and implementation guide

This page provides a guide on the architecture of the SDV User Preferences system and instructions for implementing a user-controllable service and client.

Architecture overview

The User Preferences system decouples the storage and management of user settings from the enforcement and application of those settings. The key architectural terms are summarized in the table:

FeatureDescriptionRoleResponsibility
User-controllable service A standard SDV service bundle that manages a specific domain (for example, HVAC, car seats, audio) Provides the intended state of the capabilities and constraints of the hardware or subsystem it controls.
  • Registration: Informs the user preferences agent about the settings it supports (metadata, defaults, constraints).
  • Enforcement and autonomy: Receives requests to change settings, validates them against current state and safety rules, and applies them to the hardware. Maintains full autonomy and specifies whether a setting change is accepted or rejected.
User preferences agent The central orchestrator Provides centralized storage, user profile management, and notification hub.
  • Storage: Persists settings per user (for example, driver, passenger).
  • Routing: Proxies change requests from clients (for example, human machine interface (HMI)) to the appropriate user controllable service.
  • Notifications: Broadcasts changes to interested parties (views or HMIs) using the ChangeNotifier interface.
  • User management: Handles user switching and applying the correct persistent state to all registered services.
User preferences client An app or service that provides a user interface, for example, an IVI app with an HMI or other logic that needs to interact with user preferences Interacts with users, displays settings, and initiates change requests.
  • Display: Presents current settings and constraints to the user.
  • Request changes: Sends user-initiated setting modifications to the user preferences agent.
  • Receive notifications: Subscribes to and reacts to real-time updates of settings.

Implement a user-controllable service

To inform the User Preferences agent which keys exist, their data types, default values, and constraints (for example, minimum and maximum values), your service bundle must interact with the UserPreferencesRegistryService interface. When your service starts, it must register the settings it exposes. The agent calls RequestSettingsChange on your service when a user tries to modify a setting (or when switching users).

Follow the steps in this section to let User Preferences (and a user with HMI) control your service.

  1. Define service interfaces in the VSIDL file:

    • As a server, implement com.sdv.google.user_preferences.user_controllable.UserControllableService. This lets the agent send you change requests.
    • As a client, consume com.sdv.google.user_preferences.UserPreferencesRegistryService. This registers your settings on startup.

    The following example shows service_bundle.vsidl for a user-controllable service:

    service_bundle {
        name: "MyFeatureService"
        server {
            service: "com.sdv.google.user_preferences.user_controllable.UserControllableService"
        }
        client {
            service: "com.sdv.google.user_preferences.UserPreferencesRegistryService"
        }
    }
    
  2. Register settings on startup in the key proto user_preferences_registry_service.proto:

    1. Connect to UserPreferencesRegistryService.
    2. Create an instance of RegisterSettingsRequest.
    3. Define an instance of SettingsGroup (a logical collection of settings).
    4. For each setting, define:

      *   **Key:** Unique string ID (for example, `TEMPERATURE`)
      *   **Kind:** `PER_USER` (stored per user profile) or `SHARED`
          (global)
      *   **Default value:** Initial value if no user preference exists
      *   **Constraints:** (optional) Validation rules (for example, Min
          16, Max 32 for HVAC).
      
    5. Call RegisterSettings().

    The following example is in conceptual Rust:

    let temperature_setting = SettingDefinition {
        name: "TEMPERATURE".to_string(),
        kind: SettingKind::PER_USER.into(),
        default_value: Value::Int64(22), // Default 22 degrees
        constraint: Some(Constraints::Int64Constraints(Int64Constraints {
            min_value: Some(16),
            max_value: Some(32),
            ..Default::default()
        })),
        ..Default::default()
    };
    
    registry_client.RegisterSettings(&RegisterSettingsRequest {
        group_name: "HVAC".to_string(),
        version: "1.0".to_string(),
        settings_definitions: vec![temperature_setting],
    }).await?;
    
  3. Handle settings change requests in user_controllable_service.proto:

    1. Implement the RequestSettingsChange RPC.
    2. Validate if the requested values are valid within the current context (for example, if the hardware is ready).
    3. Apply specific logic to apply the change (for example, move the seat, change the fan speed).
    4. Return the applied values in RequestSettingsChangeResponse:
    5. If you accepted the change, return the new value.
    6. If you rejected or clamped the value, return the value you set (or kept).

    The following example is in conceptual Rust:

    async fn RequestSettingsChange(
        &self,
        _caller_id: ServiceFqin,
        request: &RequestSettingsChangeRequest
    ) -> SdvResult<RequestSettingsChangeResponse> {
        let mut applied_settings = Vec::new();
    
        for setting in &request.settings {
            if self.hardware.set_value(setting.key, setting.value).is_ok() {
                // Change accepted
                applied_settings.push(setting.clone());
            } else {
                // Change rejected, return current actual value
                let current_val = self.hardware.get_value(setting.key);
                applied_settings.push(create_setting(setting.key, current_val));
            }
        }
    
        Ok(RequestSettingsChangeResponse {
            settings: applied_settings,
        })
    }
    
  4. Implement the FactoryReset RPC to revert your subsystem to a clean state:

    1. Reset all settings to their default values (as defined in your code configuration).
    2. Call UpdateSettings on the registry service to inform the agent that the values have changed externally (by the reset, not by a user request).

Backward compatibility and schema evolution

The schema of the settings defined by a user-controllable service is considered a public interface. This interface is consumed by the user preferences agent and various clients (for example, HMIs), which might have different release schedules. Therefore, maintaining strict backward compatibility is essential to prevent system instability and client breakage.

Compatible changes

A user-controllable service should introduce only compatible changes to its settings schema. These changes ensure that older clients continue to operate correctly without updates:

  • Add a new, optional setting with a reasonable default value:

    • This setting is optional, so clients must check for its presence to support different versions of the service.
    • Clients ignore this setting if they haven't been updated to recognize it.
    • If no user preference for the new setting exists, the agent uses the provided default value.

Incompatible changes

The following changes are considered breaking and are prohibited as they immediately compromise older clients:

  • Remove an existing setting.

  • Change the meaning, unit, or data type of an existing setting (for example, changing from an integer representing temperature in Celsius to a float representing pressure in Pascal).

  • Change the default value of an existing setting. The primary reason for prohibiting default value changes is the potential complication with persisted data migration. The agent stores settings based on the service's registration schema. Changing a default value would require complex logic to migrate all existing user profiles to the new default or risk using a technically incorrect value for users who never explicitly set the preference.

Handle required breaking changes

If a breaking change is required, the service must not modify the existing setting. The approach to manage this transition while maintaining compatibility is primarily located in the user-controllable service logic:

  1. Create a new setting with the desired new definition, key, or unit.
  2. Deprecate the existing setting by registering it for compatibility. Use this new setting when you develop new clients.
  3. Advise new clients to implement compatibility logic within the User-controllable service's business logic.

The service's RequestSettingsChange implementation must ensure that a change to one setting is reflected in its deprecated counterpart, and a change to the counterpart is reflected in the setting.

Example compatibility logic:

  1. A service deprecates the old setting TEMPERATURE_C (Celsius) and introduces TEMPERATURE_K (Kelvin).

  2. When the agent sends a request to update the new setting:

    • The service receives a request for TEMPERATURE_K (for example, 295.15 K).

    • The service's logic converts this to Celsius (22°C) and internally updates and persists both values to maintain consistency for legacy clients.

  3. When the agent sends a request for the deprecated setting from a legacy client:

    • The service receives a request for TEMPERATURE_C (for example, 24°C).
    • The service converts this to Kelvin (297.15 K) and internally updates and persists both values.

This dual-write strategy ensures that all clients, regardless of their release schedule, read consistent and accurate data, avoiding breakage caused by external release mismatches.

Implement a client

To implement a client (for example, an HMI) that interacts with the User Preferences agent:

  1. Define your client service bundle to interact with specific User Preferences interfaces. In your VSIDL file:

    • As a server, implement com.sdv.google.user_preferences.view.ChangeNotifier to enable the agent to send your client real-time notifications about settings changes.
    • As a client, use com.sdv.google.user_preferences.UserPreferencesManagementService to request settings modifications and subscribe to updates.
    • As a client, use com.sdv.google.user_preferences.UserPreferencesAdminService to manage user profiles (for example, create, select, delete, factory reset).

    The following example shows a service_bundle.vsidl file for a client:

    service_bundle {
        name: "MyHmiClient"
        server {
            service: "com.sdv.google.user_preferences.view.ChangeNotifier"
        }
        client {
            service: "com.sdv.google.user_preferences.UserPreferencesManagementService"
        }
        client {
            service: "com.sdv.google.user_preferences.UserPreferencesAdminService"
        }
    }
    

    Add authorization policies accordingly. To allow communication for the user preferences agent, create a client for the specified servers. For example:

    server {
        service: "com.sdv.google.user_preferences.view.ChangeNotifier"
        allow_all_channels: true
    }
    client {
        service: "com.sdv.google.user_preferences.UserPreferencesManagementService"
        allow_all_channels: true
    }
    client {
        service: "com.sdv.google.user_preferences.UserPreferencesAdminService"
        allow_all_channels: true
    }
    
  2. You can change request settings in UserPreferencesManagementService:

    1. Connect to UserPreferencesManagementService.
    2. Create an instance of RequestSettingsChangeRequest, specifying the SettingsGroupId and the desired settings.
    3. Optional. Set the ChangePersistencePolicy to PERSISTENT_CHANGE (default) or NON_PERSISTENT_CHANGE.
    4. Call RequestSettingsChange().

    The following example is in conceptual Rust:

    management_client.RequestSettingsChange(&RequestSettingsChangeRequest {
        settings_group_id: Some(SettingsGroupId {
            service_fqin: hvac_service_fqin.to_string(),
            name: "HVAC".to_string(),
            ..Default::default()
        }).into(),
        settings: vec![Setting {
            key: "TEMPERATURE".to_string(),
            value: Some(Value::Int64(24)),
            ..Default::default()
        }],
        change_persistence_policy: ChangePersistencePolicy::PERSISTENT_CHANGE.into(),
        ..Default::default()
    }).await?;
    
  3. Subscribe to settings changes with user_preferences_management_service.proto and change_notifier.proto:

    1. Implement the OnSettingsChange RPC from the ChangeNotifier interface within your service bundle. The User Preferences agent invokes this method when a change occurs.
    2. Connect to UserPreferencesManagementService.
    3. Create a SubscribeToSettingsChangeAndGetSettingsRequest specifying the SettingsGroupId you wish to monitor.
    4. To return the state of the settings and then send future updates through your OnSettingsChange implementation, call SubscribeToSettingsChangeAndGetSettings().

    The following example of implementing the ChangeNotifier interface is in conceptual Rust:

    #[async_trait]
    impl ChangeNotifier for MyHmiServiceImpl {
        async fn OnSettingsChange(
            &self,
            _caller_id: ServiceFqin,
            request: &OnSettingsChangeRequest,
        ) -> SdvResult<OnSettingsChangeResponse> {
            // Process the active_settings, pending_changes, and persisted_settings
            // Update your UI or internal state accordingly.
            info!("Received settings change for group: {}", request.settings_group_id.name);
            // ...
            Ok(OnSettingsChangeResponse::new())
        }
    }
    

    The following example of subscribing is in conceptual Rust:

    management_client.SubscribeToSettingsChangeAndGetSettings(&SubscribeToSettingsChangeAndGetSettingsRequest {
        settings_group_id: Some(SettingsGroupId {
            service_fqin: hvac_service_fqin.to_string(),
            name: "HVAC".to_string(),
            ..Default::default()
        }).into(),
        ..Default::default()
    }).await?;
    
  4. Optional: Clients that manage user profiles (for example, a dedicated settings app) can use user_preferences_admin_service.proto to interact with the admin service:

    1. Connect to UserPreferencesAdminService.
    2. Use RPCs such as CreateUser, SelectUser, DeleteUser, FactoryReset, and ListUsers to manage user profiles.

    The following example of creating a user is in conceptual Rust:

    admin_service_client.CreateUser(&CreateUserRequest {
        user: Some(User {
            id: 1,
            flags: UserFlags::DRIVER.value(),
            ..Default::default()
        }).into(),
        ..Default::default()
    }).await?;
    
  5. Discover available settings and users through the admin service with user_preferences_admin_service.proto:

    1. Get a list of all registered users by calling ListUsers() on the UserPreferencesAdminService.
    2. Get the settings for a specific user by calling GetUserSettings(user_id) on the UserPreferencesAdminService.

    The following example of listing users and getting settings is in conceptual Rust:

    // List all users
    let list_users_response = admin_service_client.ListUsers(&ListUsersRequest::new()).await?;
    info!("Available users: {:?}", list_users_response.users);
    
    // Get settings for a specific user (for example, user with ID 1)
    if let Some(user_id) = list_users_response.users.first().map(|u| u.id) {
        let get_settings_response = admin_service_client.GetUserSettings(&GetUserSettingsRequest {
            user_id,
            ..Default::default()
        }).await?;
        info!("Settings for user {}: {:?}", user_id, get_settings_response.groups);
    }
    

Flow summary

  1. The client starts and connects to the agent's management and administrative services.
  2. The client discovers and subscribes to settings groups and displays the initial state.
  3. The client sends a RequestSettingsChange to the agent.
  4. The agent sends an OnSettingsChange notification to the client with updated settings and state.

For a complete working example, see the HMIService in @samples/user_preferences/v1/.

Implement the agent

An orchestrator-launched service bundle implements the user preferences agent. For convenience, a reference implementation is provided.

In addition, this sample service bundle user_preferences_sample.vsidl file for the agent is provided:

package: "com.sdv.oem.user_preferences"

service_bundle {
    name: "UserPreferencesServiceBundle"
    server {
        service: "com.sdv.google.user_preferences.UserPreferencesAdminService"
    }
    server {
        service: "com.sdv.google.user_preferences.UserPreferencesManagementService"
    }
    server {
        service: "com.sdv.google.user_preferences.UserPreferencesRegistryService"
    }
    client {
        service: "com.sdv.google.user_preferences.view.ChangeNotifier"
    }
    client {
        service: "com.sdv.google.user_preferences.user_controllable.UserControllableService"
    }
}

The implementation can use the generated middleware and should compile into a rust_ffi_shared that is referenced by the service bundle that implements the user preferences agent:

sdv_service_bundle_metadata {
  # This name must match the agent's Bundle Name
  name: "UserPreferencesServiceBundle"
  version_number: 1
  version_name: "1"

  native_library_path: "lib64/<USER-PREFERENCES-FFI-LIB>.so"

  orchestration_config_path: "etc/user_preferences_service_bundle/<USER-PREFERENCES-ORCHESTRATION>.textproto"

  authorization_policy_path: "etc/user_preferences_service_bundle/permissions.textproto"
}

The client's authorization policy .textproto file requires the necessary permissions:

client {
    service: "com.sdv.google.user_preferences.UserPreferencesManagementService"
    allow_all_channels: true
}

client {
    service: "com.sdv.google.user_preferences.UserPreferencesRegistryService"
    allow_all_channels: true
}

client {
    service: "com.sdv.google.user_preferences.UserPreferencesAdminService"
    allow_all_channels: true
}

Appendix: key concepts

This section describes some key concepts.

Users

Each vehicle user (User class) can create an account in which they can store their preferences. User Preferences supports accounts only for vehicle drivers. Guest drivers can create temporary accounts that are automatically deleted after one use.

message User {
  // Required.
  // A unique ID for the user.
  int32 id = 1;

  // Bit flags that define properties of user. Integer values have to be powers of 2 as they are used as
  // a bit mask.
  enum UserFlags {
    // Due to Protobuff requirement to have the first enum option set to 0,
    // Assign 0 to an unused flag
    UNSET = 0x0;
    // Marks the user as vehicle driver
    DRIVER = 0x01;
    // Ephemeral users have non-persistent state, once another user is selected
    // the profile is deleted automatically
    EPHEMERAL = 0x02;
  }

  // Required.
  // Bitmask for the user flags defined above
  int32 flags = 2;
}

Settings groups

Settings groups (SettingsGroup class) organize and manage related settings. Each configurable component within the vehicle defines its settings as groups, consisting of a list of settings represented as key-value pairs. For example, vehicles with electric seats can define a setting group for the driver's seat and another for the front passenger where both groups contain identically named settings.

message SettingsGroup {
  // Required.
  // The identifier for this group.
  SettingsGroupId id = 1;

  // Required.
  // The version number of schema used by this setting group.
  string version = 2;

  // Required.
  // The list of settings within the setting group.
  repeated Setting settings = 3;
}

Settings

The setting, message (Setting class), represents a single setting within SettingsGroup. This message consists of a key, which is the name of the setting and a value, which can be one of several types.

This example shows how a setting is represented with a key and a value:

// A key value pair representing a setting within a UserControllableService SettingsGroup.
message Setting {
  // Required.
  // A name that uniquely identifies a setting within a SettingsGroup.
  string key = 1;

  // Required.
  // New value of the setting.
  oneof value {
    bool bool = 2;
    float float = 3;
    int32 int32 = 4;
    int64 int64 = 5;
    bytes blob = 6;
    int32 enum = 7;
  }
}

Setting definitions

Setting definitions (SettingDefinition class) serve as templates for individual settings within a settings group. They specify the setting's fundamental characteristics, such as whether the setting is shared across all users, specific to each user, or managed externally (passthrough).

Importantly, setting definitions also define any constraints on the setting's value, such as minimum and maximum values, allowable increments, or a restricted set of options. These constraints lead to data integrity and consistency for each setting.

// The definition of a setting, along with its properties such as type and constraints
message SettingDefinition {
  // Required.
  SettingKind kind = 1;

  // Required.
  SettingWithConstraints setting_with_constraints = 2;
}

enum SettingKind {
  // A setting which is applied for all vehicle users.
  SHARED = 0;
  // Store the value of the setting for each vehicle user separately.
  PER_USER = 1;
  // UserControllableService is fully responsible for the storage of PASSTHROUGH setting.
  // User Preferences only notifies UserControllableService when the value is explicitly set by
  // user. User Preferences does not attempt to request changes based on user change or service
  // registration.
  PASSTHROUGH = 2;
};

message SettingWithConstraints {
  // Required
  Setting setting = 1;

  // Required.
  // Defines restrictions on the setting's value
  oneof constraints {
    FloatConstraints float_constraints = 2;
    Int32Constraints int32_constraints = 3;
    Int64Constraints int64_constraints = 4;
    EnumConstraints enum_constraints = 5;
  }
}

message Int32Constraints {
  // The minimum value that a setting can have.
  optional int32 min_value = 1;
  // The maximum value that a setting can have.
  optional int32 max_value = 2;
  // The step by which a setting's value can be increased or decreased.
  optional int32 step = 3;
}

message Int64Constraints {
  // The minimum value that a setting can have.
  optional int64 min_value = 1;
  // The maximum value that a setting can have.
  optional int64 max_value = 2;
  // The step by which a setting's value can be increased or decreased.
  optional int64 step = 3;
}

message FloatConstraints {
  // The minimum value that a setting can have.
  optional float min_value = 1;
  // The maximum value that a setting can have.
  optional float max_value = 2;
  // The step by which a setting's value can be increased or decreased.
  optional float step = 3;
}

message EnumConstraints {
  // Required.
  // List of unique values sorted in ascending order.
  repeated int32 possible_values = 1;
}

Example sequence of events when a user updates a setting

This example illustrates the sequence of events occur when a user uses the Android Automotive OS (AAOS) in-vehicle infotainment (IVI) to update a setting:

Events when user changes
setting

Figure 1. Events when a user changes a setting.

Example sequence of events when a vehicle is starting

This example illustrates how SDV User Preferences applies a user's preferred settings when a vehicle is starting:

Events when vehicle is
starting

Figure 2. Events when a vehicle is starting.