Merged PR 13745: Update Windows Graph Notifications Sample

Update Windows Graph Notifications Sample
This commit is contained in:
Ari Morgan 2019-01-11 21:11:17 +00:00
Родитель fa1f7d7847
Коммит 234c403ae0
14 изменённых файлов: 914 добавлений и 721 удалений

Просмотреть файл

@ -0,0 +1,467 @@
//*********************************************************
//
// Copyright (c) Microsoft. All rights reserved.
// This code is licensed under the Microsoft Public License.
// THIS CODE IS PROVIDED *AS IS* WITHOUT WARRANTY OF
// ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING ANY
// IMPLIED WARRANTIES OF FITNESS FOR A PARTICULAR
// PURPOSE, MERCHANTABILITY, OR NON-INFRINGEMENT.
//
//*********************************************************
using Microsoft.ConnectedDevices;
using Microsoft.ConnectedDevices.UserData;
using Microsoft.ConnectedDevices.UserData.UserNotifications;
using Microsoft.IdentityModel.Clients.ActiveDirectory;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Runtime.InteropServices.WindowsRuntime;
using System.Runtime.Serialization;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;
using Windows.Data.Xml.Dom;
using Windows.Networking.PushNotifications;
using Windows.Security.Authentication.Web;
using Xamarin.Auth;
namespace SDKTemplate
{
public enum AccountRegistrationState
{
InAppCacheAndSdkCache,
InAppCacheOnly,
InSdkCacheOnly
}
public class UserNotificationsManager
{
private UserDataFeed m_feed;
private UserNotificationReader m_reader;
private UserNotificationChannel m_channel;
public event EventHandler CacheUpdated;
private List<UserNotification> m_newNotifications = new List<UserNotification>();
public bool NewNotifications
{
get
{
return m_newNotifications.Count > 0;
}
}
private List<UserNotification> m_historicalNotifications = new List<UserNotification>();
public IReadOnlyList<UserNotification> HistoricalNotifications
{
get
{
return m_historicalNotifications.AsReadOnly();
}
}
public UserNotificationsManager(ConnectedDevicesPlatform platform, ConnectedDevicesAccount account)
{
m_feed = UserDataFeed.GetForAccount(account, platform, Secrets.APP_HOST_NAME);
m_feed.SyncStatusChanged += Feed_SyncStatusChanged;
m_channel = new UserNotificationChannel(m_feed);
m_reader = m_channel.CreateReader();
m_reader.DataChanged += Reader_DataChanged;
Logger.Instance.LogMessage($"Setup feed for {account.Id} {account.Type}");
}
public async Task RegisterAccountWithSdkAsync()
{
var scopes = new List<IUserDataFeedSyncScope> { UserNotificationChannel.SyncScope };
bool registered = await m_feed.SubscribeToSyncScopesAsync(scopes);
if (!registered)
{
throw new Exception("Subscribe failed");
}
}
private void Feed_SyncStatusChanged(UserDataFeed sender, object args)
{
Logger.Instance.LogMessage($"SyncStatus is {sender.SyncStatus.ToString()}");
}
private async void Reader_DataChanged(UserNotificationReader sender, object args)
{
Logger.Instance.LogMessage("New notification available");
await ReadNotificationsAsync(sender);
}
public async Task RefreshAsync()
{
Logger.Instance.LogMessage("Read cached notifications");
await ReadNotificationsAsync(m_reader);
Logger.Instance.LogMessage("Request another sync");
m_feed.StartSync();
}
private async Task ReadNotificationsAsync(UserNotificationReader reader)
{
var notifications = await reader.ReadBatchAsync(UInt32.MaxValue);
Logger.Instance.LogMessage($"Read {notifications.Count} notifications");
foreach (var notification in notifications)
{
if (notification.Status == UserNotificationStatus.Active)
{
m_newNotifications.RemoveAll((n) => { return (n.Id == notification.Id); });
if (notification.UserActionState == UserNotificationUserActionState.NoInteraction)
{
// Brand new notification, add to new
m_newNotifications.Add(notification);
Logger.Instance.LogMessage($"UserNotification not interacted: {notification.Id}");
if (!string.IsNullOrEmpty(notification.Content) && notification.ReadState != UserNotificationReadState.Read)
{
RemoveToastNotification(notification.Id);
ShowToastNotification(BuildToastNotification(notification.Id, notification.Content));
}
}
else
{
RemoveToastNotification(notification.Id);
}
m_historicalNotifications.RemoveAll((n) => { return (n.Id == notification.Id); });
m_historicalNotifications.Insert(0, notification);
}
else
{
// Historical notification is marked as deleted, remove from display
m_newNotifications.RemoveAll((n) => { return (n.Id == notification.Id); });
m_historicalNotifications.RemoveAll((n) => { return (n.Id == notification.Id); });
RemoveToastNotification(notification.Id);
}
}
CacheUpdated?.Invoke(this, new EventArgs());
}
public async Task ActivateAsync(string id, bool dismiss)
{
var notification = m_historicalNotifications.Find((n) => { return (n.Id == id); });
if (notification != null)
{
notification.UserActionState = dismiss ? UserNotificationUserActionState.Dismissed : UserNotificationUserActionState.Activated;
await notification.SaveAsync();
RemoveToastNotification(notification.Id);
Logger.Instance.LogMessage($"{notification.Id} is now DISMISSED");
}
}
public async Task MarkReadAsync(string id)
{
var notification = m_historicalNotifications.Find((n) => { return (n.Id == id); });
if (notification != null)
{
notification.ReadState = UserNotificationReadState.Read;
await notification.SaveAsync();
Logger.Instance.LogMessage($"{notification.Id} is now READ");
}
}
public async Task DeleteAsync(string id)
{
var notification = m_historicalNotifications.Find((n) => { return (n.Id == id); });
if (notification != null)
{
await m_channel.DeleteUserNotificationAsync(notification.Id);
Logger.Instance.LogMessage($"{notification.Id} is now DELETED");
}
}
// Raise a new toast with UserNotification.Id as tag
private void ShowToastNotification(Windows.UI.Notifications.ToastNotification toast)
{
var toastNotifier = Windows.UI.Notifications.ToastNotificationManager.CreateToastNotifier();
toast.Activated += async (s, e) => await ActivateAsync(s.Tag, false);
toastNotifier.Show(toast);
}
// Remove a toast with UserNotification.Id as tag
private void RemoveToastNotification(string notificationId)
{
Windows.UI.Notifications.ToastNotificationManager.History.Remove(notificationId);
}
public static Windows.UI.Notifications.ToastNotification BuildToastNotification(string notificationId, string notificationContent)
{
XmlDocument toastXml = Windows.UI.Notifications.ToastNotificationManager.GetTemplateContent(Windows.UI.Notifications.ToastTemplateType.ToastText02);
XmlNodeList toastNodeList = toastXml.GetElementsByTagName("text");
toastNodeList.Item(0).AppendChild(toastXml.CreateTextNode(notificationId));
toastNodeList.Item(1).AppendChild(toastXml.CreateTextNode(notificationContent));
IXmlNode toastNode = toastXml.SelectSingleNode("/toast");
((XmlElement)toastNode).SetAttribute("launch", "{\"type\":\"toast\",\"notificationId\":\"" + notificationId + "\"}");
XmlElement audio = toastXml.CreateElement("audio");
audio.SetAttribute("src", "ms-winsoundevent:Notification.SMS");
return new Windows.UI.Notifications.ToastNotification(toastXml)
{
Tag = notificationId
};
}
public void Reset()
{
Logger.Instance.LogMessage("Resetting the feed");
m_feed = null;
m_newNotifications.Clear();
m_historicalNotifications.Clear();
CacheUpdated?.Invoke(this, new EventArgs());
}
}
[DataContract]
public class Account
{
public AccountRegistrationState RegistrationState { get; set; }
[DataMember]
public String Token { get; set; }
[DataMember]
public String Id { get; set; }
[DataMember]
public ConnectedDevicesAccountType Type { get; set; }
private ConnectedDevicesPlatform m_platform;
public UserNotificationsManager UserNotifications { get; set; }
public Account(ConnectedDevicesPlatform platform, String id,
ConnectedDevicesAccountType type, String token, AccountRegistrationState registrationState)
{
m_platform = platform;
Id = id;
Type = type;
Token = token;
RegistrationState = registrationState;
// Accounts can be in 3 different scenarios:
// 1: cached account in good standing (initialized in the SDK and our token cache).
// 2: account missing from the SDK but present in our cache: Add and initialize account.
// 3: account missing from our cache but present in the SDK. Log the account out async
// Subcomponents (e.g. UserDataFeed) can only be initialized when an account is in both the app cache
// and the SDK cache.
// For scenario 1, immediately initialize our subcomponents.
// For scenario 2, subcomponents will be initialized after InitializeAccountAsync registers the account with the SDK.
// For scenario 3, InitializeAccountAsync will unregister the account and subcomponents will never be initialized.
if (RegistrationState == AccountRegistrationState.InAppCacheAndSdkCache)
{
InitializeSubcomponents();
}
}
public async Task InitializeAccountAsync()
{
if (RegistrationState == AccountRegistrationState.InAppCacheOnly)
{
// Scenario 2, add the account to the SDK
var account = new ConnectedDevicesAccount(Id, Type);
await m_platform.AccountManager.AddAccountAsync(account);
RegistrationState = AccountRegistrationState.InAppCacheAndSdkCache;
InitializeSubcomponents();
await RegisterAccountWithSdkAsync();
}
else if (RegistrationState == AccountRegistrationState.InSdkCacheOnly)
{
// Scenario 3, remove the account from the SDK
var account = new ConnectedDevicesAccount(Id, Type);
await m_platform.AccountManager.RemoveAccountAsync(account);
}
}
public async Task RegisterAccountWithSdkAsync()
{
if (RegistrationState != AccountRegistrationState.InAppCacheAndSdkCache)
{
throw new Exception("Account must be in both SDK and App cache before it can be registered");
}
var channel = await PushNotificationChannelManager.CreatePushNotificationChannelForApplicationAsync();
ConnectedDevicesNotificationRegistration registration = new ConnectedDevicesNotificationRegistration();
registration.Type = ConnectedDevicesNotificationType.WNS;
registration.Token = channel.Uri;
var account = new ConnectedDevicesAccount(Id, Type);
await m_platform.NotificationRegistrationManager.RegisterForAccountAsync(account, registration);
await UserNotifications.RegisterAccountWithSdkAsync();
}
public async Task LogoutAsync()
{
ClearSubcomponents();
await m_platform.AccountManager.RemoveAccountAsync(new ConnectedDevicesAccount(Id, Type));
RegistrationState = AccountRegistrationState.InAppCacheOnly;
}
private void InitializeSubcomponents()
{
if (RegistrationState != AccountRegistrationState.InAppCacheAndSdkCache)
{
throw new Exception("Account must be in both SDK and App cache before subcomponents can be initialized");
}
var account = new ConnectedDevicesAccount(Id, Type);
UserNotifications = new UserNotificationsManager(m_platform, account);
}
private void ClearSubcomponents()
{
UserNotifications.Reset();
UserNotifications = null;
}
public async Task<String> GetAccessTokenAsync(IReadOnlyList<string> scopes)
{
if (Type == ConnectedDevicesAccountType.MSA)
{
return await MSAOAuthHelpers.GetAccessTokenUsingRefreshTokenAsync(Token, scopes);
}
else if (Type == ConnectedDevicesAccountType.AAD)
{
var authContext = new AuthenticationContext("https://login.microsoftonline.com/common");
UserIdentifier aadUserId = new UserIdentifier(Id, UserIdentifierType.UniqueId);
AuthenticationResult result;
try
{
result = await authContext.AcquireTokenSilentAsync(scopes[0], Secrets.AAD_CLIENT_ID);
}
catch (Exception ex)
{
Logger.Instance.LogMessage($"Token request failed: {ex.Message}");
// Token may have expired, try again non-silently
result = await authContext.AcquireTokenAsync(scopes[0], Secrets.AAD_CLIENT_ID,
new Uri(Secrets.AAD_REDIRECT_URI), new PlatformParameters(PromptBehavior.Auto, true));
}
return result.AccessToken;
}
else
{
throw new Exception("Invalid Account Type");
}
}
public static async Task<AuthenticationResult> GetAadTokenAsync(string scope)
{
var authContext = new AuthenticationContext("https://login.microsoftonline.com/common");
return await authContext.AcquireTokenAsync(scope, Secrets.AAD_CLIENT_ID, new Uri(Secrets.AAD_REDIRECT_URI),
new PlatformParameters(PromptBehavior.Auto, true));
}
}
public class MSAOAuthHelpers
{
static readonly string ProdAuthorizeUrl = "https://login.live.com/oauth20_authorize.srf";
static readonly string ProdRedirectUrl = "https://login.microsoftonline.com/common/oauth2/nativeclient";
static readonly string ProdAccessTokenUrl = "https://login.live.com/oauth20_token.srf";
static readonly string OfflineAccessScope = "wl.offline_access";
static readonly string WNSScope = "wns.connect";
static readonly string DdsScope = "dds.register dds.read";
static readonly string CCSScope = "ccs.ReadWrite";
static readonly string UserActivitiesScope = "https://activity.windows.com/UserActivity.ReadWrite.CreatedByApp";
static readonly string UserNotificationsScope = "https://activity.windows.com/Notifications.ReadWrite.CreatedByApp";
static Random Randomizer = new Random((int)DateTime.Now.Ticks);
static SHA256 HashProvider = SHA256.Create();
static async Task<IDictionary<string, string>> RequestAccessTokenAsync(string accessTokenUrl, IDictionary<string, string> queryValues)
{
// mc++ changed protected to public for extension methods RefreshToken (Adrian Stevens)
var content = new FormUrlEncodedContent(queryValues);
HttpClient client = new HttpClient();
HttpResponseMessage response = await client.PostAsync(accessTokenUrl, content).ConfigureAwait(false);
string text = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
// Parse the response
IDictionary<string, string> data = text.Contains("{") ? WebEx.JsonDecode(text) : WebEx.FormDecode(text);
if (data.ContainsKey("error"))
{
throw new AuthException(data["error_description"]);
}
return data;
}
public static async Task<string> GetRefreshTokenAsync()
{
byte[] buffer = new byte[32];
Randomizer.NextBytes(buffer);
var codeVerifier = Convert.ToBase64String(buffer).Replace('+', '-').Replace('/', '_').Replace("=", "");
byte[] hash = HashProvider.ComputeHash(Encoding.UTF8.GetBytes(codeVerifier));
var codeChallenge = Convert.ToBase64String(hash).Replace('+', '-').Replace('/', '_').Replace("=", "");
var redirectUri = new Uri(ProdRedirectUrl);
string scope = $"{OfflineAccessScope} {WNSScope} {CCSScope} {UserNotificationsScope} {UserActivitiesScope} {DdsScope}";
var startUri = new Uri($"{ProdAuthorizeUrl}?client_id={Secrets.MSA_CLIENT_ID}&response_type=code&code_challenge_method=S256&code_challenge={codeChallenge}&redirect_uri={ProdRedirectUrl}&scope={scope}");
var webAuthenticationResult = await WebAuthenticationBroker.AuthenticateAsync(
WebAuthenticationOptions.None,
startUri,
redirectUri);
if (webAuthenticationResult.ResponseStatus == WebAuthenticationStatus.Success)
{
var codeResponseUri = new Uri(webAuthenticationResult.ResponseData);
IDictionary<string, string> queryParams = WebEx.FormDecode(codeResponseUri.Query);
if (!queryParams.ContainsKey("code"))
{
return string.Empty;
}
string authCode = queryParams["code"];
Dictionary<string, string> refreshTokenQuery = new Dictionary<string, string>
{
{ "client_id", Secrets.MSA_CLIENT_ID },
{ "redirect_uri", redirectUri.AbsoluteUri },
{ "grant_type", "authorization_code" },
{ "code", authCode },
{ "code_verifier", codeVerifier },
{ "scope", WNSScope }
};
IDictionary<string, string> refreshTokenResponse = await RequestAccessTokenAsync(ProdAccessTokenUrl, refreshTokenQuery);
if (refreshTokenResponse.ContainsKey("refresh_token"))
{
return refreshTokenResponse["refresh_token"];
}
}
return string.Empty;
}
public static async Task<string> GetAccessTokenUsingRefreshTokenAsync(string refreshToken, IReadOnlyList<string> scopes)
{
Dictionary<string, string> accessTokenQuery = new Dictionary<string, string>
{
{ "client_id", Secrets.MSA_CLIENT_ID },
{ "redirect_uri", ProdRedirectUrl },
{ "grant_type", "refresh_token" },
{ "refresh_token", refreshToken },
{ "scope", string.Join(" ", scopes.ToArray()) },
};
IDictionary<string, string> accessTokenResponse = await RequestAccessTokenAsync(ProdAccessTokenUrl, accessTokenQuery);
if (accessTokenResponse == null || !accessTokenResponse.ContainsKey("access_token"))
{
throw new Exception("Unable to fetch access_token!");
}
return accessTokenResponse["access_token"];
}
}
}

