Tuesday, June 27, 2023

Android CarPropertyManager: 차량 데이터 연동의 핵심

Android Automotive OS(AAOS)는 Google이 차량용으로 설계한 안드로이드 플랫폼의 확장 버전으로, 인포테인먼트(IVI) 시스템의 새로운 표준을 제시합니다. 이 강력한 플랫폼의 핵심에는 애플리케이션이 차량의 하드웨어와 직접적으로 소통할 수 있도록 하는 정교한 아키텍처가 자리 잡고 있습니다. 그 중심에 바로 CarPropertyManager가 있습니다. CarPropertyManager는 단순한 API를 넘어, 앱 개발자와 복잡한 차량 하드웨어 간의 필수적인 다리 역할을 수행하며, 차량의 속도, 온도, 연료량 등 수많은 데이터에 접근하고 제어할 수 있는 표준화된 방법을 제공합니다.

이 문서는 CarPropertyManager의 근본적인 개념부터 시작하여, 실제 애플리케이션에서 이를 활용하는 구체적인 방법, 그리고 안정적이고 효율적인 코드를 작성하기 위한 고급 전략과 모범 사례에 이르기까지 모든 것을 심도 있게 다룹니다. 단순히 API를 사용하는 방법을 나열하는 것을 넘어, AAOS의 아키텍처 내에서 CarPropertyManager가 어떻게 작동하는지 이해하고, 이를 통해 더욱 강력하고 지능적인 차량용 애플리케이션을 개발할 수 있는 기반을 마련하는 것을 목표로 합니다.

1. CarPropertyManager와 Android Automotive 아키텍처

CarPropertyManager를 제대로 이해하기 위해서는 먼저 Android Automotive OS의 전체적인 아키텍처를 파악하는 것이 중요합니다. 차량용 앱은 일반적인 모바일 앱과 달리, 자동차라는 특수한 하드웨어 환경과 상호작용해야 합니다. 이 상호작용은 여러 계층으로 구성된 아키텍처를 통해 이루어집니다.

1.1. 계층적 구조: 앱에서 하드웨어까지

Android Automotive 아키텍처는 크게 네 가지 주요 계층으로 나눌 수 있습니다.

  1. 애플리케이션 계층 (Application Layer): 미디어, 내비게이션, HVAC(공조) 제어 등 우리가 직접 사용하는 써드파티 앱 또는 OEM 앱이 존재하는 최상위 계층입니다. 이 앱들은 차량 데이터에 접근하기 위해 Car API를 사용합니다.
  2. Car API 계층 (Car API Layer): 개발자에게 노출되는 공식 API 집합입니다. android.car.* 패키지에 포함된 클래스들이 여기에 속하며, CarPropertyManager 역시 이 계층의 일부입니다. 이 API는 앱이 특정 하드웨어 구현에 종속되지 않도록 추상화된 인터페이스를 제공합니다.
  3. Car Service 계층 (Car Service Layer): 안드로이드 프레임워크 내에서 실행되는 핵심 시스템 서비스입니다. Car API를 통해 들어온 앱의 요청을 받아 처리하고, 하드웨어 추상화 계층(HAL)과 통신하여 실제 차량 하드웨어를 제어하거나 데이터를 수신합니다. 전원 관리, 오디오, 센서 등 다양한 자동차 관련 서비스를 관리합니다.
  4. 하드웨어 추상화 계층 (Vehicle HAL - Hardware Abstraction Layer): Car Service와 차량의 실제 하드웨어(주로 CAN 버스와 같은 차량 네트워크) 사이의 인터페이스입니다. OEM(Original Equipment Manufacturer, 자동차 제조사)은 Vehicle HAL을 구현하여 자신들의 독자적인 하드웨어 신호를 안드로이드가 이해할 수 있는 표준화된 '차량 속성(Vehicle Property)' 형식으로 변환해줍니다.

