Monday, August 21, 2023

Navigating User Spaces in AOSP System Development

The Android Open Source Project (AOSP) forms the bedrock of the world's most popular mobile operating system. It provides a robust, open platform that manufacturers and developers leverage to create a diverse ecosystem of devices. A core, yet often complex, feature of this platform is its multi-user architecture. This capability transforms a single physical device into a collection of separate, sandboxed environments, allowing multiple individuals to share a tablet or phone without compromising their private data, applications, and settings. For developers of standard applications, this architecture is mostly transparent. For those working on AOSP system applications, however, understanding and correctly interacting with this multi-user framework is not just beneficial—it is absolutely essential for creating stable, secure, and feature-rich system components.

System applications operate with elevated privileges and are responsible for the core functionality of the device. They may need to manage settings, perform updates, or provide services that are aware of, and can interact with, different user profiles. This necessity introduces significant challenges. How does a system service running as the primary user know which user is currently active in the foreground? How can it securely access or modify data that belongs to a different user profile without violating the fundamental principles of data isolation? This document delves into the intricacies of the Android multi-user model specifically from the perspective of an AOSP system app developer. We will move beyond superficial examples to explore the foundational concepts, critical APIs, and robust design patterns required to manage and access data across different user spaces effectively and securely.

Throughout this exploration, we will cover the following key areas in depth:

  • The Android Multi-User Architecture: A detailed look at the different user types, the file system-level separation, and how the system manages application instances on a per-user basis.
  • Identifying User States: Examining the roles of UserManager and ActivityManager to accurately determine the current foreground user, background users, and the overall state of all profiles on the device.
  • Secure Data Interaction: Moving beyond fragile path manipulation to demonstrate the canonical methods for data access, including creating user-specific Context objects and leveraging ContentProviders for structured data exchange.
  • Permissions and Security: A critical analysis of the permissions, such as INTERACT_ACROSS_USERS, that govern cross-user operations, and the security best practices that must be followed.
  • Advanced System Operations: Exploring how system apps can initiate services, send broadcasts, and start activities on behalf of other users, along with the lifecycle events that signal user switches.

By mastering these concepts, AOSP developers can build sophisticated system applications that fully embrace the collaborative potential of Android devices while upholding the stringent security and privacy guarantees that users expect.

Foundations of the Multi-User Architecture

Before diving into the code, it is crucial to establish a solid understanding of how Android implements its multi-user environment at a conceptual and file-system level. This framework is built on the principle of user isolation, a cornerstone of Linux kernel functionality that Android extends and adapts for its specific needs.

User Types and Roles

Android defines several distinct types of users, each with a specific role and set of restrictions. A system app must be aware of these distinctions to function correctly.

  • System User (User ID 0): This is the first user created when a device is set up. It is a special, non-removable user, often referred to as the primary user or device owner. The system user has unique privileges; for instance, it is the only user that can perform factory resets and manage other users. Many critical system services run continuously as User 0, regardless of which user is in the foreground.
  • Secondary Users: These are full-fledged user profiles created by the system user. Each secondary user has their own set of applications, data, and customizable settings. Their experience is akin to having a separate device.
  • Guest User: A temporary, ephemeral user profile. When a guest session ends, all data associated with it (installed apps, files, accounts) is wiped from the device. This is ideal for lending a device to someone for a short period. Only one guest user can exist at a time.
  • Restricted Profiles: Primarily used on tablets, these are profiles based on the system user's account but with a controlled set of application access and content restrictions. They are useful for parental controls or creating curated demo environments.

Data Isolation at the File System Level

The security of the multi-user model hinges on strict data isolation. Android enforces this at the file system level. An application's private data is stored in a directory structure that is unique to both the application and the user. For a given package, com.example.app, the data directories would be structured as follows:

  • System User (ID 0): /data/user/0/com.example.app/
  • First Secondary User (e.g., ID 10): /data/user/10/com.example.app/
  • Second Secondary User (e.g., ID 11): /data/user/11/com.example.app/