Просмотреть файл

@ -33,7 +33,6 @@
</TextBlock>
<Button x:Name="AadButton" Content="Login with AAD" Margin="0,10,0,0" Click="Button_LoginAAD"/>
<Button x:Name="MsaButton" Content="Login with MSA" Margin="0,10,0,0" Click="Button_LoginMSA"/>
<Button x:Name="LogoutButton" Content="Logout" Margin="0,10,0,0" Click="Button_Logout"/>
</StackPanel>
<!-- Status Block for providing messages to the user. Use the

Просмотреть файл

@ -18,7 +18,7 @@ namespace SDKTemplate
public sealed partial class AccountsPage : Page
{
private MainPage rootPage;
private MicrosoftAccountProvider accountProvider;
private ConnectedDevicesManager connectedDevicesManager;
public AccountsPage()
{
@ -28,78 +28,51 @@ namespace SDKTemplate
protected override void OnNavigatedTo(NavigationEventArgs e)
{
rootPage = MainPage.Current;
accountProvider = ((App)Application.Current).AccountProvider;
connectedDevicesManager = ((App)Application.Current).ConnectedDevicesManager;
connectedDevicesManager.AccountsChanged += ConnectedDevicesManager_AccountsChanged;
UpdateUI();
}
private void ConnectedDevicesManager_AccountsChanged(object sender, System.EventArgs e)
{
UpdateUI();
}
private void UpdateUI()
{
MsaButton.IsEnabled = (accountProvider.SignedInAccount == null);
AadButton.IsEnabled = (accountProvider.SignedInAccount == null);
LogoutButton.IsEnabled = (accountProvider.SignedInAccount != null);
LogoutButton.Content = "Logout";
if (accountProvider.SignedInAccount != null)
{
Description.Text = $"{accountProvider.SignedInAccount.Type} user ";
if (accountProvider.AadUser != null)
{
Description.Text += accountProvider.AadUser.DisplayableId;
}
LogoutButton.Content = $"Logout - {accountProvider.SignedInAccount.Type}";
}
// The ConnectedDevices SDK does not support multi-user currently. When this support becomes available
// these buttons would always be enabled.
bool hasAccount = connectedDevicesManager.Accounts.Count > 0;
MsaButton.IsEnabled = !hasAccount;
AadButton.IsEnabled = !hasAccount;
}
private async void Button_LoginMSA(object sender, RoutedEventArgs e)
{
if (accountProvider.SignedInAccount == null)
bool success = await connectedDevicesManager.SignInMsaAsync();
if (!success)
{
((Button)sender).IsEnabled = false;
bool success = await accountProvider.SignInMsa();
if (!success)
{
rootPage.NotifyUser("MSA login failed!", NotifyType.ErrorMessage);
}
else
{
rootPage.NotifyUser("MSA login successful", NotifyType.StatusMessage);
}
UpdateUI();
rootPage.NotifyUser("MSA login failed!", NotifyType.ErrorMessage);
}
else
{
rootPage.NotifyUser("MSA login successful", NotifyType.StatusMessage);
}
}
private async void Button_LoginAAD(object sender, RoutedEventArgs e)
{
if (accountProvider.SignedInAccount == null)
{
((Button)sender).IsEnabled = false;
bool success = await accountProvider.SignInAad();
if (!success)
{
rootPage.NotifyUser("AAD login failed!", NotifyType.ErrorMessage);
}
else
{
rootPage.NotifyUser("AAD login successful", NotifyType.StatusMessage);
}
UpdateUI();
}
}
private async void Button_Logout(object sender, RoutedEventArgs e)
{
((Button)sender).IsEnabled = false;
accountProvider.SignOut();
rootPage.NotifyUser("Logout successful", NotifyType.ErrorMessage);
UpdateUI();
bool success = await connectedDevicesManager.SignInAadAsync();
if (!success)
{
rootPage.NotifyUser("AAD login failed!", NotifyType.ErrorMessage);
}
else
{
rootPage.NotifyUser("AAD login successful", NotifyType.StatusMessage);
}
}
}
}