이 구조에서 CarPropertyManager는 Car API 계층에 속하며, 앱 개발자가 Vehicle HAL이 정의한 '차량 속성'에 접근할 수 있도록 하는 관문(Gateway) 역할을 합니다. 개발자는 복잡한 CAN 버스 프로토콜이나 특정 차량의 하드웨어 사양을 전혀 몰라도, CarPropertyManager가 제공하는 표준화된 속성 ID(Property ID)와 API를 통해 일관된 방식으로 차량 데이터와 상호작용할 수 있습니다. 이는 앱의 이식성을 크게 높여주며, 다양한 차종에서 동일한 코드가 동작할 수 있도록 보장합니다.

1.2. 차량 속성(Vehicle Property)의 개념

CarPropertyManager가 다루는 모든 데이터의 기본 단위는 '차량 속성(Vehicle Property)'입니다. 속성은 차량의 특정 상태나 기능을 나타내는 표준화된 데이터 조각이며, 각각 고유한 정수 ID를 가집니다. 이 ID들은 AOSP(Android Open Source Project)의 VehiclePropertyIds.java 클래스에 상수로 정의되어 있습니다. 몇 가지 대표적인 속성은 다음과 같습니다.

  • VehiclePropertyIds.PERF_VEHICLE_SPEED: 현재 차량의 속도 (m/s 단위의 float 값)
  • VehiclePropertyIds.HVAC_TEMPERATURE_SET: 설정된 공조 장치 온도 (섭씨 단위의 float 값)
  • VehiclePropertyIds.INFO_FUEL_LEVEL: 현재 연료량 (ml 단위의 float 값)
  • VehiclePropertyIds.GEAR_SELECTION: 현재 선택된 기어 (VehicleGear 열거형에 정의된 정수 값)
  • VehiclePropertyIds.DOOR_LOCK: 차량 도어의 잠금 상태 (boolean 값)

각 속성은 단순한 값뿐만 아니라, 다음과 같은 다양한 메타데이터를 포함하는 CarPropertyValue<T> 객체로 캡슐화되어 전달됩니다.

  • Property ID: 속성의 고유 식별자 (예: PERF_VEHICLE_SPEED).
  • Value (T): 속성의 실제 값. 제네릭 타입(T)으로 정의되어 정수, 부동소수점, 불리언, 문자열 등 다양한 데이터 타입을 가질 수 있습니다.
  • Area ID: 속성이 적용되는 차량의 특정 구역. 예를 들어, 공조 장치 온도는 운전석(VehicleAreaSeat.SEAT_ROW_1_LEFT), 조수석(VehicleAreaSeat.SEAT_ROW_1_RIGHT) 등 구역별로 다른 값을 가질 수 있습니다. 모든 속성이 Area ID를 가지는 것은 아닙니다. 차량 속도처럼 전역적인 속성은 Area ID가 0입니다.
  • Status: 속성의 현재 상태 (STATUS_AVAILABLE, STATUS_UNAVAILABLE, STATUS_ERROR). 이를 통해 속성 값을 신뢰할 수 있는지 판단할 수 있습니다.
  • Timestamp: 이 속성 값이 하드웨어에서 생성된 시간 (부팅 이후 나노초 단위). 실시간 데이터 처리에서 값의 유효성을 판단하는 데 매우 중요합니다.

따라서 CarPropertyManager를 사용한다는 것은, 이 표준화된 속성들을 식별하고, CarPropertyValue 객체를 통해 값을 읽거나, 새로운 값을 써서 차량의 상태를 변경하는 일련의 과정을 의미합니다.

2. CarPropertyManager 사용을 위한 기본 설정

CarPropertyManager를 사용하여 차량 데이터에 접근하려면, 먼저 애플리케이션이 Android Automotive 환경에서 실행될 준비가 되어 있어야 하며, 필요한 권한과 서비스 연결 설정을 완료해야 합니다.

2.1. AndroidManifest.xml 설정

