2016/7/13

UWP - 操作 SD Card

過去開發衹能注冊處理檔案類型的方式,讀取 SD card 中的內容, 無法寫入任何資料。UWP 開放功能,可以利用 FilePicker 或是 FolderPicker 選到自己需要的檔案,感覺就非常有彈性。本篇將介紹如何使用。

要在 Package.appxmanifest 宣告 <uap:Capability Name="removableStorage" /> 才可以使用,如下圖:

<App capability declarations>定義宣告這個特性可以讓 app 能存取在 remoable storage(例如:USB keys 或 external hard drives)中宣告的檔案類型或是使用 file picker 來提供各種類型的檔案。

在 Mobile 衹有宣告使用 removable Storage 是不夠的,要記得注冊要處理的檔案類型,例如:

不然會遇到無法存取的 System.UnauthorizedAccessException 。


存取 SD card 的方式主要兩種:
  • File/Folder Picker
  • Windows.Storage API
  • 或是注冊特定的 fileTypeAssociation 關聯想要處理的檔案

[注意]
  1. 當 App 把檔案存放在 SD card,這些檔案不會被加密,其他 App 可以直接存取
  2. 接上 SD card 後,App 衹能讀寫注冊在 app manifest 中的特定檔案類型的檔案,跟 Desktop 連綫後也可以直接用 File Explorer 操作
  3. SD card 中系統的目錄或是檔案依舊無法看見或是讀取,包括被隱藏的檔案
  4. 如果 App 是直接被安裝在 SD card 裏面,寫在 LocalFolder 下的内容依舊會被機密,其他 App 不能使用
  5. 需要存取 Video, Music, Picture Library 内容的話,一樣要記得宣告對應的 capabilities,可參考<Files and folders in the Music, Pictures, and Videos libraries>
  6. 無法直接使用 KnownFolders.DocumentsLibrary,但是可透過 file system(或是 fileopenpicker, folderpicker) 與它連結
  7. 操作 SD Card 時,建議使用 background thread 去讀取或是寫入檔案,因爲 SD Card 大部分容量都很大,這樣做法可以減少畫面的等待
  8. 建議使用 folder picker 或是 file picker 取得特定的對象,減少大量查詢所耗的效能
  9. 利用 KnowsFolders.RemovableDevices 取得的對象,有兩個狀況:
    • GetFilesAsync method 衹會得到在 app manifest 注冊要處理的檔案類型
    • GetFileFromPathAsync method 時如果是給予未注冊的檔案類型,將會收到失敗

[範例程式]

1. 檢查設備是否已經有接上 SD card
public static async Task<bool> CheckHasExternalStorage()
{
    try
    {
        // 取得 logic root external folder
        StorageFolder externalDevices = KnownFolders.RemovableDevices;

        // 取得可用的 folders, 如果有 SD card 預設是第一個,
        // 但是 Desktop 可以接上多個 external storages 所以用 Count 檢查
        var folders = await externalDevices.GetFoldersAsync();

        return folders.Count > 0;
    }
    catch (Exception)
    {
        return false;
    }
}

KnownFolders.RemovableDevices folder 是一個 logical root StorageFolder, 代表目前設備有連結上的 removable devices,如果沒有 SD card 拿到的 Count 會是 0 或是 null。
預設 SD card 接上設備後預設第一個(或是唯一);如果是 Destop 的設備就不一定會是唯一值,需要特別處理有多少設備已經被連上


2. 取得 SD card 的唯一識別值
private async Task<bool> CheckSDCardId()
{
    StorageFolder externalDevices = KnownFolders.RemovableDevices;

    // 取得 SD card
    StorageFolder sdCard = (await externalDevices.GetFoldersAsync()).FirstOrDefault();

    if (sdCard != null)
    {
        // 取得 ExternalStorageId        
        var allProperties = sdCard.Properties;

        IEnumerable<string> propertiesToRetrieve = new List<string> { "WindowsPhone.ExternalStorageId" };

        var storageIdProperties = await allProperties.RetrievePropertiesAsync(propertiesToRetrieve);

        string cardId = (string)storageIdProperties["WindowsPhone.ExternalStorageId"];

        // 利用 ID 做一些判斷的邏輯或是相關的應用
    }

}

