Android Automotive OS(AAOS)는 Google이 차량용 인포테인먼트(IVI) 시스템을 위해 제공하는 강력한 플랫폼입니다. 기본 제공되는 수많은 기능 외에도, 특정 차량의 하드웨어를 제어하거나 독자적인 기능을 제공하기 위해서는 커스텀 시스템 서비스를 개발해야 하는 경우가 많습니다. 이는 단순한 앱 개발을 넘어 안드로이드 플랫폼 자체를 확장하는 고도의 기술입니다.
이 가이드는 AOSP(Android Open Source Project) 환경에서 자신만의 시스템 서비스를 처음부터 만들어 SystemServer에 등록하고, 애플리케이션에서 사용할 수 있도록 연동하는 전체 과정을 상세하게 다룹니다. 가상의 '차량 확장 서비스(Vehicle Extension Service)'를 예제로 들어, 실내 앰비언트 라이트 색상을 제어하는 기능을 구현해보겠습니다. 이 여정을 통해 여러분은 Android Automotive 플랫폼의 핵심 동작 원리를 깊이 이해하게 될 것입니다.
- AIDL(Android Interface Definition Language)을 사용한 서비스 인터페이스 정의
- Binder를 상속받는 핵심 시스템 서비스 로직 구현
- 안드로이드 부팅의 심장, SystemServer에 새로운 서비스 추가하기
- 애플리케이션에서
context.getSystemService()로 서비스를 호출할 수 있도록 SystemServiceRegistry에 등록하는 방법 - AOSP 전체 빌드, 배포 및 테스트 전략
- 실무에서 반드시 마주치는 SELinux 정책 및 권한 문제 해결 팁
1단계: 서비스 인터페이스 정의 (AIDL)
모든 시스템 서비스의 첫걸음은 인터페이스 정의에서 시작합니다. 안드로이드에서 애플리케이션 프로세스와 시스템 서비스 프로세스는 서로 다른 공간에서 실행됩니다. 이 둘 사이의 통신(IPC, Inter-Process Communication)을 위해 안드로이드는 Binder 프레임워크를 사용하며, 이 통신의 규약, 즉 API 명세를 정의하는 언어가 바로 AIDL입니다.
AIDL은 자바와 유사한 문법을 가지며, 우리가 만들 서비스가 외부에 어떤 기능(메서드)을 제공할지 명시하는 역할을 합니다. 컴파일 과정에서 이 `.aidl` 파일은 실제 통신 로직이 담긴 자바 인터페이스와 Stub 클래스로 자동 변환됩니다. 이것이 바로 원격 프로시저 호출(RPC)의 기반이 됩니다.
IVehicleExtensionService.aidl 파일 생성
먼저, 우리 서비스의 기능을 정의할 AIDL 파일을 생성해야 합니다. AOSP 소스 트리 내에서 시스템 API가 위치하는 경로에 파일을 만드는 것이 일반적입니다. 가상의 '차량 확장 서비스'를 위한 인터페이스를 정의해 보겠습니다. 이 서비스는 앰비언트 라이트의 색상을 설정하고 현재 색상 값을 가져오는 두 가지 기능을 제공한다고 가정합니다.
파일 위치: frameworks/base/core/java/android/car/vehicle/IVehicleExtensionService.aidl
일반적으로 새로운 시스템 API는 `frameworks/base` 아래에 위치시키며, 관련된 패키지 경로에 맞게 디렉토리를 생성합니다. 이 경우 `android.car.vehicle` 패키지를 사용하겠습니다.
// IVehicleExtensionService.aidl
package android.car.vehicle;
/**
* Custom vehicle extension service for controlling non-standard vehicle features.
* This is an example for demonstrating how to add a new system service.
*
* {@hide}
*/
interface IVehicleExtensionService {
/**
* Sets the color of the interior ambient light.
* @param color The integer representation of the color (e.g., Color.RED).
*/
void setAmbientLightColor(int color);
/**
* Gets the current color of the interior ambient light.
* @return The integer representation of the current color.
*/
int getAmbientLightColor();
/**
* An example of a one-way call. The client will not block waiting for a response.
* This is useful for commands that do not need to return a value.
*/
oneway void rebootInfotainmentSystem();
}
주요 개념 분석:
- package: 이 AIDL 파일이 속할 자바 패키지를 지정합니다. 생성될 자바 코드도 이 패키지 하위에 위치합니다.
- {@hide}: Javadoc 주석으로, 이 API가 공개 SDK에 포함되지 않음을 나타냅니다. 시스템 내부 API는 대부분 `@hide` 처리됩니다.
- interface: 서비스가 제공할 메서드들을 정의하는 부분입니다.
- 메서드 시그니처: 자바와 동일하게 반환 타입, 메서드 이름, 파라미터를 정의합니다. AIDL은 `int`, `String`, `boolean`과 같은 기본 타입과 `List`, `Map` (특정 조건 하에) 및 다른 AIDL 인터페이스, Parcelable을 구현한 객체를 지원합니다.
- oneway 키워드: 이 키워드가 붙은 메서드는 비동기 호출이 됩니다. 즉, 클라이언트(앱)가 이 메서드를 호출하면 응답을 기다리지 않고 즉시 다음 코드를 실행합니다. 반환 값이 없어야 하며, 시스템에 명령만 내리고 빠른 응답이 필요 없는 경우에 유용합니다. 스레드 블로킹을 방지하여 시스템 전반의 반응성을 높이는 데 중요한 역할을 합니다.
빌드 시스템에 AIDL 파일 추가
AOSP 빌드 시스템이 새로 추가된 AIDL 파일을 인지하고 자바 코드를 생성하도록 설정해야 합니다. 이는 `.bp` (Blueprint) 파일을 수정하여 이루어집니다. `frameworks/base`의 `Android.bp` 파일을 열어 `framework-minus-apex` 모듈을 찾고, `srcs` 목록에 방금 생성한 AIDL 파일 경로를 추가합니다.
파일 위치: frameworks/base/Android.bp
...
java_library {
name: "framework-minus-apex",
...
srcs: [
...
"core/java/android/app/IActivityManager.aidl",
// 많은 aidl 파일들...
"core/java/android/car/vehicle/IVehicleExtensionService.aidl", // <-- 이 줄을 추가합니다.
...
],
...
}
...
이 수정을 통해 다음 AOSP 전체 빌드 시, 빌드 시스템은 `IVehicleExtensionService.aidl` 파일을 파싱하여 `out/soong/.intermediates/frameworks/base/framework-minus-apex/android_common/xref/src/android/car/vehicle/IVehicleExtensionService.java` 와 같은 경로에 자바 인터페이스 파일을 생성합니다. 이 생성된 파일에는 통신을 위한 내부 클래스인 `Stub`과 `Proxy`가 포함됩니다.
이제 서비스의 '설계도'가 완성되었습니다. 다음 단계에서는 이 설계도를 바탕으로 실제 동작하는 '구현체'를 만들 차례입니다.
2단계: 시스템 서비스 구현
이제 AIDL 인터페이스의 실제 로직을 담을 자바 클래스를 작성합니다. 이 클래스는 SystemServer 프로세스 내에서 실행되며, 앱으로부터 Binder를 통해 요청을 받아 처리하는 역할을 합니다. 안드로이드의 모든 시스템 서비스는 `frameworks/base/services/` 디렉토리 하위에 위치합니다. 우리도 이 규칙을 따라 서비스를 구현하겠습니다.
VehicleExtensionService.java 파일 생성
서비스의 핵심 구현 파일입니다. 이 클래스는 AIDL을 통해 생성된 `IVehicleExtensionService.Stub` 추상 클래스를 상속받아야 합니다. `Stub` 클래스는 Binder 통신의 서버 측 로직을 담고 있으며, 우리는 AIDL에 정의된 메서드들을 오버라이드하여 실제 기능을 구현하게 됩니다.
파일 위치: frameworks/base/services/core/java/com/android/server/car/vehicle/VehicleExtensionService.java
(services/core/ 또는 services/autofill/ 등 기능에 맞는 디렉토리를 선택하거나 새로 생성할 수 있습니다. 여기서는 `car` 관련 기능을 모으기 위해 `com/android/server/car/vehicle` 패키지를 새로 만들었다고 가정합니다.)
package com.android.server.car.vehicle;
import android.car.vehicle.IVehicleExtensionService;
import android.content.Context;
import android.os.Process;
import android.util.Slog;
import com.android.server.SystemService;
/**
* Vehicle Extension Service.
* This service runs in the SystemServer process and provides custom vehicle APIs.
* For demonstration, it simulates controlling an ambient light.
*/
public class VehicleExtensionService extends IVehicleExtensionService.Stub {
private static final String TAG = "VehicleExtensionService";
private static final String ENFORCE_MANAGE_AMBIENT_LIGHT_PERMISSION =
"android.car.permission.MANAGE_AMBIENT_LIGHT";
private final Context mContext;
private int mCurrentLightColor; // 현재 앰비언트 라이트 색상을 저장하는 변수
/**
* Service constructor. This is called by the SystemServiceManager.
*/
public VehicleExtensionService(Context context) {
mContext = context;
mCurrentLightColor = 0; // 초기 색상 (예: 꺼짐)
Slog.i(TAG, "VehicleExtensionService is constructed.");
}
@Override
public void setAmbientLightColor(int color) {
// 1. 권한 확인
mContext.enforceCallingOrSelfPermission(
ENFORCE_MANAGE_AMBIENT_LIGHT_PERMISSION,
"Must have MANAGE_AMBIENT_LIGHT permission to set color"
);
Slog.i(TAG, "Setting ambient light color from " + mCurrentLightColor + " to " + color +
" (called from PID=" + getCallingPid() + ", UID=" + getCallingUid() + ")");
// 2. 핵심 로직 수행
mCurrentLightColor = color;
// 3. 실제 하드웨어 제어 (예: HAL을 통해)
// nativeSetHardwareLightColor(color);
}
@Override
public int getAmbientLightColor() {
// 읽기 권한은 보통 더 관대하지만, 예제에서는 동일한 권한을 요구합니다.
mContext.enforceCallingOrSelfPermission(
ENFORCE_MANAGE_AMBIENT_LIGHT_PERMISSION,
"Must have MANAGE_AMBIENT_LIGHT permission to get color"
);
Slog.d(TAG, "Getting ambient light color: " + mCurrentLightColor);
return mCurrentLightColor;
}
@Override
public void rebootInfotainmentSystem() {
// oneway 호출이므로 클라이언트는 이 메서드가 끝날 때까지 기다리지 않습니다.
// 매우 민감한 작업이므로 높은 수준의 권한이 필요합니다.
mContext.enforceCallingOrSelfPermission(
android.Manifest.permission.REBOOT,
"Must have REBOOT permission to reboot IVI"
);
Slog.w(TAG, "Rebooting infotainment system as requested by PID=" + getCallingPid());
// 실제 재부팅 로직 호출 (주의: 실제 시스템에서는 더 정교한 처리가 필요)
// PowerManager pm = mContext.getSystemService(PowerManager.class);
// pm.reboot("userrequested");
}
}
구현 코드 상세 분석
- 상속 구조: `IVehicleExtensionService.Stub`을 상속받습니다. `Stub`은 `android.os.Binder`를 구현하고 있으며, Binder IPC 메커니즘의 핵심입니다.
- 생성자 (Constructor): `Context` 객체를 인자로 받습니다. 시스템 서비스는 이 `Context`를 통해 다른 시스템 서비스에 접근하거나, 권한을 확인하고, 시스템 리소스를 사용하는 등 다양한 작업을 수행합니다. 이 생성자는 나중에 `SystemServer`에서 서비스를 시작할 때 호출됩니다.
- 권한 확인 (Permission Enforcement): 시스템 서비스 개발에서 가장 중요한 부분 중 하나입니다. `mContext.enforceCallingOrSelfPermission()` 메서드는 서비스를 호출한 클라이언트(앱)가 지정된 권한을 가지고 있는지 확인합니다. 만약 권한이 없다면 `SecurityException`을 발생시켜 메서드 실행을 즉시 중단시킵니다. 이는 악의적인 앱으로부터 시스템을 보호하는 필수적인 장치입니다. `getCallingPid()`와 `getCallingUid()`는 호출한 프로세스와 사용자의 ID를 식별하는 데 사용됩니다.
- 핵심 로직: `mCurrentLightColor` 변수는 서비스의 상태를 저장하는 역할을 합니다. 실제 제품에서는 이 값은 단순히 메모리에 저장되는 것이 아니라, JNI(Java Native Interface)를 통해 HAL(Hardware Abstraction Layer)을 호출하여 차량의 CAN 버스나 특정 ECU(Electronic Control Unit)로 전달되어야 합니다.
- 로깅 (Logging): `Slog` (System Log)를 사용하여 주요 동작을 기록합니다. 일반 `Log`와 달리 시스템 서버 로그에 출력되며, 디버깅에 매우 중요합니다. `logcat -b system` 명령으로 확인할 수 있습니다.
3단계: SystemServer에 서비스 등록하기
서비스의 인터페이스와 구현이 모두 완료되었습니다. 하지만 아직 이 서비스는 독립된 코드 조각일 뿐, 안드로이드 시스템의 일부가 아닙니다. 이제 안드로이드 부팅 과정의 핵심인 SystemServer에 우리가 만든 서비스를 '등록'하여 생명력을 불어넣을 차례입니다.
SystemServer는 안드로이드가 부팅될 때 Zygote로부터 포크(fork)되는 첫 번째 핵심 프로세스입니다. Activity Manager, Package Manager, Power Manager 등 거의 모든 주요 시스템 서비스를 시작하고 관리하는 역할을 담당합니다. 여기에 우리 서비스를 추가한다는 것은, 안드로이드 플랫폼의 공식적인 구성 요소로 편입시킨다는 의미입니다.
SystemServer.java 파일 수정
파일 위치: frameworks/base/services/java/com/android/server/SystemServer.java
이 파일은 매우 크고 복잡하며, 안드로이드의 부팅 단계를 순차적으로 보여줍니다. 서비스는 그 중요도와 의존성에 따라 여러 다른 단계에서 시작될 수 있습니다. `SystemServer`의 `run()` 메서드를 보면 `startBootstrapServices()`, `startCoreServices()`, `startOtherServices()`와 같은 메서드 호출을 볼 수 있습니다.
- startBootstrapServices: 시스템의 가장 기본적인 서비스들 (예: ActivityManagerService, PowerManagerService)이 시작됩니다.
- startCoreServices: 핵심적이지만 부트스트랩 단계만큼 중요하지는 않은 서비스들이 시작됩니다.
- startOtherServices: 대부분의 다른 서비스들과 써드파티(OEM) 커스텀 서비스들이 시작되는 단계입니다. 우리가 만든 서비스는 여기에 추가하는 것이 가장 안전하고 일반적입니다.
`startOtherServices()` 메서드 내부의 적절한 위치에 다음 코드를 추가합니다.
// In SystemServer.java, inside startOtherServices() method
...
t.traceBegin("StartInputManager");
inputManager = new InputManagerService(context);
t.traceEnd();
...
// 다른 서비스 시작 코드들...
t.traceBegin("StartVehicleExtensionService");
try {
// SystemServiceManager를 통해 서비스를 시작합니다.
mSystemServiceManager.startService(com.android.server.car.vehicle.VehicleExtensionService.class);
Slog.i(TAG, "Vehicle Extension Service started.");
} catch (Throwable e) {
// 서비스 시작 실패 시 치명적인 오류 로그를 남깁니다.
reportWtf("starting Vehicle Extension Service", e);
}
t.traceEnd();
...
SystemServiceManager vs ServiceManager.addService
과거에는 `ServiceManager.addService("service_name", new MyService())`와 같은 형태로 서비스를 직접 등록했습니다. 하지만 안드로이드 롤리팝(5.0) 이후부터는 `SystemServiceManager`를 사용하는 것이 표준 방식이 되었습니다. 이 둘의 차이점을 이해하는 것은 중요합니다.
| 기능 | SystemServiceManager | ServiceManager.addService (직접 호출) |
|---|---|---|
| 생명주기 관리 | 자동 관리. 서비스의 시작, 중지, 재시작 등을 관리하며 부팅 단계에 맞춰 서비스를 체계적으로 로드합니다. | 수동 관리. 개발자가 직접 서비스 객체를 생성하고 등록해야 합니다. 생명주기 관리가 복잡합니다. |
| 부팅 단계 통합 | onStart(), onBootPhase() 콜백을 통해 특정 부팅 단계(예: `PHASE_ACTIVITY_MANAGER_READY`)에서 코드를 실행할 수 있어 의존성 관리가 용이합니다. |
부팅 단계와 직접적인 연동이 없어, 다른 서비스가 준비되었는지 수동으로 확인해야 할 수 있습니다. |
| 현대적 접근 방식 | 권장되는 표준 방식입니다. 코드의 가독성과 유지보수성을 높여줍니다. | 레거시 방식이며, 매우 낮은 레벨의 핵심 서비스에 제한적으로 사용됩니다. |
| 코드 예시 | mSystemServiceManager.startService(MyService.class); |
ServiceManager.addService("my_service", new MyService()); |
`SystemServiceManager`를 사용하기 위해, 우리 서비스 클래스인 `VehicleExtensionService.java`가 `SystemService`를 상속하도록 구조를 약간 변경하는 것이 더 정교한 방법입니다. `SystemService`는 서비스의 생명주기 콜백을 제공합니다.
(개선된) VehicleExtensionService.java 구현
// ... imports
import com.android.server.SystemService;
// Stub 상속 대신 SystemService를 상속하고, 내부적으로 Binder 객체를 관리합니다.
public class VehicleExtensionService extends SystemService {
private static final String TAG = "VehicleExtensionService";
public static final String VEHICLE_EXTENSION_SERVICE_NAME = "vehicle_extension";
private final VehicleExtensionServiceImpl mImpl;
public VehicleExtensionService(Context context) {
super(context);
mImpl = new VehicleExtensionServiceImpl(context);
}
@Override
public void onStart() {
Slog.i(TAG, "Registering service " + VEHICLE_EXTENSION_SERVICE_NAME);
// 실제 Binder 객체를 ServiceManager에 게시합니다.
publishBinderService(VEHICLE_EXTENSION_SERVICE_NAME, mImpl);
}
@Override
public void onBootPhase(int phase) {
// 특정 부팅 단계에서 수행할 작업이 있다면 여기에 구현합니다.
// if (phase == SystemService.PHASE_BOOT_COMPLETED) { ... }
}
// 실제 AIDL 구현은 내부 클래스로 분리하여 관리합니다.
private static class VehicleExtensionServiceImpl extends IVehicleExtensionService.Stub {
private final Context mContext;
private int mCurrentLightColor;
private static final String ENFORCE_MANAGE_AMBIENT_LIGHT_PERMISSION =
"android.car.permission.MANAGE_AMBIENT_LIGHT";
VehicleExtensionServiceImpl(Context context) {
mContext = context;
}
@Override
public void setAmbientLightColor(int color) {
mContext.enforceCallingOrSelfPermission(/* ... */);
// ... 로직 구현 ...
}
// ... 나머지 AIDL 메서드 구현 ...
}
}
이 개선된 구조에서는 `VehicleExtensionService`가 `SystemService`의 생명주기를 따르게 되고, `onStart()` 콜백에서 `publishBinderService()`를 호출하여 자신의 Binder 구현체(`mImpl`)를 `ServiceManager`에 "vehicle_extension"이라는 이름으로 등록합니다. `SystemServer`의 코드는 변경 없이 `mSystemServiceManager.startService(VehicleExtensionService.class);`를 그대로 사용하면 됩니다. 이 방식이 훨씬 더 깔끔하고 안드로이드 프레임워크의 설계 철학에 부합합니다.
4단계: 클라이언트(앱) 연동을 위한 SystemServiceRegistry 설정
이제 우리 서비스는 시스템 부팅 시 성공적으로 시작되고 `ServiceManager`에 등록되었습니다. 하지만 아직 애플리케이션 개발자에게는 보이지 않는 존재입니다. 앱에서 `Context.getSystemService(String)` 메서드를 통해 서비스에 접근할 수 있도록 하려면, 이 '이름'과 실제 서비스 프록시 객체를 연결해주는 마지막 다리가 필요합니다. 그 역할을 하는 것이 바로 SystemServiceRegistry입니다.
`SystemServiceRegistry`는 모든 시스템 서비스의 '전화번호부'와 같습니다. "activity"라는 이름을 주면 `ActivityManager`를, "power"라는 이름을 주면 `PowerManager`를 반환하도록 매핑 정보가 저장되어 있습니다. 여기에 우리 서비스 "vehicle_extension"을 추가해야 합니다.
1. 서비스 이름 정의 (Context.java)
먼저, 앱 개발자들이 사용할 서비스의 공식적인 '이름'(상수)을 정의해야 합니다. 이 상수는 `Context` 클래스에 추가하는 것이 관례입니다.
파일 위치: frameworks/base/core/java/android/content/Context.java
// In Context.java class body
...
/**
* Use with {@link #getSystemService(String)} to retrieve a
* {@link android.os.vibrator.VibratorManager} for interacting with the device
* vibrators.
*
* @see #getSystemService(String)
* @see android.os.vibrator.VibratorManager
*/
public static final String VIBRATOR_MANAGER_SERVICE = "vibrator_manager";
/**
* Use with {@link #getSystemService(String)} to retrieve a
* {@link android.car.vehicle.VehicleExtensionManager} for controlling custom vehicle features.
* @hide
*/
@SdkConstant(SdkConstantType.SERVICE_NAME)
public static final String VEHICLE_EXTENSION_SERVICE = "vehicle_extension";
...
@hide 어노테이션은 이 서비스가 일반 앱 개발자가 아닌, 시스템 앱이나 특정 권한을 가진 앱만 사용하도록 의도되었음을 의미합니다. @SdkConstant는 이 필드가 SDK에 포함될 상수임을 나타냅니다.
2. Manager 클래스 생성
앱 개발자에게 AIDL 인터페이스를 직접 노출하는 것은 좋지 않습니다. 복잡한 `RemoteException` 처리 등을 강요하기 때문입니다. 대신, AIDL 호출을 감싸는 편리한 API를 제공하는 'Manager' 클래스를 만드는 것이 표준 패턴입니다. 이것이 바로 앱 개발자가 실제로 사용하게 될 클래스입니다.
파일 위치: frameworks/base/core/java/android/car/vehicle/VehicleExtensionManager.java
package android.car.vehicle;
import android.annotation.NonNull;
import android.annotation.SystemService;
import android.content.Context;
import android.os.IBinder;
import android.os.RemoteException;
import android.os.ServiceManager;
import android.util.Slog;
/**
* VehicleExtensionManager provides a high-level API for controlling custom vehicle features.
* Apps can get an instance of this class by calling
* {@link android.content.Context#getSystemService(String)}.
*
* @hide
*/
@SystemService(Context.VEHICLE_EXTENSION_SERVICE)
public final class VehicleExtensionManager {
private static final String TAG = "VehicleExtensionManager";
private final Context mContext;
private final IVehicleExtensionService mService;
/**
* @hide - constructor is not public
*/
public VehicleExtensionManager(Context context, IVehicleExtensionService service) {
mContext = context;
mService = service;
if (mService == null) {
// Service not available on this device.
Slog.e(TAG, "VehicleExtensionService is not available.");
}
}
/**
* Sets the color of the interior ambient light.
* Requires the android.car.permission.MANAGE_AMBIENT_LIGHT permission.
*
* @param color The integer representation of the color.
*/
public void setAmbientLightColor(int color) {
try {
if (mService != null) {
mService.setAmbientLightColor(color);
}
} catch (RemoteException e) {
// RemoteException은 Binder 통신 중 상대 프로세스가 죽는 등
// 심각한 오류 발생 시 던져집니다. 이를 런타임 예외로 다시 던져 앱을 크래시시킵니다.
throw e.rethrowFromSystemServer();
}
}
/**
* Gets the current color of the interior ambient light.
* Requires the android.car.permission.MANAGE_AMBIENT_LIGHT permission.
*
* @return The current color, or 0 if the service is unavailable.
*/
public int getAmbientLightColor() {
try {
if (mService != null) {
return mService.getAmbientLightColor();
}
return 0;
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
}
}
이 Manager 클래스는 `RemoteException`을 처리하여 앱 개발자가 신경 쓰지 않도록 하고, 서비스가 null일 경우(예: 해당 기능이 없는 디바이스)를 우아하게 처리합니다. 이것이 바로 잘 설계된 안드로이드 API의 모습입니다.
3. SystemServiceRegistry에 등록
이제 마지막 조각을 맞출 시간입니다. `SystemServiceRegistry`에 `VEHICLE_EXTENSION_SERVICE`라는 이름이 요청되면, `VehicleExtensionManager` 객체를 생성해서 반환하도록 등록해야 합니다.
파일 위치: frameworks/base/core/java/android/app/SystemServiceRegistry.java
이 파일의 거대한 `static` 블록 안에 다음 코드를 추가합니다. 이 블록은 시스템이 부팅될 때 단 한 번 실행되어 모든 서비스 생성 로직을 등록합니다.
// In SystemServiceRegistry.java's static block
...
registerService(Context.STATS_MANAGER_SERVICE, StatsManager.class,
new CachedServiceFetcher<StatsManager>() {
@Override
public StatsManager createService(ContextImpl ctx) throws ServiceNotFoundException {
IBinder b = ServiceManager.getServiceOrThrow(Context.STATS_MANAGER_SERVICE);
return new StatsManager(ctx, IStatsManager.Stub.asInterface(b));
}});
...
// 우리 서비스를 여기에 추가합니다. 보통 알파벳 순서로 추가합니다.
registerService(Context.VEHICLE_EXTENSION_SERVICE, VehicleExtensionManager.class,
new CachedServiceFetcher<VehicleExtensionManager>() {
@Override
public VehicleExtensionManager createService(ContextImpl ctx) {
// 1. ServiceManager로부터 서비스의 Binder 객체를 이름으로 가져옵니다.
IBinder b = ServiceManager.getService(Context.VEHICLE_EXTENSION_SERVICE);
if (b == null) {
// 서비스가 없는 경우 null을 반환하여 Manager 생성자에서 처리하도록 합니다.
return new VehicleExtensionManager(ctx, null);
}
// 2. 가져온 IBinder 객체를 AIDL 인터페이스 타입으로 변환합니다.
IVehicleExtensionService service = IVehicleExtensionService.Stub.asInterface(b);
// 3. Manager 객체를 생성하여 반환합니다.
return new VehicleExtensionManager(ctx, service);
}
});
...
이 코드는 `getSystemService(Context.VEHICLE_EXTENSION_SERVICE)`가 호출될 때 실행될 로직을 정의합니다.
- `ServiceManager.getService()`를 통해 SystemServer에 있는 우리 서비스의 원격 Binder 객체(`IBinder`)를 가져옵니다.
- `IVehicleExtensionService.Stub.asInterface(b)`는 가져온 `IBinder` 객체를 앱 프로세스에서 사용할 수 있는 프록시(Proxy) 객체로 변환합니다.
- 이 프록시 객체를 `VehicleExtensionManager` 생성자에 넘겨주어 Manager 인스턴스를 생성하고 반환합니다.
이것으로 모든 코드 수정이 끝났습니다. 이제 전체 시스템을 빌드하고 우리가 만든 서비스가 잘 동작하는지 확인할 차례입니다.
5단계: 전체 빌드 및 테스트
AOSP에서 프레임워크 코드를 수정했다면, 변경사항을 적용하기 위해 전체 시스템 이미지를 다시 빌드해야 합니다. 이 과정은 컴퓨터 사양에 따라 수십 분에서 수 시간이 소요될 수 있습니다.
AOSP 빌드
AOSP 소스 코드의 최상위 디렉토리에서 다음 명령을 실행합니다.
# 1. 빌드 환경 설정
source build/envsetup.sh
# 2. 빌드 타겟 선택 (예: Automotive 12 에뮬레이터)
lunch sdk_car_x86_64-userdebug
# 3. 전체 빌드 시작 (컴퓨터의 코어 수에 맞게 -j 옵션 조정)
m -j16
빌드가 성공적으로 완료되면, 생성된 이미지 파일들(`system.img`, `boot.img` 등)을 실제 타겟 디바이스나 에뮬레이터에 플래싱합니다.
# 에뮬레이터 재시작 (변경된 이미지 자동 로드)
emulator &
# 실제 디바이스의 경우 fastboot 사용
adb reboot bootloader
fastboot flashall -w
테스트 방법
서비스가 정상적으로 동작하는지 확인하는 방법은 여러 가지가 있습니다.
1. 로그 확인
부팅 후 `logcat`을 통해 서비스가 시작되었는지 확인합니다. 우리가 추가한 로그가 보여야 합니다.
adb logcat -b system | grep "VehicleExtensionService"
# 예상 출력:
# I VehicleExtensionService: VehicleExtensionService is constructed.
# I VehicleExtensionService: Registering service vehicle_extension
# I SystemServer: Vehicle Extension Service started.
2. dumpsys를 이용한 서비스 상태 확인
`dumpsys`는 시스템 서비스의 현재 상태를 덤프하는 강력한 디버깅 도구입니다. 이를 활용하려면 서비스에 `dump()` 메서드를 구현해야 합니다.
VehicleExtensionServiceImpl.java에 dump() 메서드 추가:
@Override
public void dump(FileDescriptor fd, PrintWriter writer, String[] args) {
// dump 권한 확인
if (mContext.checkCallingOrSelfPermission(android.Manifest.permission.DUMP)
!= PackageManager.PERMISSION_GRANTED) {
writer.println("Permission Denial: can't dump VehicleExtensionService from from pid="
+ Binder.getCallingPid() + ", uid=" + Binder.getCallingUid());
return;
}
writer.println("--- Vehicle Extension Service ---");
writer.println("Current Ambient Light Color: " + mCurrentLightColor);
writer.println("---------------------------------");
}
다시 빌드하고 플래싱한 후, 쉘에서 다음 명령을 실행합니다.
adb shell dumpsys vehicle_extension
# 예상 출력:
# --- Vehicle Extension Service ---
# Current Ambient Light Color: 0
# ---------------------------------
3. 테스트 앱을 통한 기능 확인
가장 확실한 방법은 서비스를 사용하는 테스트 앱을 만들어보는 것입니다. 이 앱은 시스템 서비스에 접근하고 제어하기 위한 커스텀 권한을 `AndroidManifest.xml`에 선언해야 합니다.
AndroidManifest.xml 예시:
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.vehicletest">
<!-- 우리 서비스가 요구하는 커스텀 권한을 사용한다고 선언 -->
<uses-permission android:name="android.car.permission.MANAGE_AMBIENT_LIGHT" />
<application ... >
...
</application>
</manifest>
이 앱은 일반 앱과 달리 시스템 이미지에 포함되거나, `adb push`를 통해 `/system/priv-app`과 같은 특권 파티션에 설치되어야 커스텀 권한을 부여받을 수 있습니다.
테스트 액티비티 코드 예시:
import android.car.vehicle.VehicleExtensionManager;
import android.content.Context;
import android.graphics.Color;
import android.os.Bundle;
import android.view.View;
import android.widget.Button;
import android.widget.TextView;
import androidx.appcompat.app.AppCompatActivity;
public class MainActivity extends AppCompatActivity {
private VehicleExtensionManager mVehicleManager;
private TextView mColorTextView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mVehicleManager = (VehicleExtensionManager) getSystemService(Context.VEHICLE_EXTENSION_SERVICE);
mColorTextView = findViewById(R.id.colorTextView);
Button redButton = findViewById(R.id.redButton);
redButton.setOnClickListener(v -> {
if (mVehicleManager != null) {
mVehicleManager.setAmbientLightColor(Color.RED);
updateColor();
}
});
// ... 다른 색상 버튼들 ...
updateColor();
}
private void updateColor() {
if (mVehicleManager != null) {
int currentColor = mVehicleManager.getAmbientLightColor();
mColorTextView.setText("Current Color: " + Integer.toHexString(currentColor));
mColorTextView.setBackgroundColor(currentColor);
} else {
mColorTextView.setText("Service not available");
}
}
}
이 앱을 실행하여 버튼을 눌렀을 때 `dumpsys`의 색상 값이 변경되고 로그가 정상적으로 출력된다면, 여러분은 성공적으로 Android Automotive 시스템 서비스를 만든 것입니다!
심화: 고려사항 및 문제 해결
지금까지의 과정은 시스템 서비스를 추가하는 핵심적인 흐름입니다. 하지만 실제 제품 수준의 서비스를 개발하기 위해서는 몇 가지 더 깊이 있는 주제들을 반드시 고려해야 합니다.
SELinux (Security-Enhanced Linux) 정책
현대 안드로이드에서 가장 흔하게 겪는 장벽은 바로 SELinux입니다. 아무리 코드가 완벽하고 권한 설정을 다 했더라도, SELinux 정책이 허용하지 않으면 서비스는 등록조차 되지 않거나 다른 프로세스와 통신할 수 없습니다. 서비스 추가 시 필요한 최소한의 SELinux 설정은 다음과 같습니다.
- 서비스 컨텍스트 정의:
system/sepolicy/private/service_contexts파일에 서비스 이름과 보안 컨텍스트를 매핑합니다.vehicle_extension u:object_r:vehicle_extension_service:s0 - 서비스 타입 정의:
system/sepolicy/public/service.te파일에 새로운 서비스 타입을 선언합니다.type vehicle_extension_service, service_manager_type; - SystemServer 권한 부여:
system/sepolicy/private/system_server.te파일에 `system_server` 프로세스가 우리 서비스를 추가(add)하고 찾을(find) 수 있는 권한을 부여합니다.add_service(system_server, vehicle_extension_service) - 클라이언트(앱) 권한 부여: 특권 앱(`priv_app`)이 서비스를 사용할 수 있도록 `system/sepolicy/private/priv_app.te` 등에 규칙을 추가합니다.
allow priv_app vehicle_extension_service:service_manager find;
SELinux 거부(denial) 로그는 `logcat`에서 `avc: denied` 키워드로 검색할 수 있습니다. 이 로그를 분석하고 `audit2allow` 도구를 사용하면 필요한 규칙을 생성하는 데 도움을 받을 수 있습니다.
SELinux는 어렵지만 안드로이드 보안의 핵심입니다. 'permissive' 모드로 설정하여 임시로 비활성화할 수는 있지만, 이는 개발 단계에서만 사용해야 하며 최종 제품에서는 반드시 'enforcing' 모드에서 모든 정책을 만족시켜야 합니다.
커스텀 권한 정의 및 관리
예제에서 `android.car.permission.MANAGE_AMBIENT_LIGHT`라는 가상의 권한을 사용했습니다. 이 커스텀 권한을 시스템에 공식적으로 등록하려면 `frameworks/base/core/res/AndroidManifest.xml`에 `<permission>` 태그를 추가해야 합니다.
<!-- In frameworks/base/core/res/AndroidManifest.xml -->
<permission android:name="android.car.permission.MANAGE_AMBIENT_LIGHT"
android:protectionLevel="signature|privileged" />
`protectionLevel`은 이 권한의 민감도를 결정합니다. `signature|privileged`는 플랫폼 서명 키로 사인된 앱이나 `/system/priv-app`에 설치된 앱에게만 부여될 수 있는 높은 수준의 권한입니다.
스레딩 모델
Binder 호출은 기본적으로 동기식으로 동작합니다. 만약 서비스의 메서드가 파일 I/O나 네트워크 통신, 복잡한 연산 등 16ms 이상 소요될 수 있는 작업을 수행한다면, 절대로 Binder 스레드에서 직접 처리해서는 안 됩니다. 이는 전체 시스템의 반응성을 저하시키고 ANR(Application Not Responding)을 유발할 수 있습니다.
이런 경우, `HandlerThread`나 `Executors`를 사용하여 작업을 별도의 워커 스레드로 보내고, Binder 스레드는 즉시 반환하도록 설계해야 합니다. `oneway` 키워드는 이러한 비동기 작업에 특히 유용합니다.
결론
지금까지 우리는 Android Automotive OS의 심장부인 프레임워크를 직접 수정하여 새로운 시스템 서비스를 성공적으로 추가했습니다. AIDL로 통신 규약을 만들고, 서비스 로직을 구현했으며, SystemServer에 등록하여 생명을 불어넣고, SystemServiceRegistry를 통해 앱이 사용할 수 있는 다리를 놓았습니다.
이 과정은 단순히 기능을 추가하는 것을 넘어, 안드로이드 플랫폼이 어떻게 수많은 서비스들을 유기적으로 관리하고, 프로세스 간 통신을 안전하게 처리하며, 강력한 권한 모델과 SELinux를 통해 시스템을 보호하는지에 대한 깊은 통찰을 제공합니다. 이제 여러분은 이 지식을 바탕으로 차량의 고유한 하드웨어를 제어하거나, 복잡한 데이터를 처리하는 등 상상하는 어떤 기능이든 AAOS에 통합할 수 있는 강력한 무기를 손에 쥐게 된 것입니다. AOSP의 세계는 넓고, 여러분의 여정은 이제 막 시작되었습니다.
Post a Comment