본문 바로가기

Linux/Android

MountService의 기본과 UMS 연결 과정

출처 : http://blog.daum.net/baramjin/16010974


안드로이드에서 USB 또는 SD 카드 등의 Mount 를 체크하고 동작하는 서버 서비스이다.

관련된 내용에 대한 설명을 인터넷으로 찾은 곳은 다음과 같다.

 

http://letsgoustc.spaces.live.com/?_c11_BlogPart_pagedir=Last&_c11_BlogPart_BlogPart=blogview&_c=BlogPart&partqs=cat%3DGoogle%2520Android

 

 대략적으로 번역하면 다음과 같다.

 

실제 mount 관련 작업은 mountd에서 이루어진다. 시스템 서버 어플리케이션인 MountService는 JAVA 레이어이며 "/dev/socket/mountd를 통해서 mountd와 통신한다.

 

mountd는 "/system/etc/mountd.conf"로부터 해석된 mount point 리스트를 생성하고 이 설정 파일로부터 정의된 장치를 mount 시도한다.

이는 자동적으로 USB mass storage의 uevent와 "/dev/block"를 위한 inotify event를 감시하는  thread를 연다. 즉 이 thread는 SD card의 mount와 USB의 plug in/out를 체크한다. mountd는 MountService로부터 명령(command)을 받고, 이벤트(event)를 보내기 위하여 socket을 생성하고 동작한다.

 

MountService dhk mountd 사이의 명령과 이벤트는 텍스트로 이루어져 있기 때문에 이해하기 쉽다.

USB를 위한 uevent는 두개의 문자열 "POWER_SUPPLY_TYPE=USB"와 “POWER_SUPPLY_ONLINE=”+[“1” | “0”]을 포함하면 감시된다. (무슨 소리인지)

 

만약 MountService가 mountd로부터 어떤 이벤트를 받았다면 다른 클라이언트에서 알려주기 위하여 다음의 intents를 브로드 캐스팅한다.

 

ACTION_UMS_CONNECTED,

ACTION_UMS_DISCONNECTED,

ACTION_MEDIA_REMOVED,

ACTION_MEDIA_UNMOUNTED,

ACTION_MEDIA_MOUNTED,

ACTION_MEDIA_SHARED,

ACTION_MEDIA_BAD_REMOVAL,

ACTION_MEDIA_UNMOUNTABLE,

ACTION_MEDIA_EJECT.

 

  

날림 번역이지만 대강의 동작은 설명이 될 것이다.

위의 내용은 안드로이드 cupcake 버젼이 초기 발표되었을 때의 내용인 것 같다. 최근에는 mountd 대신에 vold로 바뀌었다.

mountd와 vold에 대한 것은 다음을 참조하면 된다.

 

/system/core/mountd

/system/core/vold

 

mountd가 동작하기 위해서는 실제 "/system/etc/mountd.conf"를 읽어들여야 하는데 안드로이드를 동작시켜 보면 이 파일이 없는 것 같다. 그리고 mountd의 make 파일을 보면 주석으로 mountd 대신에 vold를 사용한다고 언급되어 있다.

 

 

안드로이드의 기본적인 서버 서비스는 다음 폴더에 소스가 있다.

 

/frameworks/base/services/java/com/android/server

 

위의 설명과 관련된 소스는 다음 파일이다.

 

/frameworks/base/services/java/com/android/server/MountService.java

/frameworks/base/services/java/com/android/server/MountListener.java

 

위의 설명처럼 MountListener과 vold와 텍스트로 통신하기 때문에 다음과 같은 명령어와 이벤트에 대한 정의가 있다. 이벤트와 명령어도 VOLD로 시작한다. 역시 같은 정의가 /system/core/vold 의 소스 파일과 헤더 파일 등에 정의되어 있다.

 

final class MountListener implements Runnable {

 

    ......

 