모든 AAOS 애플리케이션은 AndroidManifest.xml 파일에 자신의 특성을 명시해야 합니다. 이는 시스템이 앱을 차량용 앱으로 인식하고 필요한 기능을 제공하도록 하는 필수 단계입니다.

2.1.1. 차량용 기능 선언

가장 먼저, 앱이 자동차 하드웨어를 필요로 한다는 것을 시스템에 알려야 합니다. <uses-feature> 태그를 사용하여 이를 선언합니다.

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.mycarapp">

    <!-- 이 앱이 Android Automotive 기기에서 실행되어야 함을 명시 -->
    <uses-feature
        android:name="android.hardware.type.automotive"
        android:required="true" />

    <application ...>
        ...
    </application>
</manifest>

android:required="true" 속성은 이 앱이 Automotive 기능이 없는 기기(예: 일반 스마트폰)의 Play Store에 표시되지 않도록 합니다.

2.1.2. 필요한 권한 요청

차량 속성은 민감한 정보를 포함할 수 있으므로, 각 속성에 접근하기 위해서는 적절한 권한이 필요합니다. 필요한 권한은 접근하려는 속성에 따라 다릅니다. 안드로이드 공식 문서나 AOSP 소스 코드의 VehiclePropertyIds.java 파일을 참조하여 각 속성에 필요한 권한을 확인해야 합니다. 예를 들어, 차량 속도에 접근하려면 Car.PERMISSION_SPEED 권한이 필요합니다.

<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
    package="com.example.mycarapp">

    <!-- 차량 속도 정보를 읽기 위한 권한 -->
    <uses-permission android:name="android.car.permission.CAR_SPEED" />
    
    <!-- 공조 장치(HVAC)를 제어하기 위한 권한 -->
    <uses-permission android:name="android.car.permission.CONTROL_CAR_CLIMATE" />
    
    <!-- 연료 정보를 읽기 위한 권한 -->
    <uses-permission android:name="android.car.permission.CAR_FUEL" />

    <uses-feature ... />

    <application ...>
        ...
    </application>
</manifest>

이러한 권한들은 설치 시점에 부여되는 'normal' 권한부터 사용자에게 직접 동의를 구해야 하는 'dangerous' 권한, 심지어 시스템 앱이나 OEM 서명 앱에만 허용되는 'privileged' 또는 'signature' 권한까지 다양합니다. 개발하려는 앱의 기능에 맞춰 필요한 권한을 정확히 명시하고, 필요하다면 런타임에 권한을 요청하는 로직을 구현해야 합니다.

2.2. Car Service에 연결하기

CarPropertyManager 인스턴스를 얻기 위해서는 먼저 Car Service에 비동기적으로 연결해야 합니다. 이 과정은 Car 클래스를 통해 이루어지며, 안드로이드의 다른 시스템 서비스에 연결하는 과정과 유사합니다.

이 과정은 UI 스레드를 차단하지 않도록 비동기적으로 처리하는 것이 매우 중요합니다. 다음은 Activity의 생명주기에 맞춰 Car Service에 연결하고 해제하는 표준적인 구현 방법입니다.

import android.car.Car;
import android.car.CarNotConnectedException;
import android.car.hardware.property.CarPropertyManager;
import android.os.Bundle;
import androidx.appcompat.app.AppCompatActivity;
import android.util.Log;

public class CarDataActivity extends AppCompatActivity {

    private static final String TAG = "CarDataActivity";