Standard Linux user and group permissions prevent an application running as User 10 from accessing files in the /data/user/0/ or /data/user/11/ directories. This sandboxing is the fundamental mechanism that protects one user's data from another. Similarly, external storage is emulated on a per-user basis. While it might appear as /storage/emulated/0, /storage/emulated/10, etc., the system uses a sophisticated emulation layer to ensure files remain isolated.

System apps, when granted the appropriate permissions, can bypass some of these restrictions. However, directly manipulating these file paths is highly discouraged. It is a brittle approach that can break with future Android updates. The correct method, as we will see, involves using higher-level Android APIs that abstract these file system details.

The Privileged Role of System Applications

Regular third-party applications downloaded from the Play Store are largely oblivious to the multi-user environment. By default, an app is installed for each user who chooses to add it, and each installation has its own sandboxed data. System apps, however, are different. They are part of the core OS and often need to operate across user boundaries.

Manifest Declaration: android:singleUser

A key attribute in the AndroidManifest.xml for a system app is android:singleUser. This boolean flag dictates how the package manager handles the application in a multi-user environment.

  • android:singleUser="false" (Default): The application is "multi-user aware." This means that separate instances of the application data are created for each user. This is the standard behavior.
  • android:singleUser="true": This declares that the application is a "singleton" component that only ever runs as the system user (User 0). It will not be instantiated for any other user, and its data directory will only exist under /data/user/0/. This is common for low-level system services that manage device-wide state and do not have a user-specific component.

Essential Permissions for Cross-User Operations

To perform any action that crosses the user isolation boundary, a system application must hold powerful system-level permissions. These are typically granted only to applications signed with the platform key or installed in a privileged partition. The most important of these is:


<uses-permission android:name="android.permission.INTERACT_ACROSS_USERS" />

This permission is the master key for most cross-user interactions. It allows an app to switch users, start services for other users, and access data through designated APIs. A variant, INTERACT_ACROSS_USERS_FULL, grants the same abilities. Other related permissions include:

  • android.permission.MANAGE_USERS: Allows the app to create, delete, and manage user accounts.
  • android.permission.CREATE_USERS: A subset of MANAGE_USERS, allowing only user creation.

Without these permissions declared in the manifest and granted by the system, any attempt to perform cross-user operations will result in a SecurityException.

Identifying User Identity and State

The first step in any cross-user operation is to identify the target user. A system app might need to know who the current foreground user is, which users are currently running in the background, or get a list of all profiles on the device. Android provides two primary service managers for this purpose: UserManager and ActivityManager.

Using UserManager for Profile Information

The UserManager class is the main entry point for querying information about user profiles. It provides a comprehensive view of the users on the device.


import android.os.UserManager;
import android.content.Context;
import android.os.UserHandle;
import android.content.pm.UserInfo;
import java.util.List;

// Obtain the UserManager service
UserManager userManager = (UserManager) context.getSystemService(Context.USER_SERVICE);

// Get a list of all user profiles on the device
List<UserInfo> users = userManager.getUsers();
for (UserInfo user : users) {
    Log.d(TAG, "User ID: " + user.id + ", Name: " + user.name + ", IsGuest: " + user.isGuest());
}

// Get the UserHandle for the process that is calling this method
int callingUserId = userManager.getUserHandle();
Log.d(TAG, "This code is running as user: " + callingUserId);

The UserInfo object is particularly useful as it contains rich information about each user, including their name, flags (e.g., admin, guest), and creation time. However, UserManager on its own does not reliably tell you which user is currently active in the foreground.

Using ActivityManager for the Active User

To determine which user is currently in the foreground (i.e., whose UI is visible on the screen), the ActivityManager service is the authoritative source. While many of its methods are deprecated for third-party apps, they remain vital for system components.


import android.app.ActivityManager;
import android.content.Context;

// Obtain the ActivityManager service
ActivityManager activityManager = (ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);

// Get the user ID of the current foreground user.
// This requires the INTERACT_ACROSS_USERS permission.
int currentForegroundUserId = ActivityManager.getCurrentUser();

Log.d(TAG, "The active foreground user is: " + currentForegroundUserId);