Просмотреть файл

@ -11,6 +11,7 @@
using Newtonsoft.Json;
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Windows.ApplicationModel.Activation;
using Windows.ApplicationModel.Background;
@ -29,14 +30,23 @@ namespace SDKTemplate
public string notificationId { get; set; }
}
public class Activity
{
public string id { get; set; }
}
public class RawNotificationPayload
{
public List<Activity> activities { get; set; }
}
/// <summary>
/// Provides application-specific behavior to supplement the default Application class.
/// </summary>
sealed partial class App : Application
{
public PushNotificationChannel PushChannel { get; set; }
public MicrosoftAccountProvider AccountProvider { get; set; }
public GraphNotificationProvider NotificationProvider { get; set; }
public ConnectedDevicesManager ConnectedDevicesManager { get; set; }
/// <summary>
/// Initializes the singleton application object. This is the first line of authored code
@ -54,7 +64,6 @@ namespace SDKTemplate
/// <param name="e">Details about the launch request and process.</param>
protected override async void OnLaunched(LaunchActivatedEventArgs e)
{
#if DEBUG
if (System.Diagnostics.Debugger.IsAttached)
{
@ -70,11 +79,10 @@ namespace SDKTemplate
PushChannel.PushNotificationReceived += PushNotificationReceived;
}
if (NotificationProvider == null)
if (ConnectedDevicesManager == null)
{
Logger.Instance.LogMessage($"Setup AccountsProvider and NotificationsProvider");
AccountProvider = new MicrosoftAccountProvider();
NotificationProvider = new GraphNotificationProvider(AccountProvider, PushChannel.Uri);
ConnectedDevicesManager = new ConnectedDevicesManager();
}
Frame rootFrame = Window.Current.Content as Frame;
@ -109,12 +117,11 @@ namespace SDKTemplate
rootFrame.Navigate(typeof(MainPage), e.Arguments);
}
if (!string.IsNullOrEmpty(e.Arguments))
{
var result = JsonConvert.DeserializeObject<AppLauchArgs>(e.Arguments);
NotificationProvider?.Refresh();
NotificationProvider?.Activate(result.notificationId, false);
await ConnectedDevicesManager.RefreshAsync();
await ConnectedDevicesManager.ActivateAsync(result.notificationId, false);
}
// Ensure the current window is active
@ -131,13 +138,13 @@ namespace SDKTemplate
throw new Exception("Failed to load Page " + e.SourcePageType.FullName);
}
private void PushNotificationReceived(PushNotificationChannel sender, PushNotificationReceivedEventArgs e)
private async void PushNotificationReceived(PushNotificationChannel sender, PushNotificationReceivedEventArgs e)
{
Logger.Instance.LogMessage($"Push received type:{e.NotificationType}");
if (e.NotificationType == PushNotificationType.Raw)
{
e.Cancel = true;
NotificationProvider?.ReceiveNotification(e.RawNotification.Content);
await ConnectedDevicesManager?.ReceiveNotificationAsync(e.RawNotification.Content);
}
}
@ -151,26 +158,25 @@ namespace SDKTemplate
Logger.Instance.LogMessage($"Task canceled for {r}");
m_deferral.Complete();
};
Logger.Instance.LogMessage($"{args.TaskInstance.Task.Name} activated in background");
Logger.Instance.LogMessage($"{args.TaskInstance.Task.Name} activated in background with {args.TaskInstance.TriggerDetails.GetType().ToString()}");
if (args.TaskInstance.TriggerDetails is RawNotification)
{
var notification = args.TaskInstance.TriggerDetails as RawNotification;
Logger.Instance.LogMessage($"RawNotification received {notification.Content}");
var rawNotification = args.TaskInstance.TriggerDetails as RawNotification;
Logger.Instance.LogMessage($"RawNotification received {rawNotification.Content}");
await ConnectedDevicesManager.ReceiveNotificationAsync(rawNotification.Content);
await Task.Run(() =>
{
if (NotificationProvider != null)
{
AccountProvider = new MicrosoftAccountProvider();
NotificationProvider = new GraphNotificationProvider(AccountProvider, "");
}
// HACK: Crack the push body to extract the notificationId
// var result = JsonConvert.DeserializeObject<RawNotificationPayload>(rawNotification.Content);
// var notificationId = result.activities != null && result.activities.Count > 0 ? result.activities[0].id : "Graph Notifications";
// var toast = ConnectedDevicesManager.BuildToastNotification(notificationId, "New notification");
// var toastNotifier = Windows.UI.Notifications.ToastNotificationManager.CreateToastNotifier();
// toastNotifier.Show(toast);
NotificationProvider.ReceiveNotification(notification.Content);
});
await Task.Delay(TimeSpan.FromSeconds(15));
}
await Task.Delay(TimeSpan.FromSeconds(30));
Logger.Instance.LogMessage($"Task completed");
}
}