    // vold commands
    private static final String VOLD_CMD_ENABLE_UMS = "enable_ums";
    private static final String VOLD_CMD_DISABLE_UMS = "disable_ums";
    private static final String VOLD_CMD_SEND_UMS_STATUS = "send_ums_status";
    private static final String VOLD_CMD_MOUNT_VOLUME = "mount_volume:";
    private static final String VOLD_CMD_EJECT_MEDIA = "eject_media:";
    private static final String VOLD_CMD_FORMAT_MEDIA = "format_media:";

    // vold events
    private static final String VOLD_EVT_UMS_ENABLED = "ums_enabled";
    private static final String VOLD_EVT_UMS_DISABLED = "ums_disabled";
    private static final String VOLD_EVT_UMS_CONNECTED = "ums_connected";
    private static final String VOLD_EVT_UMS_DISCONNECTED = "ums_disconnected";

    private static final String VOLD_EVT_NOMEDIA = "volume_nomedia:";
    private static final String VOLD_EVT_UNMOUNTED = "volume_unmounted:";
    private static final String VOLD_EVT_MOUNTED = "volume_mounted:";
    private static final String VOLD_EVT_MOUNTED_RO = "volume_mounted_ro:";
    private static final String VOLD_EVT_UMS = "volume_ums";
    private static final String VOLD_EVT_BAD_REMOVAL = "volume_badremoval:";
    private static final String VOLD_EVT_DAMAGED = "volume_damaged:";
    private static final String VOLD_EVT_CHECKING = "volume_checking:";
    private static final String VOLD_EVT_NOFS = "volume_nofs:";
    private static final String VOLD_EVT_EJECTING = "volume_ejecting:";

 

mountd와 소켓으로 통신하고 MountService에 상태 정보를 알려주는 것은 하나의 thread를 이용한다.

 

final class MountListener implements Runnable {

 

    ......

  

    public void run() {
        // ugly hack for the simulator.
        if ("simulator".equals(SystemProperties.get("ro.product.device"))) {
            SystemProperties.set("EXTERNAL_STORAGE_STATE", Environment.MEDIA_MOUNTED);
            // usbd does not run in the simulator, so send a fake device mounted event to trigger the Media Scanner

            mService.notifyMediaMounted(Environment.getExternalStorageDirectory().getPath(), false);
            
            // no usbd in the simulator, so no point in hanging around.
            return;
        }
    
        try {  
            while (true) {
                listenToSocket();
            }
        } catch (Throwable t) {
            // catch all Throwables so we don't bring down the system process
            Log.e(TAG, "Fatal error " + t + " in MountListener thread!");
        }
    }

 