The static method ActivityManager.getCurrentUser() is the most direct and reliable way to get the ID of the user who is currently interacting with the device. It's crucial to remember that a system service running as User 0 will see its own user ID from UserManager.getUserHandle(), but ActivityManager.getCurrentUser() will return the ID of the user in the foreground, which could be 0, 10, 11, or any other valid user.

Core Techniques for Cross-User Data Access

With the ability to identify the target user, the next challenge is to access their data. As mentioned earlier, directly constructing file paths is fragile and breaks encapsulation. The Android framework provides robust, context-aware mechanisms for this purpose.

The Flawed Approach: Direct Path Manipulation

The original text demonstrated an approach of building file paths manually. Let's re-examine this and understand its pitfalls.


// --- THIS IS A BRITTLE AND DISCOURAGED METHOD ---
int targetUserId = 10;
File externalStorageDir = Environment.getExternalStorageDirectory(); // Deprecated
File userData = new File(externalStorageDir, "user/" + targetUserId); // This path is not guaranteed
// --- DO NOT USE THIS IN PRODUCTION CODE ---

This approach has several critical flaws:

  1. Unstable Paths: The underlying file system structure for multi-user storage is an implementation detail. While it has been /data/user/<id> and /storage/emulated/<id>, Google could change this in a future Android version, which would instantly break any code that hardcodes this structure.
  2. Scoped Storage: Modern Android versions heavily enforce Scoped Storage, which restricts direct file path access to external storage. System apps may have exemptions, but relying on them is poor practice.
  3. API Deprecation: Methods like Environment.getExternalStorageDirectory() are deprecated precisely because they encourage direct path manipulation.

The Canonical Method: Creating a User-Specific Context

The correct and future-proof way to operate on another user's behalf is to obtain a Context object that is configured for that specific user. The Context class is the gateway to application-specific resources and directories, and creating one for another user allows you to access their sandboxed environment cleanly.

The key API for this is Context.createPackageContextAsUser().


import android.content.Context;
import android.os.UserHandle;
import android.content.pm.PackageManager;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;

public void writeDataForUser(Context systemContext, String targetPackage, int targetUserId, String fileName, String data) {
    try {
        // Create a UserHandle object for the target user.
        UserHandle targetUserHandle = UserHandle.of(targetUserId);

        // Get the Context for your own package, but operating as the target user.
        // The CONTEXT_IGNORE_SECURITY flag can be used by system apps to bypass some checks.
        Context targetUserContext = systemContext.createPackageContextAsUser(
            targetPackage, 
            Context.CONTEXT_IGNORE_SECURITY, 
            targetUserHandle);

        // Now, use this new context to get the correct, isolated file directory.
        File userSpecificFilesDir = targetUserContext.getFilesDir();
        if (userSpecificFilesDir == null) {
            Log.e(TAG, "Could not get files directory for user " + targetUserId);
            return;
        }

        File dataFile = new File(userSpecificFilesDir, fileName);

        // Write data to the file within the target user's sandbox.
        try (FileOutputStream fos = new FileOutputStream(dataFile)) {
            fos.write(data.getBytes());
            Log.d(TAG, "Successfully wrote to: " + dataFile.getAbsolutePath());
        } catch (IOException e) {
            Log.e(TAG, "Failed to write data for user " + targetUserId, e);
        }

    } catch (PackageManager.NameNotFoundException e) {
        Log.e(TAG, "Package not found: " + targetPackage, e);
    }
}

This method is vastly superior. It doesn't make any assumptions about file paths. It asks the Android framework for the correct directory (targetUserContext.getFilesDir()), and the system provides the correct, isolated path for that specific user. This code will continue to work even if the underlying file system structure changes in future Android versions.

Structured Access with ContentProviders

For more complex data sharing, direct file access is often not the best model. A ContentProvider offers a structured, secure, and abstract API for data access, similar to a database. A system app can implement a content provider that manages data and make it accessible to different users.

When defining your provider in the manifest, you can set android:multiprocess="true" to ensure a single instance of the provider handles requests from all users. Your provider's implementation of query(), insert(), update(), and delete() can then use getCallingUserHandle() to identify which user is making the request and apply the appropriate logic and data isolation within the provider's data store.

