Mastering ActivityManagerService: Unlocking the Hidden Potential of Android for Advanced Features Like Magic Multi-User Support.
We have reached the seventh stage of our journey into the deep, hidden and intricate world of Android. This time we are going to get our hands really dirty.
In fact, the goal of this article is to customize the AOSP project to extend AMS and enable an advanced multi-user functionality in Android. Our goal is to improve the limited implementation developed by Google for tablets.
We want to create a multi-user system that provides an experience with apps like YouTube, Facebook, and Netflix identical to what we enjoy on our smartphones.
Basically we want a system that works like this: when I use it, it behaves like my smartphone, and when you use it, it behaves like your smartphone. At the same time, it must respect all privacy rules and app usage protections.
But before we tackle this very challenging goal, we need to learn more. In particular, we need to understand the process creation mechanism in depth and the role of AMS and the ProcessRecord in this mechanism. So let's get started.
In the previous article we saw that ActivityManagerService (AMS) is the brain of Android's application lifecycle management (1.). It oversees interactions between processes, activities, and system resources by utilizing key data structures such as ActivityRecord, ProcessRecord, and Task. These structures are critical for efficiently managing application behavior and system resources.
In the next article we will explore ActivityRecord and Task in detail, wich are classes responsible for managing the lifecycle of Activities.
In this episode we will dive into the details of ProcessRecord, which is a key element for achieving the goal we’ve set for this session: creating an advanced multi-user Android system.
To better understand AMS and the role of the ProcessRecord class, we will delve deeper into the process creation and management flow, with detailed code snippets. I remind you that you can find the AMS code in these two folders of the AOSP project:
The snippets I’ll show are simplified versions of the AOSP code. The purpose of this simplification is to avoid getting lost in the details of the real implementation and focusing on the process creation flow instead.
As we will see, fully understanding the process creation mechanism will be crucial if we want to achieve our goal of a "magic" multi-user Android system.
So let's begin our in-depth analysis of the process creation flow (all the source code I will show is taken from the Android Open Source Project, version 12.1.0_r11).
Creating a New Process:
When an application requests to start a component (e.g., an Activity or Service), AMS first checks if there is already a process associated with the application. If not, it creates a new process using the ProcessRecord class and the Zygote daemon.
The process of creating a new application process follows these steps:
Step 1. Checking for an Existing Process:
AMS verifies whether a process for the application is already running:
where:
mProcessList is an internal AMS data structure that tracks all active processes.
ProcessRecord represents a single process and holds information like UID, process name, state, and associated components.
Step 2. Creating the ProcessRecord:
If the process doesn’t exist, AMS creates a new ProcessRecord object:
The ProcessRecord class is defined as follows:
Each ProcessRecord contains:
info: is an ApplicationInfo, data parsed from AndroidManifest.xml
processName: The name of the process.
uid: The unique identifier of the process. The uid of the system process is 1000 (Process.SYSTEM_UID), the uid of the application process is allocated from 10000 (Process.FIRST_APPLICATION_UID).
state: The current state of the process (e.g., INITIALIZING).
Step 3. Starting the Process with Zygote:
AMS uses the Zygote daemon to spawn a new process (2.):
Here:
Process.start(): Invokes Zygote to create a new process by forking from the pre-initialized parent process.
ActivityThread: The entry point of the newly created application process.
In Android, ActivityThread is a critical class within the Android framework. Despite its name, it is not a thread itself but a class that plays a central role in managing the main application thread. It acts as the entry point for an application's process and orchestrates the execution of activities, services, and other components in the application.
When a new application process is created by the Android system (via the Zygote process), the ActivityThread is responsible for starting the application. It initializes the application context and sets up the environment for running the app.
The ActivityThread communicates with the Android system's ActivityManagerService (AMS) via the Binder IPC mechanism. AMS sends commands to the ActivityThread to perform tasks like starting activities, services, or broadcasting intents.
The following snippet highlights the key role of ActivityThread in activity launching:
Step 4. Communicating with the Process:
After creating the process, AMS communicates with the new process using Binder to start the requested component (e.g., an Activity).
The ActivityThread in the newly created process receives this command via the main handler.
performLaunchActivity() is called (see snippet above), which creates an instance of the Activity class specified in the Intent and Initializes the Activity by calling the attach() and onCreate() methods.
During onCreate(), the Activity configures its user interface via setContentView().
A window (Window) is created and registered with the WindowManagerService (WMS).
The WMS adds the window to the visual hierarchy and makes it visible.
Managing ProcessRecord Objects:
ProcessRecord objects are continuously monitored and updated by AMS.
The AMS monitors the memory usage of applications and categorizes processes based on importance (e.g., foreground, visible, service, background). When memory becomes scarce, less critical processes are terminated first.
Each process is assigned an "OOM adjustment" score based on its priority. Lower scores represent higher-priority processes.
Here is the code snippet related to what was just explained.
The oom_score_adj is an integer value ranging from -1000 to +1000:
-1000: The process is immune to termination (e.g., critical system processes like init).
+1000: The process is most likely to be killed under memory pressure.
Intermediate values determine the likelihood of termination relative to other processes.
AMS categorizes processes into different priority levels based on their role and visibility, each category is mapped to a specific range of oom_score_adj values:
Persistent System Processes: refers to system processes (e.g. SystemServer, MediaServer) that are critical to the proper functioning of the operating system. These processes are marked as "persistent" to ensure they are given high priority and are not killed by the system, even under severe memory pressure, except in extreme cases like a critical failure or reboot (oom_score_adj: 900 to 1000).
Foreground process: Actively interacting with the user. (e.g., current Activity) (oom_score_adj: 0 to 100).
Visible process: Not in the foreground but visible to the user (e.g., a Service updating the UI) (oom_score_adj: 100 to 200).
Service process: Hosting a background Service performing critical tasks (oom_score_adj: 200 to 500).
Background process: Running but not interacting with the user (oom_score_adj: 500 to 900).
Cached process: No active task but kept in memory for faster relaunch (oom_score_adj: 900 to 1000).
If the system is low on memory, AMS terminates the least important processes:
At the end of this article I will tell you a trick to make a process immortal !! 🙂💪
Let's summarize the flow of starting an app:
An app calls startActivity(Intent).
AMS checks if the process exists: If it doesn’t, AMS creates a new ProcessRecord and starts the process via Zygote.
AMS sends a command to the process to launch the Activity.
The process receives the command via ActivityThread and starts the Activity.
The Activity registers with WMS to be displayed.
After this detailed look at how apps start in Android, we’re ready to discuss the solution I’ve developed for achieving an advanced multi-user functionality in Android. Google’s implementation on tablets has some limits: it only supports a fixed number of users and doesn’t work across multiple devices. For instance, if I used the YouTube app on a tablet, I won't find it in the same state when I open it on another tablet. I am forced to log into the YouTube app on the second tablet as well if I want to update the status.
Before diving into the diagrams and source code, here’s an important tip:
When modifying Android’s source code, try to keep your changes in separate modules and classes as much as possible. If you need to make changes to existing code, it’s better to extend the code rather than rewrite it, so you don’t accidentally break other features. This is important because, while Android’s architecture like the Zygote process for faster app launches and sandboxing for app security is very smart, the AOSP (Android Open Source Project) code quality isn’t always the best. Keeping your changes separate also makes it easier to update your code when new versions of Android come out.
The solution I’m about to show you solves a common problem in contexts where it’s hard to use your personal smartphone. For example, think about gym equipment like a treadmill or cycle. While running, it’s easier to interact with the equipment’s display than your phone. Apps like YouTube or Netflix can be preinstalled, but the problem is ensuring that the user has the same experience as on their own phone. For example, when they open an app, it should be in the same state they left it in during their last workout.
We also need to protect the privacy of the next user. When a workout is finished, all data from the session must be erased. Otherwise, if someone else uses the machine, they might find Netflix or Facebook still logged into the previous user’s account.
Luckily, in gyms with this kind of advanced equipment, users usually authenticate themselves before starting their workout. This is often done with a username entered on the display or through an NFC device like a wristband.
We can use this to create a system in Android that does the following:
When a user launches an app, the system pauses the launch.
It retrieves the user’s saved app state (sandbox) from a server.
It restores the app’s sandbox, then resumes the launch, so the app starts exactly as the user left it.
When the user finishes his session and logs out, the system saves the app states (sandboxes) it used. It encrypts them and uploads them to the server, ready for the next session. This way, if the user switches from a treadmill to a cycle, they can be picked up where they had been left off, even on a different machine.
Let’s look at the practical implementation of what we just described. I call this implementation "Multiuser Extension".
Before we begin, one clarification: when I refer to a "session," I mean the time between the user logging into the equipment and the end of the exercise.
We’ll start with the class diagram and see all the classes involved in this process:
Except for ActivityManagerService and ProcessRecord, which are operating system classes that I slightly modified to adapt to the new behaviors, all the other classes are new additions that I created and added to the AMS package in the Android framework.
CurrentUser is the main class of MultiUser Extension:
It receives notifications when a session starts (Login) and ends (Logout).
It manages the list of apps used during the session (UsedApps) via the RunningPackage object (we’ll explain this mechanism shortly).
It handles retrieving sandboxes from the server and restoring them to the system’s data partition (the /data/data/"packagename" directory).
At the end of a session, it terminates the processes of apps launched during the session, saves their sandboxes to the server, and then deletes them from the local system.
UsedApps is the list of apps used during a session.
Repo represents the sandbox repository, which stores encrypted sandboxes indexed by the combination of the app’s package name and the user ID. In my implementation, this is an internet-based server.
AMS (Activity Manager Service) connects these new logics and behaviors to the app process creation flow. It also implements a new interface, ProcessKiller, which allows CurrentUser to terminate all apps in the UsedApps list when the session ends.
ProcessRecord, as we discussed in detail in the first part of this article, is the object created by AMS whenever a new app starts (when a new process is created).
RunningPackages is an object created by UserClient and passed as a static reference to ProcessRecord, enabling apps launched during the session to be added to the UsedApps list.
Now, let’s look at the four main sequences in this implementation:
Multiuser Extension Startup.
Start of a session (Login).
End of a session (Logout).
Launching an app during a session.
The Multiuser Extension startup sequence begins with the SystemServer. In the previous article, we explained that SystemServer creates all system services by calling the methods startBootstrapServices, startCoreServices, and startOtherServices in sequence.
AMS (Activity Manager Service) is one of the first services created during the startBootstrapServices method.
At the end of the startOtherServices method (after all other system services have been initialized), SystemServer calls the systemReady method on the AMS instance.
Within AMS, the process reaches the finishBooting method, here I added the call to a new method I defined in AMS, the init method.
In the init method, an instance of UserClient is created.
UserClient initializes RunningPackages and UsedApps and registers receivers for session start (Login) and session end (Logout) intents.
The startup sequence concludes with AMS setting the static reference to RunningPackages in ProcessRecord to the value of RunningPackages from UserClient.
Please refer to the ActivityManagerService, ProcessRecord, CurrentUser and UsedApp code snippets after these diagrams.
The Multiuser Extension session start sequence begins with the Login intent, which is received by UserSession.
UserSession responds by killing all the apps in the UsedApps list using the ProcessKiller interface. This interface is implemented by AMS and passed as a reference to ProcessRecord during its creation.
Next, all the sandboxes of the apps in UsedApps are saved to the repository, and the local sandboxes in the data partition are deleted.
Finally set the new value of UserId (Id of User) with the value received with Login intent.
Please refer to the ActivityManagerService, ProcessRecord, CurrentUser and UsedApp code snippets after these diagrams.
The Multiuser Extension session end sequence starts with the Logout intent, which is received by UserSession.
The sequence is identical to the Login sequence, except that the userId value is reset.
Note that if apps are started while UserSession is in the Logout state, these apps are not registered in the UsedApps list. Without user identification, it is not possible to save the state of the apps (sandboxes).
Please refer to the ActivityManagerService, ProcessRecord, CurrentUser and UsedApp code snippets after these diagrams.
In this sequence diagram, we see how the Multiuser Extension integrates into the app process creation flow.
The concept is straightforward. In the first part of this article, we explained that whenever an app is launched, AMS creates a new process (via the Zygote process server) and a new ProcessRecord structure. I leveraged this process to connect the Multiuser Extension.
Since the ProcessRecord class has a static reference to the RunningPackages component of CurrentUser, its constructor can call the add method implemented in CurrentUser.
This method is the core of the Multiuser Extension, performing the following actions:
Adds the new app to the UsedApps list.
Retrieves the app's sandbox for the logged-in user.
Restores the sandbox in the data partition under the directory /data/data/packagename-app.
Then, it continues with the normal process creation flow.
Below is the source code for the Multiuser Extension project, which we just analyzed through UML diagrams.
For the ActivityManagerService and ProcessRecord classes, I have included only the sections that are impacted by the modifications, specifically the connectors to the CurrentUser class.
I have also left the methods for restoring and saving app sandboxes unimplemented. This is because there are several ways to implement these functions, ranging from simple to more complex solutions.
I opted for a more radical approach.
I developed a daemon that starts at boot_completed with root privileges. This daemon listens on a local socket and executes Linux commands by passing them to the "/bin/sh" shell using the "system" call from Bionic, the libc of Android. In the init.rc file, I added instructions to launch the daemon at boot_completed with root user privileges.
In the CurrentUser methods for restoring and saving sandboxes, requests are sent to this daemon to perform the copy, move, and delete operations needed for sandbox management.
I plan to dedicate one of the upcoming parts of this series to explaining this topic in detail.
Here is the code that implements the UML diagrams for the Multiuser Extension project.
Before we wrap up, let me share the trick I mentioned earlier to make an app "immortal."🙂💪
If you open the original ProcessRecord.java file in the Android AOSP project, you'll notice that the constructor sets the initial oom_score_adj value. This score is used by AMS to determine which apps to terminate when the system runs low on resources. The score is assigned to the process state (ProcessStateRecord) using the setMaxAdj method.
If you assign it the value ProcessList.SYSTEM_ADJ (-900, the same score used for system services), after ensuring that the packageName matches the app you want to keep alive, you can be confident that your app will stay running until Android exhausts all its resources.
Here is the code to insert inside the ProcessRecord constructor (XXXX.YYYY.ZZZZ is the packageName of your app).
We have reached the end of this challenging stage of our journey into the deep, hidden and intricate world of Android.
In the next episode we will dive deeper into the role of AMS in managing the lifecycle of Activities and data structures like ActivityRecord and Tasks.
I remind you my newsletter "Sw Design & Clean Architecture" : https://lnkd.in/eUzYBuEX where you can find my previous articles and where you can register, if you have not already done, so you will be notified when I publish new articles.
Thanks for reading my article, and I hope you have found the topic useful,
Feel free to leave any feedback.
Your feedback is very appreciated.
Thanks again.
Stefano
References:
1. S.Santilli: "https://www.linkedin.com/pulse/inside-brain-android-unveiling-power-stefano-santilli-fuhvf/"
2. S.Santilli: "https://www.linkedin.com/pulse/android-boot-process-part-2-from-zygote-systemserver-stefano-santilli-vm1sf/"
CTO at Ostorlab
7moThanks for sharing this nice article!
Quality and Certification Manager, RSPP
8moWow, Android secrets explained so well! "At the end of this article I will tell you a trick to make a process immortal !!" makes the whole reading worth it, definitely! Nice to read from you, Stefano Santilli
IT Services and Operations Manager at Technogym S.p.A.
8moThis is a great writing not just for being a dive into aspects of Android you will never find treated elsewhere but also for its absolute clarity and effectiveness in presentation. Great work Stefano!