Merged PR 13745: Update Windows Graph Notifications Sample
Update Windows Graph Notifications Sample
This commit is contained in:
Родитель
fa1f7d7847
Коммит
234c403ae0
|
@ -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>>";
|
||||
}
|
||||
}
|
Загрузка…
Ссылка в новой задаче