    private Car mCar;
    private CarPropertyManager mCarPropertyManager;

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_car_data);
    }

    @Override
    protected void onResume() {
        super.onResume();
        connectToCarService();
    }

    @Override
    protected void onPause() {
        super.onPause();
        disconnectFromCarService();
    }

    private void connectToCarService() {
        if (mCar != null && mCar.isConnected()) {
            // 이미 연결된 경우, 아무것도 하지 않음
            return;
        }

        // Car.createCar는 정적 팩토리 메소드
        mCar = Car.createCar(this, /* serviceConnection */ null, Car.CAR_WAIT_TIMEOUT_WAIT_FOREVER, (car, ready) -> {
            // 이 람다는 메인 스레드에서 실행됨
            if (ready) {
                Log.d(TAG, "Successfully connected to Car Service");
                onCarServiceConnected(car);
            } else {
                Log.e(TAG, "Failed to connect to Car Service");
                // 연결 실패 처리 로직 (예: 사용자에게 알림)
            }
        });
        // connect()는 비동기 호출이며, 결과는 위 람다 콜백으로 전달됨
        mCar.connect();
    }

    private void onCarServiceConnected(Car car) {
        try {
            // 연결이 성공하면, getCarManager를 통해 필요한 Manager를 가져옴
            mCarPropertyManager = (CarPropertyManager) car.getCarManager(Car.PROPERTY_SERVICE);
            if (mCarPropertyManager == null) {
                Log.e(TAG, "Failed to get CarPropertyManager");
                return;
            }
            // 이제 mCarPropertyManager를 사용하여 속성 작업을 수행할 수 있음
            // 예: registerCallback 등
            
        } catch (CarNotConnectedException e) {
            Log.e(TAG, "Car not connected while trying to get manager", e);
        }
    }
    
    private void disconnectFromCarService() {
        if (mCar != null) {
            // 등록했던 콜백이나 리스너를 모두 해제
            if (mCarPropertyManager != null) {
                // mCarPropertyManager.unregisterCallback(...);
            }
            
            // 서비스 연결 해제
            mCar.disconnect();
            mCar = null;
            mCarPropertyManager = null;
            Log.d(TAG, "Disconnected from Car Service");
        }
    }
}

위 코드에서 주목할 점은 다음과 같습니다.

  • Car.createCar(): Car 인스턴스를 생성합니다. 네 번째 인자로 전달된 Car.CarServiceLifecycleListener는 연결 상태가 변경될 때 호출됩니다.
  • mCar.connect(): 비동기적으로 Car Service에 연결을 시도합니다.
  • onCarServiceConnected(): 연결이 성공적으로 완료되면 리스너의 onLifecycleChanged 메소드가 ready=true와 함께 호출됩니다. 이 시점에서 car.getCarManager(Car.PROPERTY_SERVICE)를 호출하여 CarPropertyManager의 인스턴스를 안전하게 얻을 수 있습니다.
  • onPause()에서 disconnectFromCarService(): Activity가 화면에서 보이지 않을 때 리소스를 해제하기 위해 연결을 끊는 것이 중요합니다. 이는 메모리 누수를 방지하고 시스템 리소스를 효율적으로 사용하는 데 필수적입니다.

이러한 설정과 연결 과정이 완료되어야 비로소 CarPropertyManager의 강력한 기능들을 활용할 준비가 된 것입니다.

3. 차량 속성 읽기 및 쓰기

CarPropertyManager 인스턴스를 성공적으로 얻었다면, 이제 차량의 속성을 읽고 쓰는 작업을 수행할 수 있습니다. 작업은 크게 동기적(Synchronous) 방식과 비동기적(Asynchronous) 방식으로 나뉩니다.

3.1. 동기적 속성 읽기: `getProperty()`

특정 시점의 속성 값을 한 번만 가져와야 하는 경우 getProperty() 메소드를 사용합니다. 이 메소드는 네트워크 요청과 같이 동작하여 호출 스레드를 차단(blocking)하므로, UI 스레드에서 직접 호출하는 것은 피해야 합니다. 만약 UI 스레드에서 호출할 경우 ANR(Application Not Responding)을 유발할 수 있습니다.

import android.car.VehiclePropertyIds;
import android.car.hardware.property.CarPropertyValue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

// ... Activity 내부

private final ExecutorService executor = Executors.newSingleThreadExecutor();

