2017/12/24

UWP - 操作 UserActivity 讓 Timeline 接續您的工作

Windows 10 Fall Creators Update (introduced v10.0.16299.0) 支援 UserActivity 讓同一個帳號記錄不同設備裡的活動,
讓用戶在任何一臺設備都可以繼續剛才的工作。在 //build 2017 看到的 Timeline 便是整合了這樣的應用。
本篇將介紹利用 UserActivity 操作 Timeline。

Timeline 利用來自 Microsoft Graph 裏面記錄的内容,搭配 AdaptiveCard 顯示出來。而 Microsoft Graph 應用在很多的地方,例如:Project Rome,操作 OneDrvice,People 等。同樣地,UserActivity 背後操作 Microsoft Graph 將用戶在 App 裏面操作的内容給建立進去。
建立在 Microsoft Graph 的内容就能被同步到各個相同帳號的設備 (例如: iOS, Android),讓用戶在不同設備繼續完成活動或是閲讀内容。
Engaging with your customers on any platform using the Microsoft Graph, Activity Feed, and Adaptive Cards 介紹更多的細節。

想要玩 Timeline 與操作 UserActivity 目前需要做一些設定:
  1. 系統先更新到 Windows Insider Preview build 17056 以上
  2. 安裝 16299 SDK,這個版本開始支援 UserActivity
  3. 調整語言設定來開啓 Cortana,預設國家為 美國;預設語言:英語
  4. 在系統的 Settings -> Privacy -> Activity History 確定裏面的帳號是否都有開啓同步
上面幾項都確定之後,如果遇到 Timeline 沒有内容,可以操作 Edge 開啓一些網頁之後,多重開機幾次,因爲 Timeline 的同步第一次會比較慢,如果您的系統又有多個帳號會更久。

Timeline 時間為單位,把相同 App 所有 Activity 都收納起來,呈現的效果如下:


UserActivity 的運作步驟:
  • 每一個 Activity 都是獨立的,利用 ActivityId 來識別,利用 UserActivityChannel 來建立
  • Activity 需要 CreateSession() 建立 Session 開始記錄,等離開 App 或是結束記錄時需要將該 Session 給 Dispose(),才會完成記錄
  • ActivityId 只會在該 App 内使用不會與別的 App 衝突或是共用
  • ActivityId 的命名建議一致,這樣用戶操作到特定位置會跟上次來的時候一致的 Id,例如:如果 App 是利用 Page 為命名單位,那 ActivityId 可能就會是 MainPage/SecondPage 之類;如果是内容命名,則相同的内容應該是相同的 Id;
  • 使用過的 ActivityId 會得到 Published 的狀態,如果是沒有使用過的則是 New
  • Activity 有 3 個組成元素:
    • Activation Deep Link 是 URI 格式,用來回復到用戶離開 App 前的活動畫面或是内容,常見的做法是用 protocol scheme (例如: “my-app://page2?action=edit”) 或是設定 AppUriHandles (e.g. http://constoso.com/page2?action=edit)
    • Visuals 負責視覺化該 Activity,讓用戶可以識別,例如:標題,描述,或是使用 Adaptive Card
      User Activities 會出現在 Cortana 與 Timeline,如果建立的 Activity 沒有特別用
      Adaptive Card,Timeline 會簡單建立一個 activity card (利用 application name, icon 與 title/description)。
    • Content Metadata 用來描述該 Activity 是何種内容格式,例如:plain/text 或是 mp4,可以參考 http://schema.org
  • 如果您的 User Activities 要跨到 iOS/Android 的話,可以直接操作 Microsoft Graph (需要先利用 Microsoft Account 驗證)

介紹建立與維護 User Actives 的幾個類別:
UserActivityChannel
負責建立/管理與從不同設備中取得 User Activity 物件。
MethodDescription
DeleteActivityAsync(String)Delete a specific user activity.
DeleteAllActivitiesAsync()Deletes all of the user activities registered by this app.
GetDefault()Provides access to the User Activities associated with the user's
Managed Service Account (MSA).
GetOrCreateUserActivityAsync(String)Create (or get) a UserActivity with the specified id.

UserActivity
UserActivity 在 App 操作期間被建立,並且記錄在 Microsoft Graph 裏面,讓用戶隨時隨地在不同的設備也能繼續該活動。
重要的元素:
TypeNameDescription
PropertiesActivationUriGets and sets the activation Uniform Resource Identifier (URI).
(重要)代表用戶點了這個 Activity 要執行的活動參數。
ActivityIdGets the activity ID that was assigned to this UserActivity when it was created.
(重要)代表該 Activity 被建立的識別值。
ContentInfoGets or sets the content information object for this user activity.
存放 JSON,讓傳入的參數可以夾帶更多的内容。
ContentTypeGets and sets the MIME (Multipurpose Internet Mail Extensions)
type of the content stored at UserActivity.ContentUri. For example, "text/plain".
ContentUriGets and sets the content Uniform Resource Identifier (URI) of the image
that will be used to represent the activity on another device.
如果你的 App 沒有網站說明這個内容,可以給一個 ContentUri 來顯示預覽。
FallbackUriGets and sets the fallback Uniform Resource Identifier (URI) to use
if there is no handler for the activation URI.
(重要) 值必須是 http or https URI,讓系統如果找不到可以處理 activation URI 的對象
可以改導向 FallbackUri。
StateGets the state (Published or New) of this UserActivity.
GetOrCreateUserActivityAsync() 得到的結果,New 代表未發佈;Published 代表已發佈。
VisualElementsGets information that can be used for the details tile for this activity.
利用 UserActivityVisualElements 裝載要呈現的内容
MethodsCreateSession()Creates a UserActivitySession that this user activity will be associated with.
You must call this method on the UI thread.
建立 UserActivitySession 代表用戶目前正在參與這個 Activity。
SaveAsync()Publish the UserActivity.
(重要) UserActivity.DisplayText 與 UserActivity.ActivationUri 要記得設定
不然呼叫 SaveAsync() 會失敗。

UserActivitySession
一個 Activity 對應一個 UserActivitySession,它負責記錄用戶參與該 Activity 的過程與時間,當結束這個 Activity 或是切換到不同的 Activity 要記得呼叫 Dispose() 才會被記錄。例如:操作一份文件時就會先開啓 Session 代表用戶正在參與,等切換到不同的文件或是關閉,呼叫 Dispose() 把最後的時間更新到 Microsoft Graph

UserActivityVisualElements
負責描述該 Activity 的資訊,例如: description, icon ... 等,内容會被顯示在 Timeline 的 tile。
NameDescription
AttributionUserActivityAttribution Provides visual information about a user activity.
Overrides information the system provides about the user activity.
常用在修改顯示的圖片或是圖片 Uri 需要夾帶參數才能讀取的話,記得使用這個。
BackgroundColorGets and sets the background color for the details tile for this UserActivity.
ContentGets and sets the content that is used for the details tile for this UserActivity.
搭配 ICardElement 的實作使用。
DescriptionGets and sets the description text that is used for the details tile for this UserActivity.
DisplayTextGets and sets the display text that is used for the details tile text for this UserActivity.
(重要) 一定要記得給。

UserActivityContentInfo
描述該 Activity 内容的 Json。常用來夾帶一些參數,讓負責處理的對象可以拿到更多的内容。

上面介紹 User Activities 操作的必要元素,下面簡單範例説明怎麽操作。
  1. 先開啓 Package.Manifest 註冊要處理的 Protocol,例如: testapp。
    <Extensions>
        <uap:Extension Category="windows.protocol">
            <uap:Protocol Name="testapp" />
        </uap:Extension>
    </Extensions>
  2. 在 App.xaml.cs 註冊處理 Protocol 的 OnActived() 事件:
    protected override void OnActivated(IActivatedEventArgs args)
    {
        base.OnActivated(args);            
    
        var rootFrame = InitializeRootFrame(args.PreviousExecutionState);
    
        if (args.Kind == ActivationKind.Protocol)
        {
            var protocolArgs = args as ProtocolActivatedEventArgs;
    
            if (protocolArgs != null)
            {
                rootFrame.Navigate(typeof(MainPage), protocolArgs.Uri.Query);
            }
        }
    }
    在讀取特定的 Activity 把它從記錄裏面去掉:
    protected override async void OnNavigatedTo(NavigationEventArgs e)
    {
        base.OnNavigatedTo(e);
    
        if (string.IsNullOrEmpty(e.Parameter?.ToString()) == false)
        {
            string parameter = e.Parameter.ToString().Substring(1);
    
            Dictionary keyValues = parameter.Split('&').ToDictionary(x => x.Split('=')[0], x => x.Split('=')[1]);
    
            var activity = await UserActivityChannel.GetDefault().GetOrCreateUserActivityAsync(keyValues["id"]);
    
            if (activity.State== UserActivityState.Published)
            {
                await UserActivityChannel.GetDefault().DeleteActivityAsync(keyValues["id"]);
            }
        }
    }
  3. 加入要建立或更新 UserActivity 的邏輯:
    public async void OnGetActivityClick(object sender, RoutedEventArgs e)
    {
        ActivityId = Guid.NewGuid().ToString();
    
        // Get or Create a activity
        var activity = await UserActivityChannel.GetDefault().GetOrCreateUserActivityAsync(ActivityId);
    
        if (activity.State == UserActivityState.New)
        {
            // this is a new activity
            activity.VisualElements.DisplayText = "new activity";
            activity.ActivationUri = new Uri($"testapp://mainPage?state=new&id={ActivityId}");
        }
        else
        {
            // this is published activity
            activity.VisualElements.DisplayText = "published activity";
            activity.ActivationUri = new Uri($"testapp://mainPage?state=published&id={ActivityId}");
        }
    
        // set activity content info
        activity.ContentInfo = UserActivityContentInfo.FromJson(@"{
            ""user_id"": ""pou"",
            ""email"": ""poumason@live.com""
        }");
    
        // FallbackUri is handled when System invoke ActivationUri failed.
        activity.FallbackUri = new Uri("https://dotblogs.com.tw/pou");
    
        await activity.SaveAsync();
    
        // a activity match an session, need close other sessions.
        activitySesion?.Dispose();
        activitySesion = activity.CreateSession();
    }
  4. 如果要調整改用 Adaptive Cards 如下:
    // if you want to use Adaptive card, reference: https://docs.microsoft.com/en-us/adaptive-cards/create/gettingstarted
    activity.VisualElements.Content = AdaptiveCardBuilder.CreateAdaptiveCardFromJson(@"{
        ""$schema"": ""http://adaptivecards.io/schemas/adaptive-card.json"",
        ""type"": ""AdaptiveCard"",
        ""backgroundImage"": ""https://cdn2.ettoday.net/images/1376/d1376700.jpg"",
        ""version"": ""1.0"",
        ""body"" : [ {
        ""type"": ""Container"", 
        ""items"": [
        {
            ""type"": ""TextBlock"",
            ""text"": ""from adaptive card"",
            ""size"": ""large"",
            ""wrap"": ""true"",
            ""maxLines"": ""3""
        }    
        ]
        } ]
    }");
======
UserActivity 看來可以記錄很多東西,更可以放入自定義的 JSON,傳遞的内容就變得更加彈性,但是詳細的同步機制其實還沒有討論到,也許開發時會比較有心得。
希望對大家有所幫助。

References:

沒有留言:

張貼留言