 listenToSocket() 은 통신을 위한 새로운 소켓을 생성하고 inputStream을 읽어서 이벤트를 처리한다. inputStream에서 에러가 발생하는 경우에만 소켓을 닫는데 먼저 synchronized()를 호출하고 outputStream이 있는지 없는지를 체크한 후, outputstrem을 닫고, 소켓을 닫는다.

 

(소켓 관련해서는 다음 폴더 참조 : /frameworks/base/core/java/android/net

 

final class MountListener implements Runnable {

 

    ......

 

    private void listenToSocket() {
       LocalSocket socket = null;

        try {
            socket = new LocalSocket();
            LocalSocketAddress address = new LocalSocketAddress(VOLD_SOCKET, 
                    LocalSocketAddress.Namespace.RESERVED);

            socket.connect(address);

            InputStream inputStream = socket.getInputStream();
            mOutputStream = socket.getOutputStream();

            byte[] buffer = new byte[100];

            writeCommand(VOLD_CMD_SEND_UMS_STATUS);
            
            while (true) {
                int count = inputStream.read(buffer);
                if (count < 0) break;

                int start = 0;
                for (int i = 0; i < count; i++) {
                    if (buffer[i] == 0) {
                        String event = new String(buffer, start, i - start);
                        handleEvent(event);
                        start = i + 1;
                    }                   
                }
            }                
        } catch (IOException ex) {
            // This exception is normal when running in desktop simulator 
            // where there is no mount daemon to talk to

            // log("IOException in listenToSocket");
        }
        
        synchronized (this) {
            if (mOutputStream != null) {
                try {
                    mOutputStream.close();
                } catch (IOException e) {
                    Log.w(TAG, "IOException closing output stream");
                }
                
                mOutputStream = null;
            }
        }
        
        try {
            if (socket != null) {
                socket.close();
            }
        } catch (IOException ex) {
            Log.w(TAG, "IOException closing socket");
        }
       
        /*
         * Sleep before trying again.
         * This should not happen except while debugging.
         * Without this sleep, the emulator will spin and
         * create tons of throwaway LocalSockets, making
         * system_server GC constantly.
         */
        Log.e(TAG, "Failed to connect to vold", new IllegalStateException());
        SystemClock.sleep(2000);
    }

 

 listenToSocket에서 이벤트를 처리하는 과정을 확인하기 위해서 handleEvent()를 다시 보면 다음과 같다. 대부분의 동작은 이벤트의 종류를 확인하고 이를 다시 MountService로 전달(Notify)하는 역활을 한다. 전달이란 표현을 썼지만 notifyUmsXXX()나 notifyMediaXXX() 함수는 모두 MountService 클래스에 정의되어 있다. 즉 MountLister에서 MountService 콜백 함수를 호출하는 것이다.

 

final class MountListener implements Runnable {

 

    ......

 

    private void handleEvent(String event) {
        if (Config.LOGD) Log.d(TAG, "handleEvent " + event);
    
        int colonIndex = event.indexOf(':');
        String path = (colonIndex > 0 ? event.substring(colonIndex + 1) : null);
        
        if (event.equals(VOLD_EVT_UMS_ENABLED)) {
            mUmsEnabled = true;
        } else if (event.equals(VOLD_EVT_UMS_DISABLED)) {
            mUmsEnabled = false;
        } else if (event.equals(VOLD_EVT_UMS_CONNECTED)) {
            mUmsConnected = true;
            mService.notifyUmsConnected();
        } else if (event.equals(VOLD_EVT_UMS_DISCONNECTED)) {
            mUmsConnected = false;        
            mService.notifyUmsDisconnected();
        } else if (event.startsWith(VOLD_EVT_NOMEDIA)) {
            mService.notifyMediaRemoved(path);
        } else if (event.startsWith(VOLD_EVT_UNMOUNTED)) {
            mService.notifyMediaUnmounted(path);
        } else if (event.startsWith(VOLD_EVT_CHECKING)) {
            mService.notifyMediaChecking(path);
        } else if (event.startsWith(VOLD_EVT_NOFS)) {
            mService.notifyMediaNoFs(path);
        } else if (event.startsWith(VOLD_EVT_MOUNTED)) {
            mService.notifyMediaMounted(path, false);
        } else if (event.startsWith(VOLD_EVT_MOUNTED_RO)) {
            mService.notifyMediaMounted(path, true);
        } else if (event.startsWith(VOLD_EVT_UMS)) {
            mService.notifyMediaShared(path);
        } else if (event.startsWith(VOLD_EVT_BAD_REMOVAL)) {
            mService.notifyMediaBadRemoval(path);
            // also send media eject intent, to notify apps to close any open
            // files on the media.
            mService.notifyMediaEject(path);
        } else if (event.startsWith(VOLD_EVT_DAMAGED)) {
            mService.notifyMediaUnmountable(path);
        } else if (event.startsWith(VOLD_EVT_EJECTING)) {
            mService.notifyMediaEject(path);
        }   
 
    }

}

 

MountService와 MountListener는 어떻게 연결될까? MountService에서 MountListener을 생성하게 된다. 즉 MountListerer의 mService는 생성자에서 전달받은 값으로 설정된다. 관련된 코드는 MountService 코드에서 확인할 수 있다.

 

class MountService extends IMountService.Stub {

 

    ......

 

    public MountService(Context context) {
        mContext = context;

        // Register a BOOT_COMPLETED handler so that we can start
        // MountListener. We defer the startup so that we don't
        // start processing events before we ought-to
        mContext.registerReceiver(mBroadcastReceiver,
                new IntentFilter(Intent.ACTION_BOOT_COMPLETED), null, null);

        mListener =  new MountListener(this);       
        mShowSafeUnmountNotificationWhenUnmounted = false;

        mPlaySounds = SystemProperties.get("persist.service.mount.playsnd", "1").equals("1");

        mAutoStartUms = SystemProperties.get("persist.service.mount.umsauto", "0").equals("1");
    }

    BroadcastReceiver mBroadcastReceiver = new BroadcastReceiver() {
        public void onReceive(Context context, Intent intent) {
            if (intent.getAction().equals(Intent.ACTION_BOOT_COMPLETED)) {
                Thread thread = new Thread(mListener, MountListener.class.getName());
                thread.start();
            }
        }
    };

 

}

 

UMS의 연결 과정을 잠깐 보면 handleEvent()에서 VOLD_EVT_UMS_CONNECTED를 확인하면 mService.notifyUmsConnected()를 호출한다. 이를 보면 Storage 상태를 체크해서 조건이 맞는 경우에만 setMassStorageEnabled()를 호출하게 되어 있다. 안드로이드의 경우 SD 카드가 삽입되어 있지 않으면 UMS 연결이 안되는데 (안드로이드는 Default Storage로 SD 카드를 우선 지원한다. 내장 NAND 메모리는 바로 사용할 수 없다) 바로 이 조건 비교문 때문에 그렇것 같다.

 

class MountService extends IMountService.Stub {

 

    ......

  

    void notifyUmsConnected() {
        String storageState = Environment.getExternalStorageState();
        if (!storageState.equals(Environment.MEDIA_REMOVED) &&
            !storageState.equals(Environment.MEDIA_BAD_REMOVAL) &&
            !storageState.equals(Environment.MEDIA_CHECKING)) {

            if (mAutoStartUms) {
                try {
                    setMassStorageEnabled(true);
                } catch (RemoteException e) {
                }
            } else {
                updateUsbMassStorageNotification(false, true);
            }
        }

        Intent intent = new Intent(Intent.ACTION_UMS_CONNECTED);
        mContext.sendBroadcast(intent); 
    }

 

    public void setMassStorageEnabled(boolean enable) throws RemoteException {
        mListener.setMassStorageEnabled(enable);
    }

}

 

 setMassStorageEnabled()는 결국 MountListener의 setMassStorageEnabled()를 호출하는데 연결된 소켓을 이용하여 mountd에 명령어를 전송한다. (실제 동작은 mountd가 한다는 의미다.)

 

final class MountListener implements Runnable {

 

    ...... 

 

    void setMassStorageEnabled(boolean enable) {
        writeCommand(enable ? VOLD_CMD_ENABLE_UMS : VOLD_CMD_DISABLE_UMS);
    }
}

 

다시 notifyUmsConnected()를 보면 intent를 생성해서 브로드 캐스팅하는 과정이 있다. 원래 안드로이드 응용에서 intent는 새로운 Activity로 이동시 인자를 전달하기 위해 많이 사용되나, 여기서는 서비스에서 이벤트를 얻은 후, 여러 클라이언트들이 알아서 동작하도록 정보를 브로드 캐스팅하고 있다.

class MountService extends IMountService.Stub {

 

    ......

  

    void notifyUmsConnected() {
        String storageState = Environment.getExternalStorageState();
        if (!storageState.equals(Environment.MEDIA_REMOVED) &&
            !storageState.equals(Environment.MEDIA_BAD_REMOVAL) &&
            !storageState.equals(Environment.MEDIA_CHECKING)) {

            if (mAutoStartUms) {
                try {
                    setMassStorageEnabled(true);
                } catch (RemoteException e) {
                }
            } else {
                updateUsbMassStorageNotification(false, true);
            }
        }

        Intent intent = new Intent(Intent.ACTION_UMS_CONNECTED);
        mContext.sendBroadcast(intent);

    }

 

notifyUmsConnected()에서 UMS 관련해서 주의깊게 봐야 하는 내용은 mAutoStartUms 설정관련된 내용과 UpdateUsbMassStorageNotification() 이다.

 

안드로이드에서 UMS 관련 기본 동작 시나리오는 USB 케이블이 연결되면 연결 상태를 알린다. 이 경우 PC에서도 이동식 디스크는 보이지만 실제 이동식 디스크로 연결되지는 않는다.(연결 시도시 미디어를 넣어달라고 한다) 이동식 디스크로 동작 시키려면 USB 연결 아이콘 쪽을 드래그 하여 "USB Connected"란 항목이 활성화 되게 하고 이를 다시 터치하여 다이얼로그 박스를 띄운 후, UMS 연결을 선택해야 한다.

mAutoStartUms는 이와 같은 동작을 처리하는 변수이다. 따라서 MountService가 초기화 될때 관련값을 읽어오게 된다.

이를 다르게 설정하기 위한 함수로 setAutoStartUms()도 제공한다. 이를 보면 안드로이드의 환경 설정 정보를 얻어오거나 설정하는 방법을 확인할 수 있다. 또한 커스터 마이징을 위해서 위의 시나리오를 변경할 방법도 알 수 있다.

 

class MountService extends IMountService.Stub {

 

    ......

 

    public MountService(Context context) {
        mContext = context;

        // Register a BOOT_COMPLETED handler so that we can start
        // MountListener. We defer the startup so that we don't
        // start processing events before we ought-to
        mContext.registerReceiver(mBroadcastReceiver,
                new IntentFilter(Intent.ACTION_BOOT_COMPLETED), null, null);

        mListener =  new MountListener(this);       
        mShowSafeUnmountNotificationWhenUnmounted = false;

        mPlaySounds = SystemProperties.get("persist.service.mount.playsnd", "1").equals("1");

        mAutoStartUms = SystemProperties.get("persist.service.mount.umsauto", "0").equals("1");
    }

 

    public void setAutoStartUms(boolean enabled) {
        if (mContext.checkCallingOrSelfPermission(
                android.Manifest.permission.WRITE_SETTINGS) 
                != PackageManager.PERMISSION_GRANTED) {
            throw new SecurityException("Requires WRITE_SETTINGS permission");
        }
        mAutoStartUms = enabled;
        SystemProperties.set("persist.service.mount.umsauto", (enabled ? "1" : "0"));
    } 

}

 

원래 안드로이드 시나리오에서는 UpdateUsbMassStorageNotification()를 이용해서 UMS 연결 여부를 사용자가 선택할 수 있게 한다.

이를 보면 다음과 같다.  

    void updateUsbMassStorageNotification(boolean suppressIfConnected, boolean sound) {

        try {

            if (getMassStorageConnected() && !suppressIfConnected) {
                Intent intent = new Intent();
                intent.setClass(mContext, com.android.internal.app.UsbStorageActivity.class);

                PendingIntent pi = PendingIntent.getActivity(mContext, 0, intent, 0);
                setUsbStorageNotification(
                        com.android.internal.R.string.usb_storage_notification_title,
                        com.android.internal.R.string.usb_storage_notification_message,
                        com.android.internal.R.drawable.stat_sys_data_usb,
                        sound, true, pi);
            } else {
                setUsbStorageNotification(0, 0, 0, false, false, null);
            }
        } catch (RemoteException e) {
            // Nothing to do
        }
    }

 

UsbStorageActivity Activity로 Intent를 전달하는 구조로 되어 있다. UsbStorageActiviy는 다음 위치에서 확인할 수 있다.

 

/frameworks/base/core/java/com/android/internal/apps/UsbStorageActivity.java

 

이 파일의 주된 동작은 다이얼로그를 사용해서 사용자의 입력을 받도록 되어 있다. 이에 대한 내용은 추후 다시 정리한다.