private void readFuelLevel() {
    if (mCarPropertyManager == null) {
        Log.e(TAG, "CarPropertyManager is not available.");
        return;
    }

    executor.execute(() -> {
        try {
            // 연료 레벨 속성을 읽어옴. INFO_FUEL_LEVEL은 Area ID가 없음 (전역 속성)
            // getProperty의 제네릭 타입을 명시하여 반환 값의 타입을 지정
            CarPropertyValue<Float> fuelLevelProp = mCarPropertyManager.getProperty(
                    VehiclePropertyIds.INFO_FUEL_LEVEL, 
                    0 // Area ID
            );
            
            if (fuelLevelProp != null && fuelLevelProp.getStatus() == CarPropertyValue.STATUS_AVAILABLE) {
                Float fuelLevelInMl = fuelLevelProp.getValue();
                long timestamp = fuelLevelProp.getTimestamp();
                Log.d(TAG, "Current Fuel Level: " + fuelLevelInMl + " ml, timestamp: " + timestamp);
                
                // UI 업데이트는 반드시 메인 스레드에서 수행해야 함
                runOnUiThread(() -> {
                    // updateFuelLevelUI(fuelLevelInMl);
                });
            } else {
                Log.w(TAG, "Fuel level property is not available.");
            }

        } catch (CarNotConnectedException e) {
            Log.e(TAG, "Car not connected", e);
        } catch (IllegalArgumentException e) {
            // 해당 속성이 이 차량에서 지원되지 않는 경우 발생
            Log.e(TAG, "Property ID is not supported by this vehicle", e);
        }
    });
}

위 코드에서는 백그라운드 스레드에서 getProperty()를 호출하여 연료량을 가져옵니다. getProperty()는 속성 ID와 Area ID를 인자로 받습니다. 반환된 CarPropertyValue 객체의 상태를 확인하여 값이 유효한지 검사한 후, 실제 값을 추출하여 사용합니다. 지원되지 않는 속성에 접근하려고 하면 IllegalArgumentException이 발생할 수 있으므로 예외 처리가 중요합니다.

3.2. 동기적 속성 쓰기: `setProperty()`

차량의 상태를 변경하려면 setProperty() 메소드를 사용합니다. 예를 들어, 공조 장치의 온도를 설정하거나 창문을 여는 등의 작업이 여기에 해당됩니다. 이 작업 역시 시스템 서비스와 통신이 필요하므로, 별도의 스레드에서 수행하는 것이 권장됩니다.

속성 쓰기 작업은 해당 속성에 대한 쓰기 권한이 필요하며, 권한이 없을 경우 SecurityException이 발생합니다.

import android.car.VehiclePropertyIds;
import android.car.hardware.property.CarPropertyValue;
import android.car.hardware.constant.VehicleAreaSeat;

// ... Activity 내부

private void setHvacTemperature(float temperature) {
    if (mCarPropertyManager == null) {
        Log.e(TAG, "CarPropertyManager is not available.");
        return;
    }

    executor.execute(() -> {
        try {
            // 운전석(ROW 1, LEFT)의 온도를 설정.
            // setProperty의 제네릭 타입은 설정할 값의 타입과 일치해야 함.
            mCarPropertyManager.setProperty(
                    Float.class, // 설정할 값의 클래스 타입
                    VehiclePropertyIds.HVAC_TEMPERATURE_SET, // 속성 ID
                    VehicleAreaSeat.SEAT_ROW_1_LEFT, // Area ID
                    temperature // 새로운 값
            );
            Log.d(TAG, "Successfully set HVAC temperature for driver seat to " + temperature + "°C");

        } catch (CarNotConnectedException e) {
            Log.e(TAG, "Car not connected", e);
        } catch (SecurityException e) {
            // 권한 부족 (예: CONTROL_CAR_CLIMATE 권한이 없는 경우)
            Log.e(TAG, "Permission denied to set HVAC temperature", e);
        } catch (IllegalArgumentException e) {
            // 속성이 지원되지 않거나, 값이 범위를 벗어난 경우 발생
            Log.e(TAG, "Failed to set property", e);
        }
    });
}