Просмотреть файл

@ -0,0 +1,271 @@
//*********************************************************
//
// Copyright (c) Microsoft. All rights reserved.
// This code is licensed under the Microsoft Public License.
// THIS CODE IS PROVIDED *AS IS* WITHOUT WARRANTY OF
// ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING ANY
// IMPLIED WARRANTIES OF FITNESS FOR A PARTICULAR
// PURPOSE, MERCHANTABILITY, OR NON-INFRINGEMENT.
//
//*********************************************************
using Microsoft.ConnectedDevices;
using Microsoft.IdentityModel.Clients.ActiveDirectory;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Runtime.Serialization.Json;
using System.Text;
using System.Threading.Tasks;
using Windows.Storage;
namespace SDKTemplate
{
public class ConnectedDevicesManager
{
private ConnectedDevicesPlatform m_platform;
private List<Account> m_accounts = new List<Account>();
public IReadOnlyList<Account> Accounts
{
get
{
return m_accounts.Where((x) => x.RegistrationState == AccountRegistrationState.InAppCacheAndSdkCache).ToList();
}
}
public event EventHandler AccountsChanged;
static readonly string AccountsKey = "Accounts";
static readonly string CCSResource = "https://cdpcs.access.microsoft.com";
// This is a singleton object which holds onto the app's ConnectedDevicesPlatform and
// handles account management. This is accessed via App.Current.ConnectedDevicesManager
public ConnectedDevicesManager()
{
// Construct and initialize a platform. All we are doing here is hooking up event handlers before
// calling ConnectedDevicesPlatform.Start(). After Start() is called events may begin to fire.
m_platform = new ConnectedDevicesPlatform();
m_platform.AccountManager.AccessTokenRequested += AccountManager_AccessTokenRequestedAsync;
m_platform.AccountManager.AccessTokenInvalidated += AccountManager_AccessTokenInvalidated;
m_platform.NotificationRegistrationManager.NotificationRegistrationStateChanged += NotificationRegistrationManager_NotificationRegistrationStateChanged;
m_platform.Start();
// Pull the accounts from our app's cache and synchronize the list with the apps cached by
// ConnectedDevicesPlatform.AccountManager.
DeserializeAccounts();
// Finally initialize the accounts. This will refresh registrations when needed, add missing accounts,
// and remove stale accounts from the ConnectedDevicesPlatform.AccountManager.
Task.Run(() => InitializeAccountsAsync());
}
private void DeserializeAccounts()
{
// Add all our cached accounts.
var sdkCachedAccounts = m_platform.AccountManager.Accounts.ToList();
var appCachedAccounts = ApplicationData.Current.LocalSettings.Values[AccountsKey] as string;
if (!String.IsNullOrEmpty(appCachedAccounts))
{
DeserializeAppCachedAccounts(appCachedAccounts, sdkCachedAccounts);
}
// Add the remaining SDK only accounts (these need to be removed from the SDK)
foreach (var sdkCachedAccount in sdkCachedAccounts)
{
m_accounts.Add(new Account(m_platform, sdkCachedAccount.Id, sdkCachedAccount.Type, null, AccountRegistrationState.InSdkCacheOnly));
}
}
private void DeserializeAppCachedAccounts(String jsonCachedAccounts, List<ConnectedDevicesAccount> sdkCachedAccounts)
{
MemoryStream stream = new MemoryStream(Encoding.UTF8.GetBytes(jsonCachedAccounts));
DataContractJsonSerializer serializer = new DataContractJsonSerializer(m_accounts.GetType());
List <Account> appCachedAccounts = serializer.ReadObject(stream) as List<Account>;
var authContext = new AuthenticationContext("https://login.microsoftonline.com/common");
var adalCachedItems = authContext.TokenCache.ReadItems();
foreach (var account in appCachedAccounts)
{
if (account.Type == ConnectedDevicesAccountType.AAD)
{
// AAD accounts are also cached in ADAL, which is where the actual token logic lives.
// If the account isn't available in our ADAL cache then it's not usable. Ideally this
// shouldn't happen.
var adalCachedItem = adalCachedItems.FirstOrDefault((x) => x.UniqueId == account.Id);
if (adalCachedItem == null)
{
continue;
}
}
// Check if the account is also present in ConnectedDevicesPlatform.AccountManager.
AccountRegistrationState registrationState;
var sdkAccount = sdkCachedAccounts.Find((x) => x.Id == account.Id);
if (sdkAccount == null)
{
// Account not found in the SDK cache. Later when Account.InitializeAsync runs this will
// add the account to the SDK cache and perform registration.
registrationState = AccountRegistrationState.InAppCacheOnly;
}
else
{
// Account found in the SDK cache, remove it from the list of sdkCachedAccounts. After
// all the appCachedAccounts have been processed any accounts remaining in sdkCachedAccounts
// are only in the SDK cache, and should be removed.
registrationState = AccountRegistrationState.InAppCacheAndSdkCache;
sdkCachedAccounts.RemoveAll((x) => x.Id == account.Id);
}
m_accounts.Add(new Account(m_platform, account.Id, account.Type, account.Token, registrationState));
}
}
private async Task InitializeAccountsAsync()
{
foreach (var account in m_accounts)
{
await account.InitializeAccountAsync();
}
// All accounts which can be in a good state should be. Remove any accounts which aren't
m_accounts.RemoveAll((x) => x.RegistrationState != AccountRegistrationState.InAppCacheAndSdkCache);
AccountListChanged();
}
private void AccountListChanged()
{
AccountsChanged.Invoke(this, new EventArgs());
SerializeAccountsToCache();
}
private void SerializeAccountsToCache()
{
using (MemoryStream stream = new MemoryStream())
{
DataContractJsonSerializer serializer = new DataContractJsonSerializer(m_accounts.GetType());
serializer.WriteObject(stream, m_accounts);
byte[] json = stream.ToArray();
ApplicationData.Current.LocalSettings.Values[AccountsKey] = Encoding.UTF8.GetString(json, 0, json.Length);
}
}
private void AccountManager_AccessTokenInvalidated(ConnectedDevicesAccountManager sender, ConnectedDevicesAccessTokenInvalidatedEventArgs args)
{
Logger.Instance.LogMessage($"Token Invalidated. AccountId: {args.Account.Id}, AccountType: {args.Account.Id}, scopes: {string.Join(" ", args.Scopes)}");
}
private async void NotificationRegistrationManager_NotificationRegistrationStateChanged(ConnectedDevicesNotificationRegistrationManager sender, ConnectedDevicesNotificationRegistrationStateChangedEventArgs args)
{
if ((args.State == ConnectedDevicesNotificationRegistrationState.Expired) || (args.State == ConnectedDevicesNotificationRegistrationState.Expiring))
{
var account = m_accounts.Find((x) => x.Id == args.Account.Id);
if (account != null)
{
await account.RegisterAccountWithSdkAsync();
}
}
}
private async void AccountManager_AccessTokenRequestedAsync(ConnectedDevicesAccountManager sender, ConnectedDevicesAccessTokenRequestedEventArgs args)
{
Logger.Instance.LogMessage($"Token requested by platform for {args.Request.Account.Id} and {string.Join(" ", args.Request.Scopes)}");
var account = m_accounts.Find((x) => x.Id == args.Request.Account.Id);
if (account != null)
{
try
{
var accessToken = await account.GetAccessTokenAsync(args.Request.Scopes);
Logger.Instance.LogMessage($"Token : {accessToken}");
args.Request.CompleteWithAccessToken(accessToken);
}
catch (Exception ex)
{
Logger.Instance.LogMessage($"Token request failed: {ex.Message}");
args.Request.CompleteWithErrorMessage(ex.Message);
}
}
}
public async Task<bool> SignInAadAsync()
{
try
{
var authResult = await Account.GetAadTokenAsync(CCSResource);
var account = new Account(m_platform, authResult.UserInfo.UniqueId,
ConnectedDevicesAccountType.AAD, authResult.AccessToken, AccountRegistrationState.InAppCacheOnly);
m_accounts.Add(account);
await account.InitializeAccountAsync();
AccountListChanged();
return true;
}
catch
{
return false;
}
}
public async Task<bool> SignInMsaAsync()
{
string refreshToken = await MSAOAuthHelpers.GetRefreshTokenAsync();
if (!string.IsNullOrEmpty(refreshToken))
{
var account = new Account(m_platform, Guid.NewGuid().ToString(),
ConnectedDevicesAccountType.MSA, refreshToken, AccountRegistrationState.InAppCacheOnly);
m_accounts.Add(account);
await account.InitializeAccountAsync();
AccountListChanged();
return true;
}
return false;
}
public async Task LogoutAsync(Account account)
{
// First log the account out from the ConnectedDevices SDK. The SDK may call back for access tokens to perform
// unregistration with services
await account.LogoutAsync();
// Next remove the account locally
m_accounts.RemoveAll((x) => x.Id == account.Id);
if (account.Type == ConnectedDevicesAccountType.AAD)
{
var authContext = new AuthenticationContext("https://login.microsoftonline.com/common");
var cacheItems = authContext.TokenCache.ReadItems();
var cacheItem = cacheItems.FirstOrDefault((x) => x.UniqueId == account.Id);
if (cacheItem != null)
{
authContext.TokenCache.DeleteItem(cacheItem);
}
}
AccountListChanged();
}
public async Task ReceiveNotificationAsync(string content)
{
await m_platform.ProcessNotification(content).WaitForCompletionAsync();
}
public async Task RefreshAsync()
{
foreach (var account in m_accounts)
{
await account.UserNotifications?.RefreshAsync();
}
}
public async Task ActivateAsync(string id, bool dismiss)
{
foreach (var account in m_accounts)
{
await account.UserNotifications?.ActivateAsync(id, dismiss);
}
}
}
}

