2015年8月27日 星期四

error: manifest required for this command -- please run init

在做repo sync時,遇過一次這樣的問題 (error: manifest required for this command -- please run init),如果這個message有特別顯示在某一行的話,像是 (301/665),可能在checkout第301個project出問題,可以將那個project的目錄整個砍到,再重做一次repo sync,但是如果所有的project都fetch完了才出現這問題,也沒特別指明是哪個project,則可以把所有的project目錄砍掉(不要動到.repo目錄),再重做一次repo sync即可。.repo/project.list裡有所有project的列表。

2015年8月23日 星期日

Disk encryption process

在Android L裡,有兩種方式可以啟動Disk Encryption,一種是系統第一次開機時自動做encryption,這種方式是由OEM所設定,另一種則是經由Settings UI,這種方式是由end user決定是否要做encryption。當然,你也可以使用vdc直接要求vold將disk做encryption。目前Android只支援一個partition的encryption,也就是/data這個partition,/system partition的encryption目前並不支援,但其實要支援好像也不是什麼難事,只是有沒有這個需要而己。

並不是每個partition都可以做encryption,基本上,OEM必須在fstab裡對該partition標註forceencrypt或encryptable,該partition才能做encryption。如果標註forceencrypt的話,在Android第一次開機時就會將該partition做encrypt,如果是encryptable的話,則交由user決定。

針對被encrypt的partition,系統必須記錄一些資料(像是key相關資料)才能正常的管理它,這些資料稱meta data。Meta data可以存放在該Partition裡,也可以儲存在不同的partition。前者Android稱為Footer,因為這些資料是存放在該partition的最後16K,也就說user可使用的空間必須扣掉16K。

下面是fstab裡的UDA partition的描述,Android在第一次開機時,會強制對UDA這個partition做encrypt, meta data會存放在MD1這個partition。這個UDA partition的file system是f2fs

/dev/block/platform/sdhci-tegra.3/by-name/UDA  /data  f2fs  noatime,nosuid,nodev,errors=recover wait,check,forceencrypt=/dev/block/platforom/sdhci-tegra.3/by-name/MD1

forceencrpyt可以使用encryptable取代,encryptable代表這個partition可以被encrypt,但是不會在第一次開機就強制執行,而是交由使用者去選擇。如果OEM不想用另一個partition儲存meta data,可以用"footer"這個字(forceencrypt=footer),Android會將UDA partition的最後16K保留,用來存放meta data.

Service Class

在開始了解Encryption process之前,我們要先知道Init process,因為不管是Encryption Process或Encryption Booting,都跟Init Process有很大的關係。大部份用來Customize Init Process的設定都是寫入rc file。rc file可以定義Service,而每一個Service可以屬於某個class。在Android裡,定義了三種class,分別是core, main與late_start。一般來講, 屬於core class的service在啟動之後就不會被stop或restart,屬於main或late_start這兩個class的service則是有可能視情況需要而stop或restart。像是healthd, vold或surfaceflinger是屬於core class,所以除非這些service自己crash,Android並不會故意將它stop或restart,但是main與late_start這兩個class就不一定,像是encryption process或encryption booting就會將這兩個class停止或重啟。至於一個service應該定義在哪個class,則是應該視該service的用途來做判斷。以目前Android的設定,當main class啟動完成之後,重要的service都己經啟動了,user也可以看到launcher的畫面。稍後會看到encryption或encryption booting時為了要顯示UI,會先將main class啟動,而late_start則是在/data己經mount好之後才啟動。

Triggering Encryption & Encryption Booting

Encryption Process的觸發可分別兩種,一種是OEM在fstab裡指定forceencrypt參數,這是由Init Process在第一次開機(或factory reset後的第一次開機)後自動觸發。另一種則是開機後由App或User從Settings裡去觸發。不管是哪一種,最後Encryption都是由vold, init process與app/framework來共同完成。init process與vold及framework之間的溝通是透過property。

當init process執行mount_all時,它會fork一個process根據fstab內容去做mount,這是為了避免mount出現任何問題以致於init本身crash。child process做完mount之後就會結束,並將mount結果傳回給init process, init本身在fork之後則是會等child process結束才繼續。child process的結果有以下幾種

FS_MGR_MNTALL_DEV_NEEDS_ENCRYPTION
表示某個partition (其實就是指/data)被標註forceencryption,但是還沒被encrypt,init process會將vold.decrypt這個property內容設為trigger_encryption,表示要啟動Encryption process。

FS_MGR_MNTALL_DEV_MIGHT_BE_ENCRYPTED
表示Encryption process己經做過了,init process會設定下面幾個property的內容進行Encryption booting的程序

ro.crypto.state = encrypted
vold.decrypt = trigger_default_encryption


FS_MGR_MNTALL_DEV_NOT_ENCRYPTED
表示沒有任何一個partition被encrypt,而且也沒有任何一個partition被標註forceencrypt (有可能有標註encryptable)。這就是一般沒做partition encryption的開機流程。init process會將ro.crypto.state設為unencrypted,然後觸發nonencrypted的action。nonencrypted的action就是啟動main與late_start這兩個class的service。

< from rc file >
on nonencrypted
    class_start main
    class_start late_start

FS_MGR_MNTALL_DEV_NEEDS_RECOVERY
這表示child process沒辦法將partition正常mount上,有可能是encryption發生不正常的現象,init process必須wipe partition (將partition內容清除,也就是factory reset),系統會進入recovery mode。