setProperty()는 설정할 값의 클래스 타입, 속성 ID, Area ID, 그리고 새로운 값을 인자로 받습니다. 이 메소드는 성공 시 아무것도 반환하지 않지만, 실패 시 다양한 예외를 던질 수 있으므로 견고한 예외 처리가 필수적입니다.

4. 실시간 데이터 처리: 속성 변경 감지

차량 속도, 엔진 RPM, 현재 기어 등과 같이 지속적으로 변하는 데이터를 처리하기 위해 getProperty()를 주기적으로 호출하는 것은 매우 비효율적입니다. 대신, CarPropertyManager는 속성 값이 변경될 때마다 알림을 받을 수 있는 콜백(Callback) 메커니즘을 제공합니다. 이는 이벤트 기반 프로그래밍 모델로, 리소스를 훨씬 효율적으로 사용합니다.

4.1. `CarPropertyManager.Callback` 구현

속성 변경을 감지하기 위해서는 CarPropertyManager.Callback 인터페이스를 구현해야 합니다. 이 인터페이스에는 두 가지 메소드가 있습니다.

  • onChangeEvent(CarPropertyValue<T> value): 구독한 속성의 값이 변경될 때마다 호출됩니다. CarPropertyValue 객체를 통해 새로운 값과 타임스탬프 등의 정보를 얻을 수 있습니다.
  • onErrorEvent(int propId, int areaId): 구독 중인 속성에서 오류가 발생했을 때(예: 해당 센서가 갑자기 비활성화되는 경우) 호출됩니다.
private CarPropertyManager.Callback mCarPropertyCallback = new CarPropertyManager.Callback() {
    @Override
    public void onChangeEvent(CarPropertyValue<?> value) {
        // 이 콜백은 여러 속성을 동시에 처리할 수 있으므로, propertyId로 구분
        int propertyId = value.getPropertyId();
        
        switch (propertyId) {
            case VehiclePropertyIds.PERF_VEHICLE_SPEED:
                float speedInMps = (Float) value.getValue();
                float speedInKph = speedInMps * 3.6f;
                Log.d(TAG, "Vehicle speed changed: " + speedInKph + " km/h");
                // UI 업데이트는 메인 스레드에서 처리
                updateSpeedUI(speedInKph);
                break;
                
            case VehiclePropertyIds.GEAR_SELECTION:
                int currentGear = (Integer) value.getValue();
                Log.d(TAG, "Gear changed: " + currentGear);
                updateGearUI(currentGear);
                break;
        }
    }

    @Override
    public void onErrorEvent(int propId, int areaId) {
        Log.w(TAG, "Received error event for property: " + propId + " in area: " + areaId);
        // 오류 처리 로직 (예: UI에 '데이터 없음' 표시)
    }
};

중요: 이 콜백 메소드들은 Car Service와의 바인더(binder) 스레드 풀에서 호출되므로, 기본적으로 UI 스레드가 아닙니다. 따라서 콜백 내에서 UI를 업데이트하려면 Activity.runOnUiThread()View.post() 등을 사용해야 합니다.

4.2. 콜백 등록 및 해제

구현한 콜백을 특정 속성에 대해 등록하려면 registerCallback() 메소드를 사용합니다. 이 때 데이터 업데이트 주기(rate)를 지정할 수 있습니다.

애플리케이션의 생명주기에 맞춰 콜백을 등록하고 해제하는 것이 매우 중요합니다. 일반적으로 onResume()에서 등록하고 onPause()에서 해제하여, 앱이 활성 상태일 때만 데이터를 수신하고 불필요한 리소스 낭비를 막습니다.

// ... onCarServiceConnected() 내부 혹은 그 이후 호출