Просмотреть файл

@ -1,280 +0,0 @@
//*********************************************************
//
// Copyright (c) Microsoft. All rights reserved.
// This code is licensed under the Microsoft Public License.
// THIS CODE IS PROVIDED *AS IS* WITHOUT WARRANTY OF
// ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING ANY
// IMPLIED WARRANTIES OF FITNESS FOR A PARTICULAR
// PURPOSE, MERCHANTABILITY, OR NON-INFRINGEMENT.
//
//*********************************************************
using Microsoft.ConnectedDevices.Core;
using Microsoft.ConnectedDevices.UserData;
using Microsoft.ConnectedDevices.UserData.UserNotifications;
using System;
using System.Collections.Generic;
using System.Runtime.InteropServices.WindowsRuntime;
using System.Threading.Tasks;
using Windows.Data.Xml.Dom;
using Windows.Foundation;
using Windows.Storage;
namespace SDKTemplate
{
public class GraphNotificationProvider : IConnectedDevicesNotificationProvider
{
private ConnectedDevicesPlatform m_platform;
private UserDataFeed m_feed;
private UserNotificationReader m_reader;
private UserNotificationChannel m_channel;
private List<UserNotification> m_newNotifications = new List<UserNotification>();
private List<UserNotification> m_historicalNotifications = new List<UserNotification>();
private MicrosoftAccountProvider m_accoutProvider;
private string m_pushUri;
static readonly string PushUriKey = "PushUri";
public event EventHandler CacheUpdated;
public bool NewNotifications
{
get
{
return m_newNotifications.Count > 0;
}
}
public IReadOnlyList<UserNotification> HistoricalNotifications
{
get
{
return m_historicalNotifications.AsReadOnly();
}
}
public GraphNotificationProvider(MicrosoftAccountProvider accountProvider, string pushUri)
{
m_pushUri = pushUri;
if (string.IsNullOrEmpty(pushUri) && ApplicationData.Current.LocalSettings.Values.ContainsKey(PushUriKey))
{
m_pushUri = ApplicationData.Current.LocalSettings.Values[PushUriKey] as string;
}
m_accoutProvider = accountProvider;
accountProvider.SignOutCompleted += (s, e) => Reset();
}
private async Task<string> GetNotificationRegistration()
{
return m_pushUri;
}
IAsyncOperation<string> IConnectedDevicesNotificationProvider.GetNotificationRegistrationAsync()
{
Logger.Instance.LogMessage($"Push registration requested by platform");
return GetNotificationRegistration().AsAsyncOperation();
}
event TypedEventHandler<IConnectedDevicesNotificationProvider, ConnectedDevicesNotificationRegistrationUpdatedEventArgs> IConnectedDevicesNotificationProvider.RegistrationUpdated
{
add { return new EventRegistrationToken(); }
remove { }
}
public async void Refresh()
{
await SetupChannel();
if (m_reader != null)
{
Logger.Instance.LogMessage("Read cached notifications");
ReadNotifications(m_reader);
}
Logger.Instance.LogMessage("Request another sync");
m_feed?.StartSync();
}
public async void ReceiveNotification(string content)
{
await SetupChannel();
m_platform.ReceiveNotification(content);
}
public async void Activate(string id, bool dismiss)
{
await SetupChannel();
var notification = m_historicalNotifications.Find((n) => { return (n.Id == id); });
if (notification != null)
{
notification.UserActionState = dismiss ? UserNotificationUserActionState.Dismissed : UserNotificationUserActionState.Activated;
await notification.SaveAsync();
RemoveToastNotification(notification.Id);
Logger.Instance.LogMessage($"{notification.Id} is now DISMISSED");
}
}
public async void MarkRead(string id)
{
var notification = m_historicalNotifications.Find((n) => { return (n.Id == id); });
if (notification != null)
{
notification.ReadState = UserNotificationReadState.Read;
await notification.SaveAsync();
Logger.Instance.LogMessage($"{notification.Id} is now READ");
}
}
public async void Delete(string id)
{
var notification = m_historicalNotifications.Find((n) => { return (n.Id == id); });
if (notification != null)
{
await m_channel?.DeleteUserNotificationAsync(notification.Id);
Logger.Instance.LogMessage($"{notification.Id} is now DELETED");
}
}
public async void Reset()
{
if (m_platform != null)
{
Logger.Instance.LogMessage("Shutting down platform");
await m_platform.ShutdownAsync();
m_platform = null;
m_feed = null;
m_newNotifications.Clear();
m_historicalNotifications.Clear();
}
CacheUpdated?.Invoke(this, new EventArgs());
}
private async Task SetupChannel()
{
var account = m_accoutProvider.SignedInAccount;
if (account != null && m_platform == null)
{
m_platform = new ConnectedDevicesPlatform(m_accoutProvider, this);
}
if (m_feed == null)
{
// Need to run UserDataFeed creation on a background thread
// because MSA/AAD token request might need to show UI.
await Task.Run(() =>
{
lock (this)
{
if (account != null && m_feed == null)
{
try
{
m_feed = new UserDataFeed(account, m_platform, "graphnotifications.sample.windows.com");
m_feed.SyncStatusChanged += Feed_SyncStatusChanged;
m_feed.AddSyncScopes(new List<IUserDataFeedSyncScope>
{
UserNotificationChannel.SyncScope
});
m_channel = new UserNotificationChannel(m_feed);
m_reader = m_channel.CreateReader();
m_reader.DataChanged += Reader_DataChanged;
Logger.Instance.LogMessage($"Setup feed for {account.Id} {account.Type}");
}
catch (Exception ex)
{
Logger.Instance.LogMessage($"Failed to setup UserNotificationChannel {ex.Message}");
m_feed = null;
}
}
}
});
}
}
private async void ReadNotifications(UserNotificationReader reader)
{
var notifications = await reader.ReadBatchAsync(UInt32.MaxValue);
Logger.Instance.LogMessage($"Read {notifications.Count} notifications");
foreach (var notification in notifications)
{
//Logger.Instance.LogMessage($"UserNotification: {notification.Id} Status: {notification.Status} ReadState: {notification.ReadState} UserActionState: {notification.UserActionState}");
if (notification.Status == UserNotificationStatus.Active)
{
m_newNotifications.RemoveAll((n) => { return (n.Id == notification.Id); });
if (notification.UserActionState == UserNotificationUserActionState.NoInteraction)
{
// Brand new notification, add to new
m_newNotifications.Add(notification);
Logger.Instance.LogMessage($"UserNotification not interacted: {notification.Id}");
if (!string.IsNullOrEmpty(notification.Content) && notification.ReadState != UserNotificationReadState.Read)
{
RemoveToastNotification(notification.Id);
ShowToastNotification(BuildToastNotification(notification.Id, notification.Content));
}
}
else
{
RemoveToastNotification(notification.Id);
}
m_historicalNotifications.RemoveAll((n) => { return (n.Id == notification.Id); });
m_historicalNotifications.Insert(0, notification);
}
else
{
// Historical notification is marked as deleted, remove from display
m_newNotifications.RemoveAll((n) => { return (n.Id == notification.Id); });
m_historicalNotifications.RemoveAll((n) => { return (n.Id == notification.Id); });
RemoveToastNotification(notification.Id);
}
}
CacheUpdated?.Invoke(this, new EventArgs());
}
private void Feed_SyncStatusChanged(UserDataFeed sender, object args)
{
Logger.Instance.LogMessage($"SyncStatus is {sender.SyncStatus.ToString()}");
}
private void Reader_DataChanged(UserNotificationReader sender, object args)
{
Logger.Instance.LogMessage("New notification available");
ReadNotifications(sender);
}
public static Windows.UI.Notifications.ToastNotification BuildToastNotification(string notificationId, string notificationContent)
{
XmlDocument toastXml = Windows.UI.Notifications.ToastNotificationManager.GetTemplateContent(Windows.UI.Notifications.ToastTemplateType.ToastText02);
XmlNodeList toastNodeList = toastXml.GetElementsByTagName("text");
toastNodeList.Item(0).AppendChild(toastXml.CreateTextNode(notificationId));
toastNodeList.Item(1).AppendChild(toastXml.CreateTextNode(notificationContent));
IXmlNode toastNode = toastXml.SelectSingleNode("/toast");
((XmlElement)toastNode).SetAttribute("launch", "{\"type\":\"toast\",\"notificationId\":\"" + notificationId + "\"}");
XmlElement audio = toastXml.CreateElement("audio");
audio.SetAttribute("src", "ms-winsoundevent:Notification.SMS");
return new Windows.UI.Notifications.ToastNotification(toastXml)
{
Tag = notificationId
};
}
// Raise a new toast with UserNotification.Id as tag
private void ShowToastNotification(Windows.UI.Notifications.ToastNotification toast)
{
var toastNotifier = Windows.UI.Notifications.ToastNotificationManager.CreateToastNotifier();
toast.Activated += (s, e) => Activate(s.Tag, false);
toastNotifier.Show(toast);
}
// Remove a toast with UserNotification.Id as tag
private void RemoveToastNotification(string notificationId)
{
Windows.UI.Notifications.ToastNotificationManager.History.Remove(notificationId);
}
}
}