除了由init process觸發的encryption (或factory reset)之外,也可以由Setting裡的CryptKeeperConfirm來觸發。User可以從Setting選擇要做disk encryption,最後會由CryptKeeperConfirm呼叫MountService.encryptStorage(),MountService再要求vold執行"cryptfs enablecrypto inplace "命令開始做disk encryption。

如果我們比較forceencrypt與user從Setting觸發的encryption command,可以發現一點差異,稍後看到encryption的過程再來討論。
  • forceencrypt - "cryptfs enablecrypto inplace default"
  • Settings - "cryptfs enablecrypto inplace "

Encryption Process

前面提到Encryption process可以由init process觸發,也可以由Settings觸發,兩者其都是要求vold去執行"cryptfs enablecrypto inplace" command,差別在於參數略有不同而己。

下圖是一張粗略的Encryption流程,過程大致了解再來深入幾個比較有意思的地方。星星標註的地方代表可以觸發Encryption,也就是可以從Settting與開機時由Init process觸發 (forceencrypt)。不管哪一個,都是下command到vold。Vold command分成幾個類別,cryptfs這一類用來處理跟encryption相關的。



vold在做encryption時主要是有下面幾個步驟
    1. shutdown framework
    2. unmount all asec
    3. unmount sdcard
    4. unmount /data
    5. mount tmpfs on /data
    6. initialize /data through init post_fs section
    7. set encryption progress property to 0
    8. Show progress UI
    9. encrypt each sector while updating progress property
   10. Reboot system when all sectors encrypted

Encryption的做法在概念上很簡單,以ext4來講,只要將/data這個partition的每個sector加密過就完成了,簡單講就是像下面這個樣子,也就是上面step 9。

for (int i = 0; i < sector_count; ++i) {
    encrypt(sector[i]);
}

之所以要有step1 ~ step8主要就是要避免有process正在使用/data,因為/data這個partition就是我們要做encryption的對像。另一個原因就是希望將Encryption的進度以UI的方式顯示,讓user知道系統還活著,Encryption會花點時間,Step 5~9主要就是為了UI的關係。

Step 1 - Shutdown framework
Shutdown framework是將vold.decrypt設為trigger_shutdown_framework,init process收到後會將main與late_start這兩個class services停止,這是為了避免framework或app正在使用/data partition。

Step 2 - Unmount all asec
Asec是android secure container,這是加密過的資料,當user將app移到sdcard時,系統會在sdcard上產生一個asec檔案,framework會將它mount在/data裡,如果不將asec unmount的話,最後/data也無法unmount,encryption就無法進行。

Step 3 - Unmount sdcard
Sdcard的access是透過FUSE的架構,有一個sdcard service用來處理sdcard files的access,基本上user process是access /storage/emulated/,透過bind mount的方式,這些access會被導引到/mnt/shell/emulated/,而這是/dev/fuse的一個mount point,因此這些access會由/dev/fuse的driver處理,而/dev/fuse會將這些access再導引至sdcard service。sdcard service會再將這些access導引到真正sdcard的mount point,也就是/data/media。因此,如果sdcard沒有unmount的話,/data也沒辦法unmount。

Step 4 - Unmount /data
做encryption前,/data必須被unmount,避免一邊使用一邊Encryption的事情發生。

Step 5 - Mount tmpfs on /data
tmpfs是用在RAM的file system,也就是割出一塊RAM,initialize成tmpfs format,並mount在/data,這主要是為了顯示UI做準備。目前顯示UI的方式是啟動framework,執行一個特殊的Launcher,它的用途就是顯示目前Encryption的進度,當然它還有其它用途,但以Encryption而言,這是它其中一個用途。可是framework的運作是需要/data存在,而且還必須有一些資料設定好,因此,vold才會產生一個暫時的/data供framework使用。

Step 6 - Initialize /data through post_fs
上面提到vold將一塊RAM mount在/data供framework使用,但是不是只要提供/data就好了,/data裡面還是要有一些設定,這些設定就由rc file的post_fs section來處理,因此,vold會將vold.decrypt設為trigger_post_fs_data,init process就會開始執行rc file的post_fs section,進而觸發其它section的命令。

Step 7 - Set encryption progress to 0
由於Encryption還沒開始,所以它的progress是0,這個progress要能讓framework app/launcher可以看到,目前是使用一個property (vold.encrypt_progress)當作彼此溝通的媒介。

Step 8 - Show progress UI
vold會將vold.decrypt設為trigger_restart_min_framework,init process會將main class services啟動,launcher也會被啟動,但被啟動的launcher是一個特殊的launcher,在Encryption Booting這個section,會有比較詳細的討論。簡單的講,這個Launcher會根據vold.encrypt_progress內容顯示encryption進度。

Step 9 - Encryption for each sector
這個步驅主要就是產生一個crypto device (dm-crypt driver),並將它map到原本的/data partition,把原本的/data partition的每個sector讀出來,寫到crypto device,dm-crypt driver會將寫入的sector做encryption,再寫入原來的/data partition相同的sector,這樣就完成了一個sector的encryption,每個sector都按照一樣的方式處理過就完成了encryption,稍後我們會再進一步討論這部份。在Encrypt每個sector的時候,vold也會持續更新vold.encrypt_progress內容,因此Launcher也可以讓user知道最新的進度。

Step 10 - Reboot system
當encryption完成後,vold會直接reboot system,它會留個幾秒讓Launcher顯示到100%才reboot system。