private void registerPropertyCallbacks() {
    if (mCarPropertyManager == null) return;
    try {
        // 차량 속도(PERF_VEHICLE_SPEED)에 대한 콜백 등록
        // 업데이트 주기는 SENSOR_RATE_NORMAL로 설정 (약 10Hz, 100ms 간격)
        mCarPropertyManager.registerCallback(
                mCarPropertyCallback,
                VehiclePropertyIds.PERF_VEHICLE_SPEED,
                CarPropertyManager.SENSOR_RATE_NORMAL
        );

        // 기어 변경(GEAR_SELECTION)에 대한 콜백 등록
        // 기어는 자주 바뀌지 않으므로, on-change 방식으로만 업데이트 받기 위해 rate를 0으로 설정
        mCarPropertyManager.registerCallback(
                mCarPropertyCallback,
                VehiclePropertyIds.GEAR_SELECTION,
                CarPropertyManager.SENSOR_RATE_ONCHANGE
        );
        Log.d(TAG, "Successfully registered property callbacks.");

    } catch (CarNotConnectedException e) {
        Log.e(TAG, "Car not connected", e);
    } catch (SecurityException e) {
        Log.e(TAG, "Permission denied to register callback", e);
    } catch (IllegalArgumentException e) {
        Log.e(TAG, "Property is not supported or rate is invalid", e);
    }
}


// ... onPause()에서 호출될 disconnectFromCarService() 내부 혹은 별도 메소드

private void unregisterPropertyCallbacks() {
    if (mCarPropertyManager != null) {
        // 등록했던 모든 콜백을 한 번에 해제
        // 개별적으로 unregisterCallback(callback, propId)를 호출할 수도 있음
        mCarPropertyManager.unregisterCallback(mCarPropertyCallback);
        Log.d(TAG, "Unregistered property callbacks.");
    }
}

registerCallback의 세 번째 인자인 업데이트 주기는 성능에 큰 영향을 미칩니다. SENSOR_RATE_FASTEST, SENSOR_RATE_NORMAL, SENSOR_RATE_UI 등 미리 정의된 상수를 사용하거나, 직접 Hz 단위로 주기를 지정할 수 있습니다. SENSOR_RATE_ONCHANGE (0f)는 값이 변경될 때만 이벤트를 발생시켜 가장 효율적입니다.

5. 고급 주제 및 모범 사례

CarPropertyManager를 효과적으로 사용하기 위해 알아두어야 할 몇 가지 고급 주제와 모범 사례가 있습니다.

5.1. `CarPropertyConfig`: 속성의 메타데이터 활용

앱을 특정 차종에 종속되지 않고 범용적으로 만들려면, 하드코딩된 값 대신 차량이 제공하는 정보를 동적으로 활용해야 합니다. CarPropertyConfig 객체는 특정 속성에 대한 메타데이터를 제공하며, 이를 통해 동적인 UI와 로직을 구현할 수 있습니다.

getCarPropertyConfig() 메소드를 통해 CarPropertyConfig 객체를 얻을 수 있습니다.

private void checkPropertyConfiguration() {
    if (mCarPropertyManager == null) return;

    executor.execute(() -> {
        try {
            CarPropertyConfig<?> hvacTempConfig = mCarPropertyManager.getCarPropertyConfig(VehiclePropertyIds.HVAC_TEMPERATURE_SET);
            if (hvacTempConfig != null) {
                // 이 속성이 지원하는 Area ID 목록을 가져옴
                List<Integer> areaIds = hvacTempConfig.getAreaIds();
                Log.d(TAG, "Supported HVAC areas: " + areaIds); // [470048515, 470048516, ...]

                // 속성이 지원하는 값의 범위를 가져옴 (온도의 경우)
                Float minTemp = (Float) hvacTempConfig.getMinValue(areaIds.get(0));
                Float maxTemp = (Float) hvacTempConfig.getMaxValue(areaIds.get(0));
                Log.d(TAG, "Supported temperature range: " + minTemp + " - " + maxTemp); // 예: 16.0 - 32.0

                // UI의 온도 조절 슬라이더의 범위를 이 값으로 동적으로 설정할 수 있음
                runOnUiThread(() -> {
                    // setupHvacSlider(minTemp, maxTemp);
                });
            } else {
                // hvacTempConfig가 null이면, 이 차량은 HVAC_TEMPERATURE_SET 속성을 지원하지 않음
                Log.w(TAG, "This vehicle does not support HVAC_TEMPERATURE_SET property.");
            }
        } catch (CarNotConnectedException e) {
            Log.e(TAG, "Car not connected", e);
        }
    });
}