Просмотреть файл

@ -101,7 +101,7 @@
<Compile Include="LogsPage.xaml.cs">
<DependentUpon>LogsPage.xaml</DependentUpon>
</Compile>
<Compile Include="GraphNotificationProvider.cs" />
<Compile Include="ConnectedDevicesManager.cs" />
<Compile Include="Logger.cs" />
<Compile Include="NotificationsPage.xaml.cs">
<DependentUpon>NotificationsPage.xaml</DependentUpon>
@ -116,7 +116,8 @@
<Compile Include="AccountsPage.xaml.cs">
<DependentUpon>AccountsPage.xaml</DependentUpon>
</Compile>
<Compile Include="MicrosoftAccountProvider.cs" />
<Compile Include="Account.cs" />
<Compile Include="Secrets.cs" />
</ItemGroup>
<ItemGroup>
<AppxManifest Include="Package.appxmanifest">
@ -167,19 +168,19 @@
<Version>*</Version>
</PackageReference>
<PackageReference Include="Microsoft.IdentityModel.Clients.ActiveDirectory">
<Version>3.19.8</Version>
<Version>4.4.2</Version>
</PackageReference>
<PackageReference Include="Microsoft.NETCore.UniversalWindowsPlatform">
<Version>6.2.0-Preview1-26502-02</Version>
<Version>6.2.3</Version>
</PackageReference>
<PackageReference Include="Newtonsoft.Json">
<Version>11.0.2</Version>
<Version>12.0.1</Version>
</PackageReference>
<PackageReference Include="System.Security.Cryptography.Algorithms">
<Version>4.3.1</Version>
</PackageReference>
<PackageReference Include="Xamarin.Auth">
<Version>1.6.0.2</Version>
<Version>1.6.0.4</Version>
</PackageReference>
</ItemGroup>
<ItemGroup />

Просмотреть файл

@ -1,7 +1,7 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio 14
VisualStudioVersion = 14.0.22609.0
# Visual Studio 15
VisualStudioVersion = 15.0.28010.2041
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "GraphNotificationsSample", "GraphNotificationsSample.csproj", "{DC30CE66-DAEE-4CCF-BD02-8837FE918B6F}"
EndProject
@ -37,4 +37,7 @@ Global
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {968BBF48-DEBB-430C-8F86-426A8712D7CE}
EndGlobalSection
EndGlobal

Просмотреть файл