Encryption Password

在KK,password有兩種,一個是screen password,一個是encryption password,前者用來解除screen lock,後者用在開機時驗證encryption password。但是到的L的時候,這兩個password被合而為一。在KK,當user啟動disk encryption時,framework會要求user提供一組encryption password,但是在L的時候,就不再詢問user了,它會直接使用usr設定的screen password,如果user也沒設定screen password,就會直接用預設的password,可是當user更改screen password時,encryption password也會跟著更改。

有一個容易造成誤解的是encryption password並不是用來做為disk encryption的key。當disk encryption開始時,framework會產生一個key,這個key才是disk encryption時所要用的,我們稱為master key,user所輸入的password是用來加密master key而己。而加密後的master key會儲存在Meta data裡,因此,每次開機,framework都需要向user詢問encryption key,這樣它才能從meta data裡讀出加密過的key,再將它解密,然後交給dm-crypt,系統才能正常booting。


dm-crypt

dm-crypt是Linux device mapper的一種target,它可以做到transparent encryption/decryption,device mapping是一種mapping的架構,它會產生一個virtual device,並設定一個mapping table,所以當user在access這個virtual device時,它會將這個access轉介到另一個device,並根據mapping table改變要access的區域。以dm-crypt來講,假設真正儲存/data的device是B,我們會產生一個dm-crypt device,假設名字是dm-0,而mapping table會設定為dm-0的第N個sector對應到B的第N的sector。Disk encryption就會變成,將B的每個sector讀出,並寫入dm-0的每個sector。B的內容讀出後是還沒做過encryption的,但是寫到dm-0之後,dm-crypt會對寫入的資料做encryption,並寫入B的同樣sector,這樣一來,B的內容就是加密過的,如果這個時候我們直接從B再讀取一次,就會得到加密過的資料,但是如果我們透過dm-0去讀,dm-crypt會先讀B的內容,再將它解密,然後再回傳。如果我們寫入資料到dm-0,dm-crypt就會將寫入的資料加密,再寫到B的相同sector。所以,在encryption過的系統,/data是mount在dm-crypt的device,而不是原始的device,我們可以透過adb shell mount來觀察。

下面這段code是vold cryptfs的function,vold將dm-crypt的device產生出來後,會將它的path儲存在ro.crypto.fs_crypto_blkdev這個property裡,這個function再將它讀出,mount到/data下。

static int cryptfs_restart_internal(int restart_main) {
   ...
     property_get("ro.crypto.fs_crypto_blkdev", crypto_blkdev, "");
     if (strlen(crypto_blkdev) == 0) {
         SLOGE("fs_crypto_blkdev not set\n");
         return -1;
     }

     if (! (rc = wait_and_unmount(DATA_MNT_POINT, true)) ) {
         /* If that succeeded, then mount the decrypted filesystem */
         int retries = RETRY_MOUNT_ATTEMPTS;
         int mount_rc;
         while ((mount_rc = fs_mgr_do_mount(fstab, DATA_MNT_POINT,
                                            crypto_blkdev, 0))
                != 0) {
             ....
         }
         ....
     }
}


Encryption Booting

下面這張圖描述Encryption Booting的過程。Init process根據fstab在mount所有的partition時,如果disk己經被encrypt過,它會設定以下兩個property開始Encryption Booting。
  • ro.crypto.state = encrypted
  • vold.decrypt = trigger_default_encryption
trigger_default_encryption會啟動defaultcrypto這個service (one-shot service),而defaultcrypto則是純粹的使用vdc要求vold去執行"cryptfs mountdefaultencrypted"這個command。如果當初在做Encryption時,沒有指定password的話,vold這邊的動作比較單純,vold都會產生一個crypto device(dm-0),使用同樣的mapping table對應到userdata partition,並將dm-0 mount在/data目錄。因此,在Encryption之後,所有對/data partition的access都會在dm-0 device上,而dm-0則會將access導引之原本的data partition,並做encryption/decryption的動作。對user mode的process之言,並不會感覺有所變化,當然,performance或許有變,但使用上跟disk encryption之前沒有差別。