CarPropertyConfig를 통해 얻을 수 있는 정보는 다음과 같습니다.

  • getPropertyId(): 속성 ID.
  • getPropertyType(): 속성의 데이터 타입 (Float.class, Integer.class 등).
  • getAccess(): 접근 유형 (ACCESS_READ, ACCESS_WRITE, ACCESS_READ_WRITE).
  • getAreaIds(): 지원하는 모든 Area ID 목록.
  • getMinValue(areaId) / getMaxValue(areaId): 특정 구역에서 지원하는 최소/최대값 (범위가 있는 속성의 경우).

앱 시작 시점에 필요한 속성들의 설정을 미리 확인하고, 지원하지 않는 기능은 UI에서 비활성화하는 등 방어적인 프로그래밍을 통해 앱의 안정성을 크게 향상시킬 수 있습니다.

5.2. 권한 모델과 예외 처리

차량 데이터는 보안 및 안전과 직결되므로 엄격한 권한 모델을 따릅니다. 필요한 권한이 없으면 SecurityException이 발생합니다. 항상 API 호출을 try-catch 블록으로 감싸고, 특정 기능에 필요한 권한을 사용자에게 명확히 안내해야 합니다. 특히 써드파티 앱 개발자는 OEM에 의해 제한될 수 있는 'privileged' 권한이 필요한 기능은 구현이 불가능할 수 있다는 점을 인지해야 합니다.

5.3. 스레딩 관리

앞서 언급했듯이, CarPropertyManager의 동기 메소드(getProperty, setProperty 등)는 블로킹 호출이므로 절대 UI 스레드에서 실행해서는 안 됩니다. 비동기 콜백(onChangeEvent)은 바인더 스레드에서 실행되므로 UI와 관련된 작업은 반드시 UI 스레드로 전달해야 합니다. 이를 위해 RxJava, Kotlin Coroutines, 또는 간단한 ExecutorHandler 조합을 활용하여 스레드를 명확하게 관리하는 것이 중요합니다.

5.4. 생명주기 관리

콜백 등록 및 해제는 Activity나 Fragment의 생명주기와 정확하게 맞춰야 합니다. onResume/onPause 또는 onStart/onStop 쌍을 사용하여, 앱이 화면에 보일 때만 리소스를 사용하도록 구현해야 합니다. 이를 소홀히 하면 앱이 백그라운드에 있을 때도 불필요한 데이터를 계속 수신하여 배터리를 소모하고, 심각한 경우 메모리 누수를 일으킬 수 있습니다.

결론

Android CarPropertyManager는 Android Automotive 애플리케이션 개발의 핵심 구성 요소입니다. 이는 복잡한 차량 내부 네트워크를 추상화하여 개발자가 표준화되고 일관된 방식으로 차량의 상태를 읽고 제어할 수 있게 해줍니다. 이 글에서 다룬 아키텍처에 대한 이해, 기본 설정, 동기 및 비동기 API 사용법, 그리고 고급 주제 및 모범 사례를 숙지한다면, 어떤 차종에서도 안정적이고 효율적으로 동작하는 고품질 차량용 애플리케이션을 개발할 수 있을 것입니다. 차량 데이터의 무한한 가능성을 활용하여 사용자에게 혁신적인 운전 경험을 제공하는 여정에 CarPropertyManager가 든든한 동반자가 되어줄 것입니다.


0 개의 댓글:

Post a Comment