當 SD card 第一次被安裝時,系統會自動生成一個唯一識別碼,并將它寫成一個檔案儲存在 SD card 根目錄中的 WPSystem 目錄中。(但是這個在 UWP 目前無法識別)

App 可以使用這個 ID 確定它是否爲能夠識別這個卡片。如果 App 可以識別這個卡片,它就能從中推遲先前完成的某些操作。然而卡片中的内容可能被最後一個連接的 App 所改變。

要取得這個識別 ID 的 key 爲:WindowsPhone.ExternalStorageId 。更多相關的應用可以參考<WP8 - 操作External Storage API>的介紹。


3. 利用 FolderPicker 取得指定的目錄,并且加入 FutureAccessList  讓 App 有能力存取裏面的内容
由於 App 運作本身都是在 sandbox 的環境,預設 App 可讀寫的範圍衹有 LocalFolder 或是特定的範圍,確保 App 不會去影響其他系統或是不屬于這個 App 本身範圍的檔案與目錄。
換個角度來看,利用 FileOpenPicker / FolderPicker 是由用戶自己選擇提供哪些檔案與目錄給 App 有能力讀寫的話就不算在 sandox 的限制範圍内了。
因此,要怎麽讓 App 有能力存取到由 FileOpenPicker / FolderPicker 所選擇的目錄或是檔案呢,需要搭配 StorageApplicationPermissions 來指定。

系統提供一個 static properties 讓 app 可以設定專屬的 most recently used list (MRU, 最常使用的項目清單), 與 future-access list (將目錄檔案加入可以被存取的對象清單)。
Property Description
FutureAccessList
Read-only. Gets an object that represents a list that an app maintains so that the app can store files and/or locations (like folders) and easily access these items in the future.

StorageItemAccessList

經由 picking 得到的 files 或 folders,代表用戶認同 app 有權利存取它們,所以把得到的 StorageFile 或 StorageFolder 加到 future-access list,下一次 app 要操作它們就不需要再詢問過用戶。

This list can store up to 1000 items and must be maintained by the app. (重點)
MostRecentlyUsedList
Read-only. Gets an object that represents a list that an app can use to track the files and/or locations (like folders) that the app has accessed most recently.

StorageItemMostRecentlyUsedList

可以存放 app 最常使用的 IStorageItem。最多 25 個,超過的時候最先被加入的會被移除,處罰 ItemRemoved 事件。

This list can store up to 25 items. While the app must add items to the MRU in order to track them, Windows maintains the 25-item limit by removing stale items if necessary.

建議 app 要去維護加入 future-access list 或 most recently used list 得到的 token,它可以讓 app 直接快速得到 IStorageItem。更多關於檔案或是目錄存取權限的説明,可以參考<File access permissions>。

以 FolderPicker 爲例,讓用戶選擇 folder 加入讓 app 有能力可以控制它,并且加入維持有 1000 個數量的限制(利用時間來判斷):
public static async Task<StorageFolder> GetFolderFromPicker()
{
    StorageFolder folder = null;
    try
    {
        // 最大上限 1000, 要自己手動管理
        RemoveOldestFolderOfAccessList();

        var folderPicker = new FolderPicker();
        folderPicker.SuggestedStartLocation = PickerLocationId.MusicLibrary;
        folderPicker.FileTypeFilter.Add("*");
        folder = await folderPicker.PickSingleFolderAsync();

        if (folder != null)
        {
            // Application now has read/write access to all contents in the picked folder (including other sub-folder contents)
            // 相同目錄拿到的 token 是一樣的
            string token = StorageApplicationPermissions.FutureAccessList.Add(folder, DateTime.UtcNow.Ticks.ToString());                   
        }
    }
    catch (Exception)
    {
    }

    return folder;
}