@ -30,14 +30,11 @@ namespace SDKTemplate
{
rootPage = MainPage.Current;
var accountProvider = ((App)Application.Current).AccountProvider;
if (accountProvider.SignedInAccount != null)
var connectedDevicesManager = ((App)Application.Current).ConnectedDevicesManager;
if (connectedDevicesManager.Accounts.Count > 0)
{
Description.Text = $"{accountProvider.SignedInAccount.Type} user ";
if (accountProvider.AadUser != null)
{
Description.Text += accountProvider.AadUser.DisplayableId;
}
var account = connectedDevicesManager.Accounts[0];
Description.Text = $"{account.Type} user ";
}
LogView.Text = Logger.Instance.AppLogs;

Просмотреть файл

@ -1,279 +0,0 @@
//*********************************************************
//
// Copyright (c) Microsoft. All rights reserved.
// This code is licensed under the Microsoft Public License.
// THIS CODE IS PROVIDED *AS IS* WITHOUT WARRANTY OF
// ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING ANY
// IMPLIED WARRANTIES OF FITNESS FOR A PARTICULAR
// PURPOSE, MERCHANTABILITY, OR NON-INFRINGEMENT.
//
//*********************************************************
using Microsoft.ConnectedDevices.Core;
using Microsoft.IdentityModel.Clients.ActiveDirectory;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Net.Http;
using System.Runtime.InteropServices.WindowsRuntime;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;
using Windows.Foundation;
using Windows.Security.Authentication.Web;
using Windows.Storage;
using Xamarin.Auth;
namespace SDKTemplate
{
public class MicrosoftAccountProvider : IConnectedDevicesUserAccountProvider
{
static readonly string CCSResource = "https://cdpcs.access.microsoft.com";
static readonly string MsaTokenKey = "MsaToken";
public event EventHandler SignOutCompleted;
public ConnectedDevicesUserAccount SignedInAccount { get; set; }
public string MsaToken { get; set; }
public UserInfo AadUser { get; set; }
public MicrosoftAccountProvider()
{
if (ApplicationData.Current.LocalSettings.Values.ContainsKey(MsaTokenKey))
{
MsaToken = ApplicationData.Current.LocalSettings.Values[MsaTokenKey] as string;
SignedInAccount = new ConnectedDevicesUserAccount(Guid.NewGuid().ToString(), ConnectedDevicesUserAccountType.MSA);
}
else
{
var authContext = new AuthenticationContext("https://login.microsoftonline.com/common");
if (authContext.TokenCache.Count > 0)
{
SignedInAccount = new ConnectedDevicesUserAccount(Guid.NewGuid().ToString(), ConnectedDevicesUserAccountType.AAD);
}
}
}
public async Task<bool> SignInAad()
{
var result = await GetAadTokenForUserAsync(CCSResource);
if (result.TokenRequestStatus == ConnectedDevicesAccessTokenRequestStatus.Success)
{
SignedInAccount = new ConnectedDevicesUserAccount(Guid.NewGuid().ToString(), ConnectedDevicesUserAccountType.AAD);
return true;
}
return false;
}
public async Task<bool> SignInMsa()
{
string refreshToken = string.Empty;
if (ApplicationData.Current.LocalSettings.Values.ContainsKey(MsaTokenKey))
{
refreshToken = ApplicationData.Current.LocalSettings.Values[MsaTokenKey] as string;
}
if (string.IsNullOrEmpty(refreshToken))
{
refreshToken = await MSAOAuthHelpers.GetRefreshTokenAsync();
}
if (!string.IsNullOrEmpty(refreshToken))
{
MsaToken = refreshToken;
ApplicationData.Current.LocalSettings.Values[MsaTokenKey] = refreshToken;
SignedInAccount = new ConnectedDevicesUserAccount(Guid.NewGuid().ToString(), ConnectedDevicesUserAccountType.MSA);
return true;
}
return false;
}
public void SignOut()
{
MsaToken = string.Empty;
AadUser = null;
SignedInAccount = null;
ApplicationData.Current.LocalSettings.Values.Remove(MsaTokenKey);
new AuthenticationContext("https://login.microsoftonline.com/common").TokenCache.Clear();
SignOutCompleted?.Invoke(null, new EventArgs());
}
IAsyncOperation<ConnectedDevicesAccessTokenResult> IConnectedDevicesUserAccountProvider.GetAccessTokenForUserAccountAsync(string userAccountId, IReadOnlyList<string> scopes)
{
Logger.Instance.LogMessage($"Token requested by platform for {userAccountId} and {string.Join(" ", scopes)}");
if (SignedInAccount.Type == ConnectedDevicesUserAccountType.AAD)
{
return GetAadTokenForUserAsync(scopes.First()).AsAsyncOperation();
}
else
{
return GetMsaTokenForUserAsync(scopes).AsAsyncOperation();
}
}
void IConnectedDevicesUserAccountProvider.OnAccessTokenError(string userAccountId, IReadOnlyList<string> scopes, bool isPermanentError)
{
Logger.Instance.LogMessage($"Bad token reported for {userAccountId} isPermanentError: {isPermanentError}");
}
IReadOnlyList<ConnectedDevicesUserAccount> IConnectedDevicesUserAccountProvider.UserAccounts
{
get
{
var accounts = new List<ConnectedDevicesUserAccount>();
var account = SignedInAccount;
if (account != null)
{
accounts.Add(account);
}
return accounts;
}
}
event TypedEventHandler<IConnectedDevicesUserAccountProvider, object> IConnectedDevicesUserAccountProvider.UserAccountChanged
{
add { return new EventRegistrationToken(); }
remove { }
}
private async Task<ConnectedDevicesAccessTokenResult> GetAadTokenForUserAsync(string audience)
{
try
{
var authContext = new AuthenticationContext("https://login.microsoftonline.com/common");
AuthenticationResult result = await authContext.AcquireTokenAsync(
audience, Secrets.AAD_CLIENT_ID, new Uri(Secrets.AAD_REDIRECT_URI), new PlatformParameters(PromptBehavior.Auto, true));
if (AadUser == null)
{
AadUser = result.UserInfo;
Logger.Instance.LogMessage($"SignIn done for {AadUser.DisplayableId}");
}
Logger.Instance.LogMessage($"AAD Token : {result.AccessToken}");
return new ConnectedDevicesAccessTokenResult(result.AccessToken, ConnectedDevicesAccessTokenRequestStatus.Success);
}
catch (Exception ex)
{
Logger.Instance.LogMessage($"AAD Token request failed: {ex.Message}");
return new ConnectedDevicesAccessTokenResult(string.Empty, ConnectedDevicesAccessTokenRequestStatus.TransientError);
}
}
private async Task<ConnectedDevicesAccessTokenResult> GetMsaTokenForUserAsync(IReadOnlyList<string> scopes)
{
try
{
string accessToken = await MSAOAuthHelpers.GetAccessTokenUsingRefreshTokenAsync(MsaToken, scopes);
Logger.Instance.LogMessage($"MSA Token : {accessToken}");
return new ConnectedDevicesAccessTokenResult(accessToken, ConnectedDevicesAccessTokenRequestStatus.Success);
}
catch (Exception ex)
{
Logger.Instance.LogMessage($"MSA Token request failed: {ex.Message}");
return new ConnectedDevicesAccessTokenResult(string.Empty, ConnectedDevicesAccessTokenRequestStatus.TransientError);
}
}
}
public class MSAOAuthHelpers
{
static readonly string ProdAuthorizeUrl = "https://login.live.com/oauth20_authorize.srf";
static readonly string ProdRedirectUrl = "https://login.microsoftonline.com/common/oauth2/nativeclient";
static readonly string ProdAccessTokenUrl = "https://login.live.com/oauth20_token.srf";
static readonly string OfflineAccessScope = "wl.offline_access";
static readonly string CCSScope = "ccs.ReadWrite";
static readonly string UserActivitiesScope = "https://activity.windows.com/UserActivity.ReadWrite.CreatedByApp";
static readonly string UserNotificationsScope = "https://activity.windows.com/Notifications.ReadWrite.CreatedByApp";
static Random Randomizer = new Random((int)DateTime.Now.Ticks);
static SHA256 HashProvider = SHA256.Create();
static async Task<IDictionary<string, string>> RequestAccessTokenAsync(string accessTokenUrl, IDictionary<string, string> queryValues)
{
var content = new FormUrlEncodedContent(queryValues);
HttpClient client = new HttpClient();
HttpResponseMessage response = await client.PostAsync(accessTokenUrl, content).ConfigureAwait(false);
string text = await response.Content.ReadAsStringAsync().ConfigureAwait(false);
// Parse the response
IDictionary<string, string> data = text.Contains("{") ? WebEx.JsonDecode(text) : WebEx.FormDecode(text);
if (data.ContainsKey("error"))
{
throw new AuthException(data["error_description"]);
}
return data;
}
public static async Task<string> GetRefreshTokenAsync()
{
byte[] buffer = new byte[32];
Randomizer.NextBytes(buffer);
var codeVerifier = Convert.ToBase64String(buffer).Replace('+', '-').Replace('/', '_').Replace("=", "");
byte[] hash = HashProvider.ComputeHash(Encoding.UTF8.GetBytes(codeVerifier));
var codeChallenge = Convert.ToBase64String(hash).Replace('+', '-').Replace('/', '_').Replace("=", "");
var redirectUri = new Uri(ProdRedirectUrl);
string scope = $"{OfflineAccessScope} {CCSScope} {UserNotificationsScope} {UserActivitiesScope}";
var startUri = new Uri($"{ProdAuthorizeUrl}?client_id={Secrets.MSA_CLIENT_ID}&response_type=code&code_challenge_method=S256&code_challenge={codeChallenge}&redirect_uri={ProdRedirectUrl}&scope={scope}");
var webAuthenticationResult = await WebAuthenticationBroker.AuthenticateAsync(
WebAuthenticationOptions.None,
startUri,
redirectUri);
if (webAuthenticationResult.ResponseStatus == WebAuthenticationStatus.Success)
{
var codeResponseUri = new Uri(webAuthenticationResult.ResponseData);
IDictionary<string, string> queryParams = WebEx.FormDecode(codeResponseUri.Query);
if (!queryParams.ContainsKey("code"))
{
return string.Empty;
}
string authCode = queryParams["code"];
Dictionary<string, string> refreshTokenQuery = new Dictionary<string, string>
{
{ "client_id", ProdClientId },
{ "redirect_uri", redirectUri.AbsoluteUri },
{ "grant_type", "authorization_code" },
{ "code", authCode },
{ "code_verifier", codeVerifier },
{ "scope", CCSScope }
};
IDictionary<string, string> refreshTokenResponse = await RequestAccessTokenAsync(ProdAccessTokenUrl, refreshTokenQuery);
if (refreshTokenResponse.ContainsKey("refresh_token"))
{
return refreshTokenResponse["refresh_token"];
}
}
return string.Empty;
}
public static async Task<string> GetAccessTokenUsingRefreshTokenAsync(string refreshToken, IReadOnlyList<string> scopes)
{
Dictionary<string, string> accessTokenQuery = new Dictionary<string, string>
{
{ "client_id", ProdClientId },
{ "redirect_uri", ProdRedirectUrl },
{ "grant_type", "refresh_token" },
{ "refresh_token", refreshToken },
{ "scope", string.Join(" ", scopes.ToArray()) },
};
IDictionary<string, string> accessTokenResponse = await RequestAccessTokenAsync(ProdAccessTokenUrl, accessTokenQuery);
if (accessTokenResponse == null || !accessTokenResponse.ContainsKey("access_token"))
{
throw new Exception("Unable to fetch access_token!");
}
return accessTokenResponse["access_token"];
}
}
}

Просмотреть файл

@ -38,6 +38,7 @@
<StackPanel Grid.Row="1" Orientation="Horizontal" Margin="10">
<Button Name="RefreshButton" Content="Retrieve Notifications History" Margin="10" BorderBrush="AntiqueWhite" Click="Button_Refresh"/>
<Button x:Name="LogoutButton" Content="Logout" BorderBrush="AntiqueWhite" Click="Button_Logout" HorizontalAlignment="Center" VerticalAlignment="Center"/>
</StackPanel>
<Grid Grid.Row="2">

Просмотреть файл

@ -12,6 +12,7 @@
using Microsoft.ConnectedDevices.UserData.UserNotifications;
using System;
using System.Collections.ObjectModel;
using System.Threading.Tasks;
using Windows.UI;
using Windows.UI.Core;
using Windows.UI.Xaml;
@ -49,40 +50,59 @@ namespace SDKTemplate
public partial class NotificationsPage : Page
{
private MainPage rootPage;
private ObservableCollection<NotificationListItem> activeNotifications = new ObservableCollection<NotificationListItem>();
private GraphNotificationProvider notificationCache;
private ObservableCollection<NotificationListItem> m_activeNotifications = new ObservableCollection<NotificationListItem>();
private Account m_account;
private UserNotificationsManager m_userNotificationManager;
public NotificationsPage()
{
InitializeComponent();
UnreadView.ItemsSource = activeNotifications;
UnreadView.ItemsSource = m_activeNotifications;
}
protected override void OnNavigatedTo(NavigationEventArgs e)
protected override async void OnNavigatedTo(NavigationEventArgs e)
{
rootPage = MainPage.Current;
var accountProvider = ((App)Application.Current).AccountProvider;
RefreshButton.IsEnabled = (accountProvider.SignedInAccount != null);
if (accountProvider.SignedInAccount != null)
{
Description.Text = $"{accountProvider.SignedInAccount.Type} user ";
if (accountProvider.AadUser != null)
{
Description.Text += accountProvider.AadUser.DisplayableId;
}
await RefreshAsync();
}
notificationCache = ((App)Application.Current).NotificationProvider;
notificationCache.CacheUpdated += Cache_CacheUpdated;
notificationCache.Refresh();
private async Task RefreshAsync()
{
m_account = null;
m_userNotificationManager = null;
m_activeNotifications.Clear();
// The ConnectedDevices SDK does not support multi-user currently. When this support becomes available
// the user would be sent via NavigationEventArgs. For now, just grab the first one if it exists.
var connectedDevicesManager = ((App)Application.Current).ConnectedDevicesManager;
if ((connectedDevicesManager.Accounts.Count > 0))
{
m_account = connectedDevicesManager.Accounts[0];
if (m_account.UserNotifications != null)
{
m_userNotificationManager = connectedDevicesManager.Accounts[0].UserNotifications;
}
}
RefreshButton.IsEnabled = (m_userNotificationManager != null);
LogoutButton.IsEnabled = (m_account != null);
if (m_account != null)
{
Description.Text = $"{m_account.Type} user ";
}
if (m_userNotificationManager != null)
{
m_userNotificationManager.CacheUpdated += Cache_CacheUpdated;
await m_userNotificationManager.RefreshAsync();
}
}
protected override void OnNavigatedFrom(NavigationEventArgs e)
{
if (notificationCache != null)
if (m_userNotificationManager != null)
{
notificationCache.CacheUpdated -= Cache_CacheUpdated;
m_userNotificationManager.CacheUpdated -= Cache_CacheUpdated;
}
}
@ -90,48 +110,61 @@ namespace SDKTemplate
{
await Dispatcher.RunAsync(CoreDispatcherPriority.Normal, () =>
{
activeNotifications.Clear();
foreach (UserNotification notification in notificationCache.HistoricalNotifications)
{
activeNotifications.Add(new NotificationListItem()
{
Id = notification.Id,
Content = $" Content:{notification.Content}",
UnreadState = notification.ReadState == UserNotificationReadState.Unread,
UserActionState = notification.UserActionState.ToString(),
Priority = $" Priority: {notification.Priority.ToString()}",
ExpirationTime = $" Expiry: {notification.ExpirationTime.ToLocalTime().ToString()}",
ChangeTime = $" Last Updated: {notification.ChangeTime.ToLocalTime().ToString()}",
});
}
if (notificationCache.NewNotifications)
{
rootPage.NotifyUser("History is up-to-date. New notifications available", NotifyType.StatusMessage);
}
else
{
rootPage.NotifyUser("History is up-to-date", NotifyType.StatusMessage);
}
RefreshNotifications();
});
}
private void Button_Refresh(object sender, RoutedEventArgs e)
private void RefreshNotifications()
{
m_activeNotifications.Clear();
foreach (UserNotification notification in m_userNotificationManager.HistoricalNotifications)
{
m_activeNotifications.Add(new NotificationListItem()
{
Id = notification.Id,
Content = $" Content:{notification.Content}",
UnreadState = notification.ReadState == UserNotificationReadState.Unread,
UserActionState = notification.UserActionState.ToString(),
Priority = $" Priority: {notification.Priority.ToString()}",
ExpirationTime = $" Expiry: {notification.ExpirationTime.ToLocalTime().ToString()}",
ChangeTime = $" Last Updated: {notification.ChangeTime.ToLocalTime().ToString()}",
});
}
if (m_userNotificationManager.NewNotifications)
{
rootPage.NotifyUser("History is up-to-date. New notifications available", NotifyType.StatusMessage);
}
else
{
rootPage.NotifyUser("History is up-to-date", NotifyType.StatusMessage);
}
}
private async void Button_Refresh(object sender, RoutedEventArgs e)
{
rootPage.NotifyUser("Updating history", NotifyType.StatusMessage);
notificationCache.Refresh();
await m_userNotificationManager.RefreshAsync();
}
private void Button_MarkRead(object sender, RoutedEventArgs e)
private async void Button_Logout(object sender, RoutedEventArgs e)
{
var item = ((Grid)((Border)((Button)sender).Parent).Parent).DataContext as NotificationListItem;
notificationCache.MarkRead(item.Id);
rootPage.NotifyUser("Logged out", NotifyType.ErrorMessage);
await ((App)Application.Current).ConnectedDevicesManager.LogoutAsync(m_account);
await RefreshAsync();
}
private void Button_Delete(object sender, RoutedEventArgs e)
private async void Button_MarkRead(object sender, RoutedEventArgs e)
{
var item = ((Grid)((Border)((Button)sender).Parent).Parent).DataContext as NotificationListItem;
notificationCache.Delete(item.Id);
await m_userNotificationManager.MarkReadAsync(item.Id);
}
private async void Button_Delete(object sender, RoutedEventArgs e)
{
var item = ((Grid)((Border)((Button)sender).Parent).Parent).DataContext as NotificationListItem;
await m_userNotificationManager.DeleteAsync(item.Id);
}
}
}

Просмотреть файл

@ -0,0 +1,18 @@
namespace SDKTemplate
{
class Secrets
{
// Get the following from Application Registration Portal (apps.dev.microsoft.com)
// MSA_CLIENT_ID: Application Id for MSA
// AAD_CLIENT_ID: Application Id for AAD
// AAD_REDIRECT_URI: Redirect Uri for AAD
// APP_HOST_NAME Cross-device domain for UserDataFeed
// These values are specific to this app, and can't be used by your app. You will need to register
// your app and get your own secrets, these are ours.
public static readonly string MSA_CLIENT_ID = "<<MSA client ID goes here>>";
public static readonly string AAD_CLIENT_ID = "<<AAD client ID goes here>>";
public static readonly string AAD_REDIRECT_URI = "<<AAD redirect URI goes here>>";
public static readonly string APP_HOST_NAME = "<<App cross-device domain goes here>>";
}
}

Просмотреть файл

@ -1,17 +0,0 @@
namespace SDKTemplate
{
class Secrets
{
// These come from the converged app registration portal at apps.dev.microsoft.com
// MSA_CLIENT_ID: Id of this app's registration in the MSA portal
// AAD_CLIENT_ID: Id of this app's registration in the Azure portal
// AAD_REDIRECT_URI: A Uri that this app is registered with in the Azure portal.
// AAD is supposed to use this Uri to call the app back after login (currently not true, external requirement)
// And this app is supposed to be able to handle this Uri (currently not true)
// APP_HOST_NAME Cross-device domain of this app's registration
static readonly string MSA_CLIENT_ID = "<<MSA client ID goes here>>";
static readonly string AAD_CLIENT_ID = "<<AAD client ID goes here>>";
static readonly string AAD_REDIRECT_URI = "<<AAD redirect URI goes here>>";
static readonly string APP_HOST_NAME = "<<App cross-device domain goes here>>";
}
}