當crypto device被mount到/data後,vold會通知init process將persistent properties讀取進來(將vold.decrypt設為trigger_load_persistent_. Persistent properties (persist.*)是儲存在/data裡,因此,在Encryption Booting時,一直要等到/data被mount後才能將它讀出來。有一點要注意,這個時core class service都己經被啟動,如果core class service有用到persistent properties,它就讀不到真正的property內容。

接下來,vold會通知init process開始執行rc file裡post_fs這個section裡的command(將vold.decrypt設為trigger_post_fs_data),最後才要求init process啟動framework(將vold.decrypt設為trigger_restart_framework),而所謂啟動framework就是將main與late_start這兩個class裡的service啟動,main啟動完後,user就可以看到launcher的畫面了。




但是如果當初在做Encryption時,如果user有設定password的話,那狀況會比較複雜一點,因為必須將framework啟動,顯示UI詢問password。所以在vold收到cryptfs mountdefaultencrypted這個command時,它會要求init process先將main class service啟動(將vold.decrypt設為trigger_restart_min_framework)。前面有提到當main class啟動完成後,user就會看到Launcher畫面,也就是說user會看到UI詢問password,得到password後,UI會直接使用MountService.decryptStorage()來與vold確認password是否正確。decryptStorage()這裡會做兩件事,它會利用cryptfs checkpw這個command要求vold確認password是否正確,如果正確的的話,它會利用cryptfs restart這個command要求vold繼續Encryption Booting的程序。

這裡有一個比較tricky的地方,當main class啟動時,理論上user看到的第一個畫面是launcher,那為何在trigger_restart_min_framework時,會出現問user password的畫面? 原因在於這個系統裡其實至少有二個Launcher,一個是一般user使用的launcher,像是Launcher3或GoogleLauncher,另一個Launcher就是用來顯示詢問Password畫面,這個Launcher就是Settings app裡其中一個activity (CryptKeeper)




CryptKeeper有HOME這個category,所以它會被系統認定為Launcher類別,而且它的priority是10,高於一般的Launcher。因此,如果沒有特別的處理,CryptKeeper會被當做系統預設的Launcher,但事實上,除了在Encryption Booting時你會看到它詢問密碼,在其它時刻它並不會出現。原因在於這個Activity在第一次開機時就將自己本身Disable,所以framework會去使用第二順位的Launcher。但是為何它又能被執行顯示詢問password的畫面?因為當component被disable時,這個資料會儲存在/data裡,而framework被restart來顯示password晝面時,/data還沒被mount起來,因此這個時候相當於這個component從沒被disable過。

         final String state = SystemProperties.get("vold.decrypt");
         if (!isDebugView() && ("".equals(state) || DECRYPT_STATE.equals(state))) {
             // Disable the crypt keeper.
             PackageManager pm = getPackageManager();
             ComponentName name = new ComponentName(this, CryptKeeper.class);
             pm.setComponentEnabledSetting(name, PackageManager.COMPONENT_ENABLED_STATE_DISABLED,
                     PackageManager.DONT_KILL_APP);
             // Typically CryptKeeper is launched as the home app.  We didn't
             // want to be running, so need to finish this activity.  We can count
             // on the activity manager re-launching the new home app upon finishing
             // this one, since this will leave the activity stack empty.
             // NOTE: This is really grungy.  I think it would be better for the
             // activity manager to explicitly launch the crypt keeper instead of
             // home in the situation where we need to decrypt the device
             finish();
             return;
         }


cryptfs restart這個command會要求vold將main class裡的service停止(reset main),因為這時候/data還沒mount起來,所以,可能有些service讀取到的設定並不正確(像Launcher就不是user所期待的),因此這個時候將main class reset,稍後等/data mount起來,再重新啟動main class才會讓main class的service讀取到正確的設定。reset完main class後,就跟前面提到的順序一樣,mount /data,讀取persistent properties,執行rc檔的post_fs命令,然後啟動framework。

Uncrypt (OTA Update)

在KK的時候,OTA package是download到/cache裡,因此在recovery mode時,只要從/cache讀出OTA package內容,就可以直接做OTA update。/cache並不會被encrypt,所以即使/data被encrypt,OTA update也不受影響。但是這會有一個問題,/cache的size必須夠大,要不然可能無法容納OTA package。OTA package內容主要就是/system的內容,因此,在最差的情況下,OTA package的大小可能會跟/system partiton的大小類似,其實這是有點浪費空間的,畢竟除了OTA package之外,/cache也用不了那麼多的空間。

在L之後,OTA package可以被放在/data裡,在recovery mode時再將它讀出做OTA update。這似乎沒什麼大問題,但其實有兩件事值得思索一下
     1. 由於OTA package放在/data,因此,recovery mode必須將/data mount起來。
     2. 如果/data被encrypt,recovery mode就得想辦法將/data decrypt才能讀出OTA package。

第一件事似乎沒什麼大不了,反正就mount /data就好,我想也是,但似乎Google不怎麼想在recovery mode將/data mount起來,或許有什麼原因我們不知道。第二件事就比較麻煩,因為,recovery mode必須知道encrypt key的內容並將dm-crypt的device mount起來,並設定好mapping table,但這些事都是在vold做的,Google大概不想再搬一份相同的code到recovery mode裡。因此,在L做OTA update時,它會產生一個map file放在/cache裡,把OTA package (zip file)所使用到的block記錄下來,內容類似下面這個樣子

      /dev/block/platform/msm_sdcc.1/by-name/userdata     # block device
      49652 4096                        # file size in bytes, block size
      3                                 # count of block ranges
      1000 1008                         # block range 0
      2100 2102                         # ... block range 1
      30 33                             # ... block range 2

因此,在做OTA update時,只要根據這個map file,直接讀取/data所屬的block device內容就可以了。但是/data如果被encrypt過怎麼辦?當系統要reboot到recovery mode之前,它會執行一個做uncrypt的service,這個service就是用來產生map file,如果/data是被encrypt的,它會先從/data將OTA package所使用的block讀出(這個時候/data是mount在dm-crypt device上,所以讀出來的內容是decrypt過的),再將讀出的block直接寫入原本/data所屬的block device的相同block。所以在recovery mode時,系統只要按照這個map file就可以讀出OTA package的內容,即使/data己經被encrypt過。

這樣還是有一個問題,它會造成OTA update在reboot到recovery mode前有一些delay,目前framework預設是5分鐘,但是,如果這個platform decrypt的速度不快的話,可能會造成uncrypt還沒處理完,系統就會被shutdown (因為ShutdownThread.rebootOrShutdown會呼叫PowerManagerService.lowLevelReboot來reboot系統,但是如果timeout的話,它會認為reboot失敗,它就會將系統shutdown),然後OTA update就會失敗。下面這段code是PowerManagerService.lowLelvelReboot,當它被要求要reboot到recovery mode,它會要求init process啟動uncrypt,並開始計時,它最多會等個5分鐘讓uncrypt把事情做完,如果做不完,系統會被shutdown。

     public static void lowLevelReboot(String reason) {
         if (reason == null) {
             reason = "";
         }
         long duration;
         if (reason.equals(PowerManager.REBOOT_RECOVERY)) {
             // If we are rebooting to go into recovery, instead of
             // setting sys.powerctl directly we'll start the
             // pre-recovery service which will do some preparation for
             // recovery and then reboot for us.
             //
             // This preparation can take more than 20 seconds if
             // there's a very large update package, so lengthen the
             // timeout.  We have seen 750MB packages take 3-4 minutes
             SystemProperties.set("ctl.start", "pre-recovery");
             duration = 300 * 1000L;
         } else {
             SystemProperties.set("sys.powerctl", "reboot," + reason);
             duration = 20 * 1000L;
         }
         try {
             Thread.sleep(duration);
         } catch (InterruptedException e) {
             Thread.currentThread().interrupt();
         }
     }

啟動uncrypt的方法是將sys.powerctl設為pre-recovery,在rc file裡,pre-recovery是一個oneshot service,它會直接執行uncrypt這個命令。uncrypt產生完map file後,它會直接reboot系統。

service pre-recovery /system/bin/uncrypt
    class main
    disabled
    oneshot

2015年4月28日 星期二

Android External Storage

Android裡有兩種external storage, 一種是模擬出來的, 它是使用internal storage的空間, 另一種是真正的external storage, 像是sdcard與usb drive. 不管是哪一種, 它們都是使用一個service透過FUSE來管理的, 這個service叫做sdcard. 為了避免與external sdcard造成混淆, 我把它叫做sdcard service, external sdcard叫做sdcard storage. 如果你的系統支援sdcard與usb drive, 通常在rc檔裡會發現這三個service

    service sdcard /system/bin/sdcard -u 1023 -g 1023 -l /data/media /mnt/shell/emulated
        class late_start

    service fuse_sdcard1 /system/bin/sdcard -u 1023 -g 1023 -w 1023 -d /mnt/media_rw/sdcard1 /storage/sdcard1
        class late_start
        disabled
   
    service fuse_usbdrive /system/bin/sdcard -u 1023 -g 1023 -w 1023 -d /mnt/media_rw/usbdrive /storage/usbdrive
        class late_start
        disabled

這三個都是sdcard service, 使用同一個執行檔。每一個負責一個external stroage, 像是sdcard service負責模擬的external storage, fuse_sdcard1負責第一個實體的sdcard, fuse_usbdrive負責usb storage.

sdcard service
這個service的source code放在$TOP/system/core/sdcard下, 啟動這個service需要幾個參數
    -u:   service執行時所擁有的user id權限
    -g:   service執行時所擁有的groupd id權限
    -w: 擁有寫入storage空間的app id
    -d:  設定為UNIFIED derive permission.
    -l:   設定為LEGACY derive permission
    source: 原始的storage的路徑
    target: app存取storage的路徑

以emulated external storage為例, user id與groupd id都設為1023, 也就是media_rw. source是在/data/media下, 而target則是在/mnt/shell/emulated.

    service sdcard /system/bin/sdcard -u 1023 -g 1023 -l /data/media /mnt/shell/emulated

sdcard service是利用FUSE架構, 它會將/mnt/shell/emulated mount在/dev/fuse上, 所以任何在/mnt/shell/emulated上的IO都會交給/dev/fuse的driver, 這個driver會將這些IO request交給sdcard service, sdcard service再將這些request, 利用API去存取/data/media. 對/data/media的存取又會交給它底層的device driver去完成, 如下圖所示.



但並不是所有的系統一定要支援emulated external storage, 如果EMULATED_STORAGE_TARGET這個環境變數不存在, 代表emulated external storage不支援.

一般而言, 在rc file會有以下幾個變數設定
     export EXTERNAL_STORAGE /storage/emulated/legacy
     export EMULATED_STORAGE_SOURCE /mnt/shell/emulated
     export EMULATED_STORAGE_TARGET /storage/emulated
     export SECONDARY_STORAGE /storage/sdcard1

EMULATED_STORAGE_TARGET代表的是App看到的路徑, EMULATED_STORAGE_SOURCE是mount在/dev/fuse上的file system, 這也是Vold所看到的路徑. 現在問題來了, APP所存取的路徑如何對應到EMULATED_STORAGE_TARGET ? 答案就在Zygote裡. 在Zygote裡, 當一個child process產生出來之後, 它會將EMULATED_STORAGET_TARGET指的到位置mount到EMULATED_STORAGE_TARGET裡 (利用mount binding), 所以當這個App去存取/storage/emulated時, 相當於去存取/mnt/shell/emulated. 這中間還有一點值得特別注意, 在做mount的時候, 也會把user id的值放到路徑裡. 因為App只能存取啟動它的user目錄. 假設現在有兩個user, id分別是0與1. 兩個user都啟動同一個App, user 0所啟動的App只能存取/storage/emulated/0, user 1所啟動的App只能存取/storage/emulated/1. 所以, 當Zygote在做mount binding時, 只會根據user id來做mount, 因此, user 1啟動App時, Zygote只會mount /storage/emulated/1, 而不會mount /storage/emulated/0, 所以, 即使App故意更改路徑去存取/storage/emulated/0也不會成功.

在上圖中,user 0與user 1各啟動同一個App, Zygote會在user 0的process裡,將/storage/emulated/0 binding到/mnt/shell/emulated/0,Zygote也會在user 1的process裡,將/storage/emulated/1 binding到/mnt/shell/emulated/1裡。在上面有提到,/mnt/shell/emulated是mount在/dev/fuse上,sdcard service會polling /dev/fuse得知是否有任何對/mnt/shell/emulated的取存,如果有,會利用API從/data/media裡讀寫裡要求的檔案或目錄。

下面這段是Zygote裡截取出來的,大家可以做個參考

>>>
The default platform implementation of this feature leverages Linux kernel namespaces to create isolated mount tables for each Zygote-forked process, and then uses bind mounts to offer the correct user-specific primary external storage into that private namespace.
At boot, the system mounts a single emulated external storage FUSE daemon at EMULATED_STORAGE_SOURCE, which is hidden from apps. After the Zygote forks, it bind mounts the appropriate user-specific subdirectory from under the FUSE daemon to EMULATED_STORAGE_TARGET so that external storage paths resolve correctly for the app. Because an app lacks accessible mount points for other users' storage, they can only access storage for the user it was started as.
This implementation also uses the shared subtree kernel feature to propagate mount events from the default root namespace into app namespaces, which ensures that features like ASEC containers and OBB mounting continue working correctly. It does this by mounting the rootfs as shared, and then remounting it as slave after each Zygote namespace is created.
<<<


Emulated external storage的access要經過3層的轉換(/storage/emulated -> /mnt/shell/emulated -> /data/media),而真正的external storage (sdcard storage或usbdrive)則是只需要二層的轉換 (/storage/sdcard1 -> /mnt/media_rw/sdcard1與/storage/usbdrive -> /mnt/media_rw/usbdrive),external storage的轉換完全由sdservice一手包辦,不需要在Zygote做手腳。



2015年4月11日 星期六

Android Brightness

Brightness有兩種模式, 一種是App根據使用者或Window Manager Service(WMS)的要求去設定, 另一種則是透過Ambient Light Sensor(ALS)測得環境的亮度算出適當的Brightness值, 前者稱為手動模式, 後者又稱為自動模式. 在自動模式下, WMS/APP提供的Brightness不會被採用, PMS會根據ALS讀出的值, 算出要設定的Brightness, 最後再經由Light HAL設定到硬體裡.



上圖顯示的是WMS與APP如何通知PMS新的Brightness值. PMS提供兩個API給WMS與APP使用. 之所有用兩個API是因為優先權的問題. 如果WMS與APP同時要求改變Brightness, WMS的要求被會採用. 對APP而言, 除了setTemporaryScreenBrightnessSettingOverride之外, 其實還有一個方法是透過SettingProvider. 為何要提供兩個途徑供APP使用呢? 原因是setTemporaryScreenBrightnessSettingOverride是希望提供APP一個暫時改變Brightness的方式(像是提供Slider暫時改變Brightness讓user知道效果), 最後的設定是將值寫入SettingProvider裡的screen_brightness裡. PMS有設定Setting Observer, 所以當screen_brightness改變時, 它會收到通知, 並將screen_brightness的值反應到硬體裡. 我個人看不大出為何要提供APP兩個方式去設定Brightness, 感覺似乎只要是使用SettingProvider就好, 不過或許透過這兩種方式Brightness反應的時間有所不同吧.

不管WMS或APP所要求的Brightness是什麼, 在自動模式底下, PMS會根據ALS的值算出適當的Brightness, 並忽略WMS/APP的要求. 這種做法是否比較好, 見人見智. 或許應該要考慮將自動模式改成以user的選擇為基準, 再根據ALS的值做調整.

Code Flow
PMS會將WMS/APP的要求, 用一個DisplayPowerRequest的結構描述, 並交由DisplayManagerService (DMS)的DisplayPowerController來處置. DisplayPowerController的updatePowerState就是主要處理的function. 我們可以將updatePowerState分成兩個部份來看, 一個是screen的state update, 另一個則是brightness update. 這兩個state的update是同時進行的, 並以Animation的形態進行, 這樣user才不會覺得畫面的改變太突兀. 在updatePowerState裡, 會決定display state與brightness值, 並啟動這兩個animation, user就會慢慢看到screen state與brightness改變.

Screen State Animation
Screen state有以下四種, 分別代表不同的意義
  • STATE_ON
  • STATE_OFF
  • STATE_DOZE
  • STATE_SOZE_SUSPEND
monitor的狀態不是ON就是OFF, 突然由ON->OFF或OFF->ON會讓user覺得不夠smooth, 因此, CodeFade是一個Software的方式, 藉由改變畫面讓user覺得monitor漸漸關掉或漸漸打開. 所謂的screen state animation其實是利用另一個View (ColorFade)蓋住其它的Window, 然後一直改變Fade Level (0.0 -> 1.0或1.0 -> 0.0), 最後將monitor的打開或關掉. 這種漸漸改變的方式很適合利用Android Animation架構. animateScreenStateChange()就是用來啟動這個Animation.

Brightness Animation
一般來講, Brightness的值是介於0~255之間, 如果一個一個Level改變, user會覺得畫面亮度的改變不會太突兀, 但是user可能從80直接調到200, 因此, 當一個新的brightness值要改變時, AOSP也是利用animation的方式慢慢改變. animateScreenBrightness會觸發brightness animation.

DisplayPowerController算出screen state與brightness後, 就會啟動這兩個animation. 這兩個animation的執行沒有dependency, 但由於都利用Animation的架構, 所以當一個Animation進行一個frame時, 另一個Animation也會在很接近的時間處理下一個frame. 但這兩個animation也是有需要才會使用, 比方說, 如果只是brightness改變, 只要使用brightness animation即可, 不需要啟動screen state animation. 但 是當monitor變成OFF時, brightness會被設成0, 因此在screen off時, 這兩個animation同時會啟動.


Settings
Brightness有一些default值的設定, OEM可以利用這些設定預設的brightness值

config_screenBrightnessSettingMinimum
config_screenBrightnessDoze
config_screenBrightnessDim
config_screenBrightnessDark
config_animateScreenLights


APP可以將brightness的值或相關的設定寫入SettingProvider的database裡, 這個database位於
/data/data/com.android.providers.settings/databases/settings.db裡, 下面這3個跟brightness有關
  • screen_brightness - APP所設定的brightness值
  • screen_off_timeout - Monitor自動關掉的時間, 單位是milliseconds
  • screen_brightness_mode - 自動模式或手動模式
在Debug時, 我們可以利用adb讀取這些值, 下面可以讀取APP所設定的brightness值
adb shell sqlite3 /data/data/com.android.providers.settings/databases/settings.db "select value from system where name='screen_brightness';"

下面則可以寫入monitor timeout時間
adb shell sqlite3 /data/data/com.android.providers.settings/databases/settings.db "update system set value='600000' where name='screen_off_timeout';"

有時候Debug時, 我們也可以用下面的command來調整brightness的值, 而不用真的去操作系統.
    adb shell input keyevent KEYCODE_BRIGHTNESS_DOWN
    adb shell input keyevent KEYCODE_BRIGHTNESS_UP

要注意一點, 當我們使用上面兩個command時, SystemUI會將brightness模式改成手動模式.

FYI. 可以用下面的command打開monitor, 並將系統lock或unlock
    adb shell input keyevent 26 --> power key
    adb shell input keyevent 82 --> unlock
    adb shell input keyevent 82 --> lock

2015年3月20日 星期五

BufferQueue Address Space

Android因為Binder的關係, 使得IPC的使用變得很直覺, 但有時也會讓Debug變得令人困擾. BufferQueue是其中一種. BufferQueue通常用來做Producer與Consumer兩個Process之間的溝通. AOSP提供一些helper class簡化developer的工作. 一般而言, BufferQueue是這樣產生的

這會產生一個BufferQueue以及producer與consumer, 但是要注意的是producer與consumer是代表BufferQueue兩端使用的interface (AOSP有兩個class實作這兩個interface), 並不是代表真正的Producer與Consumer物件. 簡單的說, 真正的Producer object只能使用producer這個interface來操作BufferQueue, 而真正的Consumer object則只能使用consumer這個interface來操作BufferQueue. 它的用意是希望developer可以專注在真正Producer與Consumer的部份. 由於AOSP提供大部份的implementation, 所以Producer與Consumer的實作就可以大大簡化. 實作是簡化了, 但是Debug並不見就簡單.


一般而言, BufferQueue的產生是在Consumer的Process裡, Consumer再將producer interface傳回給Producer process, 兩者的溝通就開始了, Producer是透過Binder使用producer interface, Consumer則是與consumer helper執行在同一個process裡. 從上圖來看, 大部份的BufferQueue的code都執行在Consumer process裡, 之所以要強調這件事是因為當我們在debug的時候, 有時候會搞不清楚某些code在哪個process執行, 再加上AOSP裡, 有些class的名字其實很類似, 尤其是trace到BufferQueue以及它的helper class裡.

2015年3月15日 星期日

Factory Reset Protection

Android L MR1新增了一個功能叫做Factory Reset Protection. 這個功能是為了避免手機或平板被偷走, 然後做Factory Reset, 將機器據為己用. 這個功能概念其實很簡單, 只要想辦法得知做Factory Reset的人是否是機器的主人或被機器的主人授權即可. 問題來了, 要怎麼知道呢? 很簡單, 只要使用者能夠解除Lock Screen, 就代表他被授權, 它就能從Settings裡啟動factory reset. 這種reset稱為trusted reset.  可是系統可不是只有這條路徑可以做factory reset, 利用fastboot一樣可以將/data清除掉, 跟factory reset有一樣效果. fastboot在清除/data之前必須先做OEM unlock的動作. 因此, 必須避免未經授權的oem unlock被執行. 如何避免呢?

在MR1裡, 要啟動factory reset partition的功能, OEM必須設定一個partition, 並mount起來, 將它的路徑寫在ro.frp.pst這個property裡. Framework新增一個PersistenceDataBlockService, 用來管理這個Partition. 它的layout如下圖所示.


Byte 0~31是利用SHA-256計算的checksum, marker是一個4 byte的magic number (0x19901873), size則是payload的大小, Partition最後一個byte稱為oem unlock. 如果這個是1, 代表這個機器的主人允許fastboot是做oem unlock, 如果是0則不允許. 所以, 當使用者是fastboot裡要執行oem unlock時, OEM必須檢查這個byte, 確認它的值是1才能做oem unlock. 在Developer option裡有一個選項是允許oem unlock. 如果使用者可以解除lock screen, 進到developer option打開這個選項, 就表示oem unlock是被授權的.

另一個問題來了, 如果機器的主人忘了解除unlock screen的密碼, 而且也沒在developer option裡允許oem unlock怎麼辦? 別擔心, Google有Android Device Manager, 透過網路, 它可以鎖定你的手機或平板, 使用者可以遠端將他的機器做factory reset. 基本上要啟動這樣的功能, 使用者的機器必須裝有Google GMS的軟體, 並有Gmail的帳號登錄. 不過, 對大部份的Android 機器而言, 這應該都不是問題.

在上圖的layout中, payload的用途主要是GMS在使用的, 它如何使用不得而知. PersistenceDatatBlockService有提供interface允許GMS讀寫這塊區域, 但是不是只用GMS可以寫, OEM也可以設計APP去讀寫這一塊, 但要避免和GMS同時使用.

2015年2月24日 星期二

Android Layer && LayerStack

In SurfaceFlinger, the basic logic unit to show is a Layer (which corresponds to one ViewRootImpl in app process). The contents users see is a set of Layers with depth information. With depth info, some Layer displayed in front, while some in back, and this depth info is the z value. The larger, the nearer to user. WindowManagerService can adjust the z value through SurfaceControl.setLayer, which conveys this info to SurfaceFlinger so create different effect. For example, user can enable StrictMode violation flash in developer option, once there is violation, WMS creates a Layer, and make it visible for a while, then make it invisible. To prevent other Layer from hiding the flash effect, the z value of StrictMode Layer is adjusted so that it would be displayed on top to get attention.

The above works perfect for Layers showing on single display. When multiple displays are supported, we need a way to know which Layer to show on which display, hence the LayerStack. LayerStack is an attribute for both a Layer object and a display. When a Layer has same LayerStack as a display, it would be showed on that display. Effectively, LayerStack creates an depth space for a display, so that each display has its own depth space. One immediate use case for LayerStack is mirror mode. When a user plugin a HDMI to device, both displays shows the same content (mirror mode). WMS just set the LayerStack for HDMI display to the same value as panel, so that when SF composes contents for HDMI, it picks up same set of Layers as primary display.

Display LayerStack
In framework, a display device is described by a subclass of DisplayDevice. There are several different subclass of it, let's make it simple, using LocalDisplayDevice as an example. A LocalDisplayDevice means a physical device that can connect to the tablet or phone. Internal panel and HDMI are examples of LocalDisplayDevice. When a display is connected, framework creates an object of LocalDisplayDevice, and enumerates a LayerStack for this device (now LayerStack is same as display id, but they do not need to be the same), then using this LayerStack, it creates an associated LogicalDisplay object. One LogicalDisplay associates with one LocalDisplayDevice, and LogicalDisplay decides the LayerStack to use, then tell SurfaceFlinger through LocalDisplayDevice, LocalDisplayDevice probably can be think of an interface to SF for display related setting. So, basically if you connect HDMI on your device, you'll have 2 LogicalDisplay, each with its own LayerStack. But in mirror mode, LayerStack of both display is the same, what happened ? The trick is in DisplayManagerService.configureDisplayInTransactionLocked()

    private void configureDisplayInTransactionLocked(DisplayDevice device) {
        final DisplayDeviceInfo info = device.getDisplayDeviceInfoLocked();
        final boolean ownContent = (info.flags & DisplayDeviceInfo.FLAG_OWN_CONTENT_ONLY) != 0;

        // Find the logical display that the display device is showing.
        // Certain displays only ever show their own content.
        LogicalDisplay display = findLogicalDisplayForDeviceLocked(device);
        if (!ownContent) {
            if (display != null && !display.hasContentLocked()) {
                // If the display does not have any content of its own, then
                // automatically mirror the default logical display contents.
                display = null;
            }
            if (display == null) {
                display = mLogicalDisplays.get(Display.DEFAULT_DISPLAY);
            }
        

        }
        ....
        display.configureDisplayInTransactionLocked(device, info.state == Display.STATE_OFF);        ....

    }

When mirror mode is in use, FLAG_OWN_CONTENT_ONLY is not set for LogicalDisplay of HDMI, so the LogicalDisplay used to configure HDMI monitor is that of internal panel (see above code), that's why in that case, the LayerStack in both display in SF are the same, hence showing the same contents.

LayerStack@Layer
Layer is the concept used in SurfaceFlinger. It has different face in different places. A Layer in SF has a corresponding WindowState in WMS, and a corresponding ViewRootImpl object in app process. When a ViewRootImpl object is created, it has an associated display, and this display decides the LayerStack value to be assigned by WMS and used in SF. In WMS, each WindowState object has its own WindowStateAnimator object, which ask SF to create a Layer object for it. Below code is from WindowStateAnimator.

    SurfaceControl createSurfaceLocked() {
        ....
        // Start a new transaction and apply position & offset.
        SurfaceControl.openTransaction();
        try {

            ....
            try {

                ....
                final DisplayContent displayContent = w.getDisplayContent();
                if (displayContent != null) {           

                mSurfaceControl.setLayerStack(displayContent.getDisplay().getLayerStack());
            }

            ....
        } finally {
            SurfaceControl.closeTransaction();
        }

       }

WMS maintains a DisplayContent object to store the list of WindowState object to show in this display. From DisplayContent, it can get the display object, then get the LayerStack of it, and assign this to the Layer object in SF.