This pattern is extremely powerful for managing centralized, device-wide settings or databases where each user has their own view or subset of the data.

Advanced Cross-User System Operations

Beyond data access, system apps often need to perform actions on behalf of other users, such as starting a service or sending a broadcast. The framework provides user-specific variants of these common IPC mechanisms.

Interacting with Components: Services and Broadcasts

Let's say a system service running as User 0 detects a new network configuration and needs to notify an agent application running for the current foreground user.


import android.content.Intent;
import android.os.UserHandle;
import android.app.ActivityManager;

public void sendNotificationToCurrentUser(Context context, String message) {
    // Get the current foreground user
    int currentUserId = ActivityManager.getCurrentUser();
    UserHandle currentUserHandle = UserHandle.of(currentUserId);

    // Create the intent for the broadcast receiver
    Intent intent = new Intent("com.example.system.NETWORK_UPDATE");
    intent.putExtra("message", message);
    // It is a good practice to specify the package to ensure the broadcast
    // is only received by your intended application.
    intent.setPackage("com.example.agentapp");

    // Send the broadcast specifically to the current user.
    // This requires the INTERACT_ACROSS_USERS permission.
    context.sendBroadcastAsUser(intent, currentUserHandle);
    
    Log.d(TAG, "Sent broadcast to user " + currentUserId);
}

// Similarly, to start a service for a specific user:
// Intent serviceIntent = new Intent(context, UserSpecificService.class);
// context.startServiceAsUser(serviceIntent, targetUserHandle);

These `AsUser` methods are the cornerstone of cross-user component interaction. They ensure that the intent is delivered to the correct instance of the application running within the target user's profile.

Responding to User State Changes

A robust system app must be able to react dynamically as the user state changes. The system broadcasts several protected intents to signal these events. A system app can register a BroadcastReceiver to listen for them.

  • Intent.ACTION_USER_SWITCHED: Sent after a user switch has completed. The intent data contains the new user's ID. This is the primary signal to update UI or services for the new foreground user.
  • Intent.ACTION_USER_BACKGROUND: Sent when a user is moved to the background.
  • Intent.ACTION_USER_FOREGROUND: Sent when a user is brought to the foreground.
  • Intent.ACTION_USER_ADDED / Intent.ACTION_USER_REMOVED: Sent when users are created or deleted.

By listening for ACTION_USER_SWITCHED, a system service can re-query ActivityManager.getCurrentUser() and re-orient its operations to serve the new active user.

Conclusion: Responsibility and Best Practices

Developing system applications for Android's multi-user environment is a task that carries significant responsibility. The ability to operate across user boundaries grants immense power but also demands a rigorous approach to security, stability, and privacy. Direct file manipulation and hardcoded assumptions are paths to fragile, insecure code.

The core principles for successful AOSP multi-user development are clear:

  1. Prioritize High-Level APIs: Always prefer framework APIs like Context.createPackageContextAsUser(), sendBroadcastAsUser(), and ContentProviders over low-level file operations. These APIs provide a stable abstraction layer that protects your application from future platform changes.
  2. Understand the User Context: Be deliberate about which user your code is running as and which user it is targeting. Use ActivityManager.getCurrentUser() to identify the active user and UserHandle objects to specify your target.
  3. Declare and Check Permissions: Ensure your AndroidManifest.xml correctly declares all necessary permissions, such as INTERACT_ACROSS_USERS. Your code should be prepared to handle SecurityExceptions gracefully.
  4. Think in Terms of Isolation: Treat each user's data as a separate, protected sandbox. Every interaction that crosses a user boundary is a privileged operation and should be designed with security as the paramount concern.
  5. Test Thoroughly: Multi-user bugs can be subtle. Test your application's behavior during user creation, switching, and deletion. Use ADB commands (e.g., am switch-user) to simulate these scenarios rigorously.

By adhering to these guidelines, developers working within AOSP can build powerful system features that seamlessly and securely integrate with the multi-user architecture, ultimately providing a more flexible and robust experience for everyone sharing a device.


0 개의 댓글:

Post a Comment