private static void RemoveOldestFolderOfAccessList(int removeCount = 2)
{
    // https://msdn.microsoft.com/en-us/library/windows/apps/xaml/hh972344(v=win.10)
    if (StorageApplicationPermissions.FutureAccessList.Entries.Count >= StorageApplicationPermissions.FutureAccessList.MaximumItemsAllowed)
    {
        var removeItems = StorageApplicationPermissions.FutureAccessList.Entries.Select(x =>
            {
                long ticks = DateTime.UtcNow.Ticks;
                long.TryParse(x.Metadata, out ticks);
                return new Tuple<string, long>(x.Token, ticks);
            }).OrderBy(x => x).Take(removeCount);

        foreach (var item in removeItems)
        {
            StorageApplicationPermissions.FutureAccessList.Remove(item.Item1);
        }
    }
}


[補充]
  • 支援 BackgroundTask for Audio 的 App,利用 FileOpenPicker 離開 App,狀態馬上被 Suspended,在 Picker 返回時 App 已經從 OnResume 或 Launch 開始
過去開發 WP 使用 FileOpenPicker,在 App.xaml.cs 會使用 <How to continue your Windows Phone app after calling a file picker> 介紹的方式讓 FileOpenPicker 回來的内容可以被 App 繼續去用,維持好的體驗。但是 UWP 不支援 ContinuationManager,所以遇到 App 有宣告支援 Background Task for Audio 的時候馬上就會被 Suspended 了,如果沒有宣告就可以正常使用。那該怎麽處理呢?
參考<App Lifecycle - Keep Apps Alive with Background Tasks and Extended Execution>來延長 OnSuspened 的執行時間,等待用戶選擇回來(最長約 30 seconds 内,超過一樣沒有用),但是不是每次請求延長都會成功,還是要看當時系統是否有足夠的資源而定。

例如:
private async void StartTbTNavigationSession()
{
  using (var session = new ExtendedExecutionSession())
  {
    session.Reason = ExtendedExecutionReason.LocationTracking;
    session.Description = "Turn By Turn Navigation";
    session.Revoked += session_Revoked;
    var result = await session.RequestExtensionAsync();
    if (result == ExtendedExecutionResult.Denied
    {
      ShowUserWarning("Background location tracking not available");
    }
    // Do Navigation
    var completionTime = await DoNavigationSessionAsync(session);
  }
}

  • 如何取得目錄可用的與已用的空間
/// <summary>
/// 取得指令目錄路徑的 可用空間 與 全部空間,單位: MB.
/// </summary>
private static async Task<Tuple<long, long>> GetFolderSpaceInfo(StorageFolder storageFolder)
{
    try
    {    
        // 關鍵字 System.FreeSpace 與 System.Capacity
        var retrivedProperties = await storageFolder.Properties.RetrievePropertiesAsync(new string[] { "System.FreeSpace", "System.Capacity" });
        long free = Convert.ToInt64(retrivedProperties["System.FreeSpace"]);
        free = free / 1048576;
        long capacity = Convert.ToInt64(retrivedProperties["System.Capacity"]);
        capacity = capacity / 1048576;
        return new Tuple<long, long>(free, capacity);    
    }    
    catch (Exception)
    {
        return null;
    }
}

System.FreeSpace 與 System.Capacity 這兩個關鍵字可以從 StorageFolder 中取得可用與已用的空間資料。還有那些可以用的關鍵字呢,可以參考<Discover Properties of Storage Items in Windows Store Apps>與<Windows Properties>的介紹。

======

可以對 SD card 有讀寫的能力後,讓 App 可以操作的範圍就更廣闊了。也不會再遇到 internal storage 空間不足然後要把 App 移除或是清理不要的檔案,因爲可以在 App 裏面加入把一些比較大型的檔案搬到用戶覺得可以存放的地方,再藉由 Windows.Storage APIs 才讀取就可以了。

希望對大家有所幫助。


References:

沒有留言:

張貼留言