diff --git a/Android/samples/graphnotificationssample/app/src/main/AndroidManifest.xml b/Android/samples/graphnotificationssample/app/src/main/AndroidManifest.xml
index c40adf4..23f9a5b 100644
--- a/Android/samples/graphnotificationssample/app/src/main/AndroidManifest.xml
+++ b/Android/samples/graphnotificationssample/app/src/main/AndroidManifest.xml
@@ -2,12 +2,13 @@
-
+
+
-
@@ -31,6 +31,7 @@
+
\ No newline at end of file
diff --git a/Android/samples/graphnotificationssample/app/src/main/java/com/microsoft/connecteddevices/graphnotifications/Account.java b/Android/samples/graphnotificationssample/app/src/main/java/com/microsoft/connecteddevices/graphnotifications/Account.java
new file mode 100644
index 0000000..2f85b95
--- /dev/null
+++ b/Android/samples/graphnotificationssample/app/src/main/java/com/microsoft/connecteddevices/graphnotifications/Account.java
@@ -0,0 +1,219 @@
+//
+// Copyright (c) Microsoft Corporation. All rights reserved.
+//
+
+package com.microsoft.connecteddevices.graphnotifications;
+
+import android.app.Activity;
+import android.content.Context;
+import android.content.SharedPreferences;
+import android.util.Log;
+
+import com.microsoft.connecteddevices.ConnectedDevicesNotificationRegistration;
+import com.microsoft.connecteddevices.ConnectedDevicesAccount;
+import com.microsoft.connecteddevices.ConnectedDevicesAddAccountResult;
+import com.microsoft.connecteddevices.ConnectedDevicesAccountAddedStatus;
+import com.microsoft.connecteddevices.ConnectedDevicesPlatform;
+import com.microsoft.connecteddevices.signinhelpers.SigninHelperAccount;
+import com.microsoft.connecteddevices.AsyncOperation;
+
+import java.lang.IllegalStateException;
+import java.text.SimpleDateFormat;
+import java.util.Date;
+import java.util.List;
+
+/**
+ * Holds all state and logic for a single app + sdk account.
+ */
+public class Account {
+ // region Member Variables
+ private final String TAG = Account.class.getName();
+
+ public final String DATE_FORMAT = "MM/dd/yyyy HH:mm:ss";
+ public final String TIMESTAMP_KEY = "TIMESTAMP_KEY";
+ public final String PACKAGE_KEY = "PACKAGE_KEY";
+ public final String PACKAGE_VALUE = "com.microsoft.connecteddevices.graphnotifications";
+
+ private SigninHelperAccount mSignInHelper;
+ private ConnectedDevicesAccount mAccount;
+ private AccountRegistrationState mState;
+ private ConnectedDevicesPlatform mPlatform;
+ private UserNotificationsManager mNotificationsManager;
+ // endregion
+
+ // region Constructors
+ /**
+ * This constructor is for when the Account does not exist in the app cache,
+ * but does exist in the SDK cache.
+ * @param account
+ * @param platform
+ */
+ public Account(ConnectedDevicesAccount account, ConnectedDevicesPlatform platform) {
+ mAccount = account;
+ mState = AccountRegistrationState.IN_SDK_CACHE_ONLY;
+ mPlatform = platform;
+ }
+
+ /**
+ * This constructor is for when the Account is signed in.
+ * @param helperAccount
+ * @param state
+ * @param platform
+ */
+ public Account(SigninHelperAccount helperAccount, AccountRegistrationState state, ConnectedDevicesPlatform platform) {
+ // This account needs to be signed in, else the `Account` cannot be created
+ mSignInHelper = helperAccount;
+ mAccount = helperAccount.getAccount();
+ mState = state;
+ mPlatform = platform;
+ }
+ // endregion
+
+ // region public instance methods
+ /**
+ * Perform all actions required to have this account signed in, added to the
+ * ConnectedDevicesPlatform.AccountManager and registered with the Rome platform.
+ * @param context Application context
+ * @return The async result for this operation
+ */
+ public AsyncOperation prepareAccountAsync(final Context context) {
+ // 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, 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.
+ switch (mState) {
+ // Scenario 1
+ case IN_APP_CACHE_AND_SDK_CACHE:
+ mNotificationsManager = new UserNotificationsManager(context, mAccount, mPlatform);
+ return registerAccountWithSdkAsync();
+ // Scenario 2
+ case IN_APP_CACHE_ONLY: {
+ // Add the this account to the ConnectedDevicesPlatform.AccountManager
+ return mPlatform.getAccountManager().addAccountAsync(mAccount).thenComposeAsync((ConnectedDevicesAddAccountResult result) -> {
+ // We failed to add the account, so exit with a failure to prepare bool
+ if (result.getStatus() != ConnectedDevicesAccountAddedStatus.SUCCESS) {
+ Log.e(TAG, "Failed to add account " + mAccount.getId() + " to the AccountManager due to " + result.getStatus());
+ return AsyncOperation.completedFuture(false);
+ }
+
+ // Set the registration state of this account as in both app and sdk cache
+ mState = AccountRegistrationState.IN_APP_CACHE_AND_SDK_CACHE;
+ mNotificationsManager = new UserNotificationsManager(context, mAccount, mPlatform);
+ return registerAccountWithSdkAsync();
+ });
+ }
+ // Scenario 3
+ case IN_SDK_CACHE_ONLY:
+ // Remove the account from the SDK since the app has no knowledge of it
+ mPlatform.getAccountManager().removeAccountAsync(mAccount);
+ // This account could not be prepared
+ return AsyncOperation.completedFuture(false);
+ default:
+ // This account could not be prepared
+ Log.e(TAG, "Failed to prepare account " + mAccount.getId() + " due to unknown state!");
+ return AsyncOperation.completedFuture(false);
+ }
+ }
+
+ /**
+ * Performs non-blocking registrations for this account, which are
+ * for notifications then for the relay SDK.
+ * @return The async result for this operation
+ */
+ public AsyncOperation registerAccountWithSdkAsync() {
+ if (mState != AccountRegistrationState.IN_APP_CACHE_AND_SDK_CACHE) {
+ AsyncOperation toReturn = new AsyncOperation<>();
+ toReturn.completeExceptionally(new IllegalStateException("Cannot register this account due to bad state: " + mAccount.getId()));
+ return toReturn;
+ }
+
+ // Grab the shared GCM/FCM notification token from this app's BroadcastReceiver
+ return RomeNotificationReceiver.getNotificationRegistrationAsync().thenComposeAsync((ConnectedDevicesNotificationRegistration notificationRegistration) -> {
+ // Perform the registration using the NotificationRegistration
+ return mPlatform.getNotificationRegistrationManager().registerForAccountAsync(mAccount, notificationRegistration)
+ .thenComposeAsync((success) -> {
+ if (success) {
+ Log.i(TAG, "Successfully registered account " + mAccount.getId() + " for cloud notifications");
+ } else {
+ Log.e(TAG, "Failed to register account " + mAccount.getId() + " for cloud notifications!");
+ }
+
+ return mNotificationsManager.registerForAccountAsync();
+ });
+ });
+ }
+
+ /**
+ * Get an access token for this account which satisfies the given scopes
+ * @param scopes Scopes the access token must have been requested with
+ * @return The async result for this operation
+ */
+ public AsyncOperation getAccessTokenAsync(final List scopes) {
+ return mSignInHelper.getAccessTokenAsync(scopes);
+ }
+
+ /**
+ * Tear down and sign this account out
+ * @param activity Application activity
+ * @return The async result for this operation
+ */
+ public AsyncOperation logoutAsync(Activity activity) {
+ return mSignInHelper.signOut(activity);
+ }
+
+ /**
+ * Get the ConnectedDevicesAccount
+ */
+ public ConnectedDevicesAccount getAccount() {
+ return mAccount;
+ }
+
+ /**
+ * Get the AccountRegistrationState
+ */
+ public AccountRegistrationState getRegistrationState() {
+ return mState;
+ }
+
+ /**
+ * Get the UserNotificationsManager
+ */
+ public UserNotificationsManager getNotificationsManager() { return mNotificationsManager; }
+ // endregion
+
+ // region private instance methods
+
+ /**
+ * Grab the initial registration date-time if one is found, otherwise generate a new one.
+ * @param context Application context
+ * @return Datetime to insert into the RemoteSystemAppRegistration
+ */
+ private String getInitialRegistrationDateTime(final Context context) {
+ SharedPreferences preferences = context.getSharedPreferences(context.getPackageName(), Context.MODE_PRIVATE);
+
+ String timestamp;
+ // Check that the SharedPreferences has the timestamp. This should be true after the first clean install -> Platform init.
+ if (preferences.contains(TIMESTAMP_KEY)) {
+ // The `getString` API requires a default value. Since we check that key exists we should never get the default value of empty
+ // string.
+ timestamp = preferences.getString(TIMESTAMP_KEY, "");
+ if (timestamp.isEmpty()) {
+ Log.e(TAG, "getInitialRegistrationDateTime failed to get the TimeStamp although the key exists");
+ throw new RuntimeException("Failed to get TimeStamp after verifying it exists");
+ }
+ } else {
+ // Create the initial timestamp for RemoteSystemApp registration and store it in SharedPreferences
+ timestamp = new SimpleDateFormat(DATE_FORMAT).format(new Date());
+ preferences.edit().putString(TIMESTAMP_KEY, timestamp).apply();
+ }
+
+ return timestamp;
+ }
+ // endregion
+}
diff --git a/Android/samples/graphnotificationssample/app/src/main/java/com/microsoft/connecteddevices/graphnotifications/AccountRegistrationState.java b/Android/samples/graphnotificationssample/app/src/main/java/com/microsoft/connecteddevices/graphnotifications/AccountRegistrationState.java
new file mode 100644
index 0000000..a1bd8a8
--- /dev/null
+++ b/Android/samples/graphnotificationssample/app/src/main/java/com/microsoft/connecteddevices/graphnotifications/AccountRegistrationState.java
@@ -0,0 +1,11 @@
+//
+// Copyright (c) Microsoft Corporation. All rights reserved.
+//
+
+package com.microsoft.connecteddevices.graphnotifications;
+
+public enum AccountRegistrationState {
+ IN_APP_CACHE_AND_SDK_CACHE,
+ IN_APP_CACHE_ONLY,
+ IN_SDK_CACHE_ONLY
+}
diff --git a/Android/samples/graphnotificationssample/app/src/main/java/com/microsoft/connecteddevices/graphnotifications/ConnectedDevicesManager.java b/Android/samples/graphnotificationssample/app/src/main/java/com/microsoft/connecteddevices/graphnotifications/ConnectedDevicesManager.java
new file mode 100644
index 0000000..15a7ffe
--- /dev/null
+++ b/Android/samples/graphnotificationssample/app/src/main/java/com/microsoft/connecteddevices/graphnotifications/ConnectedDevicesManager.java
@@ -0,0 +1,456 @@
+//
+// Copyright (c) Microsoft Corporation. All rights reserved.
+//
+
+package com.microsoft.connecteddevices.graphnotifications;
+
+import android.app.Activity;
+import android.content.Context;
+import android.util.ArrayMap;
+import android.util.Log;
+
+import com.microsoft.connecteddevices.AsyncOperation;
+import com.microsoft.connecteddevices.ConnectedDevicesAccessTokenRequest;
+import com.microsoft.connecteddevices.ConnectedDevicesAccessTokenRequestedEventArgs;
+import com.microsoft.connecteddevices.ConnectedDevicesAccessTokenInvalidatedEventArgs;
+import com.microsoft.connecteddevices.ConnectedDevicesAccount;
+import com.microsoft.connecteddevices.ConnectedDevicesAccountManager;
+import com.microsoft.connecteddevices.ConnectedDevicesAccountType;
+import com.microsoft.connecteddevices.ConnectedDevicesAddAccountResult;
+import com.microsoft.connecteddevices.ConnectedDevicesNotificationRegistration;
+import com.microsoft.connecteddevices.ConnectedDevicesNotificationRegistrationStateChangedEventArgs;
+import com.microsoft.connecteddevices.ConnectedDevicesNotificationType;
+import com.microsoft.connecteddevices.ConnectedDevicesNotificationRegistrationManager;
+import com.microsoft.connecteddevices.ConnectedDevicesNotificationRegistrationState;
+import com.microsoft.connecteddevices.ConnectedDevicesPlatform;
+import com.microsoft.connecteddevices.signinhelpers.AADSigninHelperAccount;
+import com.microsoft.connecteddevices.signinhelpers.MSASigninHelperAccount;
+import com.microsoft.connecteddevices.signinhelpers.SigninHelperAccount;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * This is a singleton object which holds onto the app's ConnectedDevicesPlatform and handles account management.
+ */
+public class ConnectedDevicesManager {
+ // region Member Variables
+ private final String TAG = ConnectedDevicesManager.class.getName();
+
+ private List mAccounts;
+ private RomeNotificationReceiver mNotificationReceiver;
+ private ConnectedDevicesPlatform mPlatform;
+
+ private static ConnectedDevicesManager sConnectedDevicesManager;
+ // endregion
+
+ // region Constructors
+ /**
+ * This is a singleton object which holds onto the app's ConnectedDevicesPlatform and handles account management.
+ * @param context Application context
+ */
+ private ConnectedDevicesManager(Context context) {
+ // Initialize list of known accounts
+ mAccounts = new ArrayList();
+
+ // Create the NotificationReceiver
+ mNotificationReceiver = new RomeNotificationReceiver(context);
+
+ // Create Platform
+ mPlatform = new ConnectedDevicesPlatform(context);
+
+ // Create a final reference to the list of accounts
+ final List accounts = mAccounts;
+
+ // Subscribe to the AccessTokenRequested event
+ mPlatform.getAccountManager().accessTokenRequested().subscribe((accountManager, args) -> onAccessTokenRequested(accountManager, args, accounts));
+
+ // Subscribe to AccessTokenInvalidated event
+ mPlatform.getAccountManager().accessTokenInvalidated().subscribe((accountManager, args) -> onAccessTokenInvalidated(accountManager, args, accounts));
+
+ // Subscribe to NotificationRegistrationStateChanged event
+ mPlatform.getNotificationRegistrationManager().notificationRegistrationStateChanged().subscribe((notificationRegistrationManager, args) -> onNotificationRegistrationStateChanged(notificationRegistrationManager, args, accounts));
+
+ // Start the platform as we have subscribed to the events it can raise
+ mPlatform.start();
+
+ // Pull the accounts from our app's cache and synchronize the list with the apps cached by
+ // ConnectedDevicesPlatform.AccountManager.
+ List deserializedAccounts = deserializeAccounts(context);
+
+ // Finally initialize the accounts. This will refresh registrations when needed, add missing accounts,
+ // and remove stale accounts from the ConnectedDevicesPlatform AccountManager. The AsyncOperation associated
+ // with all of this asynchronous work need not be waited on as any sub component work will be accomplished
+ // in the synchronous portion of the call. If your app needs to sequence when other apps can see this app's registration
+ // (i.e. when RemoteSystemAppRegistration SaveAsync completes) then it would be useful to use the AsyncOperation returned by
+ // prepareAccountsAsync
+ prepareAccounts(deserializedAccounts, context);
+ }
+ // endregion
+
+ // region public static methods
+ public static synchronized ConnectedDevicesManager getConnectedDevicesManager(Context context) {
+ if (sConnectedDevicesManager == null) {
+ sConnectedDevicesManager = new ConnectedDevicesManager(context);
+ }
+ return sConnectedDevicesManager;
+ }
+ // endregion
+
+ // region public instance methods
+
+ /**
+ * Get the ConnectedDevicesPlatform owned by this ConnectedDevicesManager.
+ * @return Platform
+ */
+ public ConnectedDevicesPlatform getPlatform() {
+ return mPlatform;
+ }
+
+ /**
+ * Attempt to ensure there is a signed in MSA Account
+ * @param activity Application activity
+ * @return The async result for when this operation completes
+ */
+ public synchronized AsyncOperation signInMsaAsync(final Activity activity) {
+ // Create a SigninHelperAccount with a client id for msa, a map of requested scopes to override, and the context
+ final Map msaScopeOverrides = new ArrayMap<>();
+ msaScopeOverrides.put("https://activity.windows.com/UserActivity.ReadWrite.CreatedByApp",
+ new String[] { "https://activity.windows.com/UserActivity.ReadWrite.CreatedByApp",
+ "https://activity.windows.com/Notifications.ReadWrite.CreatedByApp"});
+ SigninHelperAccount signInHelper = new MSASigninHelperAccount(Secrets.MSA_CLIENT_ID, msaScopeOverrides, (Context)activity);
+
+ if (signInHelper.isSignedIn()) {
+ Log.i(TAG, "Already signed in with a MSA account");
+ return AsyncOperation.completedFuture(true);
+ }
+
+ Log.i(TAG, "Signing in with a MSA account");
+
+ // Call signIn, which may prompt the user to enter credentials or just retreive a cached token if they exist and are valid
+ return signInHelper.signIn(activity).thenComposeAsync((ConnectedDevicesAccount account) -> {
+ // Prepare the account, adding it to the list of app's cached accounts is prepared successfully
+ return prepareAccountAsync(new Account(signInHelper, AccountRegistrationState.IN_APP_CACHE_ONLY, mPlatform), (Context)activity);
+ });
+ }
+
+ /**
+ * Attempt to ensure there is a signed in MSA Account
+ * @param activity Application activity
+ * @return The async result for when this operation completes
+ */
+ public synchronized AsyncOperation signInAadAsync(final Activity activity) {
+ // Create a SigninHelperAccount with a client id for msa, a map of requested scopes to override, and the context
+ SigninHelperAccount signInHelper = new AADSigninHelperAccount(Secrets.AAD_CLIENT_ID, Secrets.AAD_REDIRECT_URI, (Context)activity);
+
+ if (signInHelper.isSignedIn()) {
+ Log.i(TAG, "Already signed in with a AAD account");
+ return AsyncOperation.completedFuture(true);
+ }
+
+ Log.i(TAG, "Signing in in with a AAD account");
+
+ // Call signIn, which may prompt the user to enter credentials or just retreive a cached token if they exist and are valid
+ return signInHelper.signIn(activity).thenComposeAsync((ConnectedDevicesAccount account) -> {
+ // Prepare the account, adding it to the list of app's cached accounts is prepared successfully
+ return prepareAccountAsync(new Account(signInHelper, AccountRegistrationState.IN_APP_CACHE_ONLY, mPlatform), (Context)activity);
+ });
+ }
+
+ /**
+ * Sign out and remove the given Account from the ConnectedDevicesManager
+ * @param activity Application activity
+ * @return The async result for when this operation completes
+ */
+ public synchronized AsyncOperation logout(Activity activity) {
+ // First remove this account from the list of "ready to go" accounts so it cannot be used while logging out
+ Account accountToRemove = getSignedInAccount();
+ mAccounts.remove(accountToRemove);
+
+ // Now log out this account
+ return accountToRemove.logoutAsync(activity).thenComposeAsync((ConnectedDevicesAccount account) -> {
+ Log.i(TAG, "Successfully signed out account: " + account.getId());
+ return AsyncOperation.completedFuture(true);
+ });
+ }
+
+ /**
+ * Get a list of "ready-to-go" accounts owned by this ConnectedDevicesManager.
+ * @return accounts
+ */
+ public Account getSignedInAccount() {
+
+ // Compare the app cached account to find a match in the sdk cached accounts
+ if (mAccounts.size() > 0) {
+ return mAccounts.get(0);
+ }
+
+ Log.e(TAG, "No signed in account found!");
+ return null;
+ }
+
+ /**
+ * Create a NotificationRegistration using the notification token gained from GCM/FCM.
+ * @param token Notification token gained by the BroadcastReceiver
+ */
+ public synchronized void setNotificationRegistration(final String token) {
+ // Get the NotificationRegistrationManager from the platform
+ ConnectedDevicesNotificationRegistrationManager registrationManager = mPlatform.getNotificationRegistrationManager();
+
+ // Create a NotificationRegistration obect to store all notification information
+ ConnectedDevicesNotificationRegistration registration = new ConnectedDevicesNotificationRegistration();
+ registration.setType(ConnectedDevicesNotificationType.FCM);
+ registration.setToken(token);
+ registration.setAppId(Secrets.FCM_SENDER_ID);
+ registration.setAppDisplayName("GraphNotificationsSample");
+
+ Log.i(TAG, "Completing the RomeNotificationReceiver operation with token: " + token);
+
+ // For each prepared account, register for notifications
+ for (final Account account : mAccounts) {
+ registrationManager.registerForAccountAsync(account.getAccount(), registration)
+ .whenCompleteAsync((Boolean success, Throwable throwable) -> {
+ if (throwable != null) {
+ Log.e(TAG, "Exception encountered in registerForAccountAsync", throwable);
+ } else if (!success) {
+ Log.e(TAG, "Failed to register account " + account.getAccount().getId() + " for cloud notifications!");
+ } else {
+ Log.i(TAG, "Successfully registered account " + account.getAccount().getId() + " for cloud notifications");
+ }
+ });
+ }
+
+ // The two cases of receiving a new notification token are:
+ // 1. A notification registration is asked for and now it is available. In this case there is a pending promise that was made
+ // at the time of requesting the information. It now needs to be completed.
+ // 2. The account is already registered but for whatever reason the registration changes (GCM/FCM gives the app a new token)
+ //
+ // In order to most cleanly handle both cases set the new notification information and then trigger a re registration of all accounts
+ // that are in good standing.
+ RomeNotificationReceiver.setNotificationRegistration(registration);
+
+ // For all the accounts which have been prepared successfully, perform SDK registration
+ for (Account account : mAccounts) {
+ if (account.getRegistrationState() == AccountRegistrationState.IN_APP_CACHE_AND_SDK_CACHE) {
+ account.registerAccountWithSdkAsync();
+ }
+ }
+ }
+
+ // endregion
+
+ // region private instance methods
+ /**
+ * Pull the accounts from our app's cache and synchronize the list with the
+ * apps cached by ConnectedDevicesPlatform.AccountManager.
+ * @param context Application context
+ * @return List of accounts from the app and SDK's cache
+ */
+ private List deserializeAccounts(Context context) {
+ // Since our helper lib can only cache 1 app at a time, we create sign-in helper,
+ // which does user account and access token management for us. Takes three parameters:
+ // a client id for msa, a map of requested auto scopes to override, and the context
+ SigninHelperAccount signInHelper = new MSASigninHelperAccount(Secrets.MSA_CLIENT_ID, new ArrayMap(), context);
+
+ // Get all of the ConnectedDevicesPlatform's added accounts
+ List sdkCachedAccounts = mPlatform.getAccountManager().getAccounts();
+
+ List returnAccounts = new ArrayList();
+
+ // If there is a signed in account in the app's cache, find it exists in the SDK's cache
+ if (signInHelper.isSignedIn()) {
+ // Check if the account is also present in ConnectedDevicesPlatform.AccountManager.
+ ConnectedDevicesAccount sdkCachedAccount = findFirst(sdkCachedAccounts, account -> accountsMatch(signInHelper.getAccount(), account));
+
+ AccountRegistrationState registrationState;
+ if (sdkCachedAccount != null) {
+ // 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.IN_APP_CACHE_AND_SDK_CACHE;
+ sdkCachedAccounts.remove(sdkCachedAccount);
+ } else {
+ // Account not found in the SDK cache. Later when we initialize the Account,
+ // it will be added to the SDK cache and perform registration.
+ registrationState = AccountRegistrationState.IN_APP_CACHE_ONLY;
+ }
+
+ // Add the app's cached account with the correct registration state
+ returnAccounts.add(new Account(signInHelper, registrationState, mPlatform));
+ }
+
+ // Add all the accounts which exist only in the SDK
+ for (ConnectedDevicesAccount account : sdkCachedAccounts) {
+ returnAccounts.add(new Account(account, mPlatform));
+ }
+
+ return returnAccounts;
+ }
+
+ /**
+ * Replacement for the java.util.function.Predicate to support pre Java 8 / API 24.
+ */
+ interface Predicate {
+ public boolean test(T t);
+ }
+
+ /**
+ * Replacement for stream.filter.findFirst to support pre Java 8 / API 24.
+ * @param list List to search
+ * @param predicate Predicate to use against the given list
+ * @return First item matching the given predicate, null if none found
+ */
+ private static T findFirst(List list, Predicate super T> predicate) {
+ for (T item : list) {
+ if (predicate.test(item)) {
+ return item;
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * Matcher function to compare ConnectedDevicesAccounts are equal
+ * @param account1 ConnectedDevicesAccount 1
+ * @param account2 ConnectedDevicesAccount 2
+ * @return Boolean of if the given accounts match
+ */
+ private static boolean accountsMatch(ConnectedDevicesAccount account1, ConnectedDevicesAccount account2) {
+ String accountId1 = account1.getId();
+ ConnectedDevicesAccountType accountType1 = account1.getType();
+
+ String accountId2 = account2.getId();
+ ConnectedDevicesAccountType accountType2 = account2.getType();
+
+ return accountId2.equals(accountId1) && accountType2.equals(accountType1);
+ }
+
+ /**
+ * Prepare the accounts; refresh registrations when needed, add missing accounts and remove stale accounts from the ConnectedDevicesPlatform.AccountManager.
+ * @param context Application context
+ * @return A async operation that will complete when all accounts are prepared
+ */
+ private AsyncOperation prepareAccounts(List accounts, Context context) {
+ List> operations = new ArrayList<>();
+
+ // Kick off all the account preparation and store the AsyncOperations
+ for (Account account : accounts) {
+ operations.add(prepareAccountAsync(account, context));
+ }
+
+ // Return an operation that will complete when all of the operations complete.
+ return AsyncOperation.allOf(operations.toArray(new AsyncOperation[operations.size()]));
+ }
+
+ /**
+ * Attempt to prepare the account. If the account was prepared successfully, add it to the list of "ready to use" accounts.
+ * @param context Application context
+ * @return AsyncOperation with the exception captured
+ */
+ private AsyncOperation prepareAccountAsync(Account account, Context context) {
+ Log.v(TAG, "Preparing account: " + account.getAccount().getId());
+
+ // Add the account to the list of available accounts
+ mAccounts.add(account);
+
+ // Prepare the account, removing it from the list of accounts if it failed
+ return account.prepareAccountAsync(context).thenComposeAsync((success) -> {
+ // If an exception is raised or we gracefully fail to prepare the account, remove it
+ if (success) {
+ Log.i(TAG, "Account: " + account.getAccount().getId() + " is ready-to-go");
+ } else {
+ mAccounts.remove(account);
+ Log.w(TAG, "Account: " + account.getAccount().getId() + " is not ready, removed from the list of ready-to-go accounts!");
+ }
+
+ // Return the success of the account preparation
+ return AsyncOperation.completedFuture(success);
+ }).exceptionally((Throwable throwable) -> {
+ mAccounts.remove(account);
+ Log.e(TAG, "Account: " + account.getAccount().getId() + " is not ready, removed from the list of ready-to-go accounts as an exception was encountered", throwable);
+ // Return the account preparation was not successful
+ return false;
+ });
+ }
+
+ /**
+ * This event is fired when there is a need to request a token. This event should be subscribed and ready to respond before any request is sent out.
+ * @param sender ConnectedDevicesAccountManager which is making the request
+ * @param args Contains arguments for the event
+ * @param accounts List of accounts to search for
+ */
+ private void onAccessTokenRequested(ConnectedDevicesAccountManager sender, ConnectedDevicesAccessTokenRequestedEventArgs args, List accounts) {
+ ConnectedDevicesAccessTokenRequest request = args.getRequest();
+ List scopes = request.getScopes();
+
+ // Compare the app cached account to find a match in the sdk cached accounts
+ Account account = findFirst(accounts, acc -> accountsMatch(request.getAccount(), acc.getAccount()));
+
+ // We always need to complete the request, even if a matching account is not found
+ if (account == null) {
+ Log.e(TAG, "Failed to find a SigninHelperAccount matching the given account for the token request");
+ request.completeWithErrorMessage("The app could not find a matching ConnectedDevicesAccount to get a token");
+ return;
+ }
+
+ // Complete the request with a token
+ account.getAccessTokenAsync(scopes)
+ .thenAcceptAsync((String token) -> {
+ request.completeWithAccessToken(token);
+ }).exceptionally(throwable -> {
+ request.completeWithErrorMessage("The Account could not return a token with those scopes");
+ return null;
+ });
+ }
+
+ /**
+ * This event is fired when a token consumer reports a token error. The token provider needs to
+ * either refresh their token cache or request a new user login to fix their account setup.
+ * If access token in invalidated, refresh token and renew access token.
+ * @param sender ConnectedDevicesAccountManager which is making the request
+ * @param args Contains arguments for the event
+ * @param accounts List of accounts to search for
+ */
+ private void onAccessTokenInvalidated(ConnectedDevicesAccountManager sender, ConnectedDevicesAccessTokenInvalidatedEventArgs args, List accounts) {
+ Log.i(TAG, "Token invalidated for account: " + args.getAccount().getId());
+ }
+
+ /**
+ * Event for when the registration state changes for a given account.
+ * @param sender ConnectedDevicesNotificationRegistrationManager which is making the request
+ * @param args Contains arguments for the event
+ * @param accounts List of accounts to search for
+ */
+ private void onNotificationRegistrationStateChanged(ConnectedDevicesNotificationRegistrationManager sender, ConnectedDevicesNotificationRegistrationStateChangedEventArgs args, List accounts) {
+ // If notification registration state is expiring or expired, re-register for account again.
+ ConnectedDevicesNotificationRegistrationState state = args.getState();
+ switch (args.getState()) {
+ case UNREGISTERED:
+ Log.w(TAG, "Notification registration state is unregistered for account: " + args.getAccount().getId());
+ break;
+ case REGISTERED:
+ Log.i(TAG, "Notification registration state is registered for account: " + args.getAccount().getId());
+ break;
+ case EXPIRING: // fallthrough
+ case EXPIRED:
+ {
+ // Because the notificaiton registration is expiring, the per account registration work needs to be kicked off again.
+ // This means registering with the NotificationRegistrationManager as well as any sub component work like RemoteSystemAppRegistration.
+ Log.i(TAG, "Notification " + args.getState() + " for account: " + args.getAccount().getId());
+ Account account = findFirst(accounts, acc -> accountsMatch(args.getAccount(), acc.getAccount()));
+
+ // If the account has been prepared for use then re-register the account with SDK
+ if (account != null && account.getRegistrationState() == AccountRegistrationState.IN_APP_CACHE_AND_SDK_CACHE) {
+ account.registerAccountWithSdkAsync();
+ }
+ break;
+ }
+ default:
+ break;
+ }
+
+ }
+ // endregion
+}
diff --git a/Android/samples/graphnotificationssample/app/src/main/java/com/microsoft/connecteddevices/graphnotifications/FCMListenerService.java b/Android/samples/graphnotificationssample/app/src/main/java/com/microsoft/connecteddevices/graphnotifications/FCMListenerService.java
index 79ad95d..e4f903b 100644
--- a/Android/samples/graphnotificationssample/app/src/main/java/com/microsoft/connecteddevices/graphnotifications/FCMListenerService.java
+++ b/Android/samples/graphnotificationssample/app/src/main/java/com/microsoft/connecteddevices/graphnotifications/FCMListenerService.java
@@ -5,17 +5,13 @@
package com.microsoft.connecteddevices.graphnotifications;
import android.content.Intent;
-import android.support.annotation.NonNull;
import android.support.v4.content.LocalBroadcastManager;
-import android.util.ArrayMap;
import android.util.Log;
-import com.google.android.gms.tasks.OnCompleteListener;
-import com.google.android.gms.tasks.Task;
import com.google.firebase.iid.FirebaseInstanceId;
-import com.google.firebase.iid.InstanceIdResult;
import com.google.firebase.messaging.FirebaseMessagingService;
import com.google.firebase.messaging.RemoteMessage;
+
import com.microsoft.connecteddevices.ConnectedDevicesPlatform;
import java.util.Map;
@@ -25,15 +21,13 @@ import java.util.Map;
*/
public class FCMListenerService extends FirebaseMessagingService {
private static final String TAG = "FCMListenerService";
-
- private static final String RegistrationComplete = "registrationComplete";
+ private static final String REGISTRATION_COMPLETE = "registrationComplete";
private static final String TOKEN = "TOKEN";
- private static String s_previousToken = null;
+ private static String sPreviousToken = "";
@Override
public void onCreate() {
- PlatformManager.getInstance().createNotificationReceiver(this);
FirebaseInstanceId.getInstance().getInstanceId().addOnCompleteListener(task -> {
if (task.isSuccessful()) {
String token = task.getResult().getToken();
@@ -45,38 +39,30 @@ public class FCMListenerService extends FirebaseMessagingService {
}
/**
- * Check whether it's a rome notification or not.
+ * Check whether it's a Rome notification or not.
* If it is a rome notification,
* It will notify the apps with the information in the notification.
* @param message FCM class for messaging with a from a data field.
*/
@Override
public void onMessageReceived(RemoteMessage message) {
- Log.d(TAG, "From: " + message.getFrom());
+ Log.d(TAG, "FCM notification received from: " + message.getFrom());
Map data = message.getData();
- ConnectedDevicesPlatform platform = ensurePlatformInitialized();
- platform.processNotification(data);
+ try {
+ ConnectedDevicesPlatform platform = ConnectedDevicesManager.getConnectedDevicesManager(getApplicationContext()).getPlatform();
+ platform.processNotification(data);
+ } catch (Exception e) {
+ Log.e(TAG, "Failed to process FCM notification" + e.getMessage());
+ }
}
@Override
public void onNewToken(String token) {
- if (token != null && !token.equals(s_previousToken)) {
- s_previousToken = token;
- Intent registrationComplete = new Intent(RegistrationComplete);
+ if (token != null && !token.equals(sPreviousToken)) {
+ sPreviousToken = token;
+ Intent registrationComplete = new Intent(REGISTRATION_COMPLETE);
registrationComplete.putExtra(TOKEN, token);
LocalBroadcastManager.getInstance(this).sendBroadcast(registrationComplete);
}
}
-
- synchronized ConnectedDevicesPlatform ensurePlatformInitialized() {
- // First see if we have an existing platform
- ConnectedDevicesPlatform platform = PlatformManager.getInstance().getPlatform();
- if (platform != null) {
- return platform;
- }
-
- // No existing platform, so we have to create our own
- return PlatformManager.getInstance().createPlatform(getApplicationContext());
- }
-
}
diff --git a/Android/samples/graphnotificationssample/app/src/main/java/com/microsoft/connecteddevices/graphnotifications/FCMRegistrationIntentService.java b/Android/samples/graphnotificationssample/app/src/main/java/com/microsoft/connecteddevices/graphnotifications/FCMRegistrationIntentService.java
new file mode 100644
index 0000000..f37345b
--- /dev/null
+++ b/Android/samples/graphnotificationssample/app/src/main/java/com/microsoft/connecteddevices/graphnotifications/FCMRegistrationIntentService.java
@@ -0,0 +1,49 @@
+//
+// Copyright (c) Microsoft Corporation. All rights reserved.
+//
+
+package com.microsoft.connecteddevices.graphnotifications;
+
+import android.app.IntentService;
+import android.content.Intent;
+import android.support.v4.content.LocalBroadcastManager;
+import android.util.Log;
+
+import com.google.firebase.iid.FirebaseInstanceId;
+
+import java.io.IOException;
+
+public class FCMRegistrationIntentService extends IntentService {
+ private static final String TAG = FCMRegistrationIntentService.class.getName();
+ private static final String REGISTRATION_COMPLETE = "registrationComplete";
+ private static final String TOKEN = "TOKEN";
+ private static final String INTENT_NAME = "FCMRegIntentService";
+ private static final String FCM_SENDER_ID = Secrets.FCM_SENDER_ID;
+
+ public FCMRegistrationIntentService() {
+ super(INTENT_NAME);
+ }
+
+ public FCMRegistrationIntentService(String name) {
+ super(name);
+ }
+
+ @Override
+ protected void onHandleIntent(Intent intent) {
+ String token = "";
+ if (FCM_SENDER_ID == null || FCM_SENDER_ID.isEmpty()) {
+ Log.i(TAG, "FCM SenderID is null/empty, skipping FCM registration!");
+ } else {
+ try {
+ token = FirebaseInstanceId.getInstance().getToken(FCM_SENDER_ID, "FCM");
+ Log.i(TAG, "FCM registration token: " + token);
+ } catch (IOException e) {
+ Log.e(TAG, "Failed to get FCM registration token " + e.getMessage());
+ }
+
+ Intent registrationComplete = new Intent(REGISTRATION_COMPLETE);
+ registrationComplete.putExtra(TOKEN, token);
+ LocalBroadcastManager.getInstance(this).sendBroadcast(registrationComplete);
+ }
+ }
+}
diff --git a/Android/samples/graphnotificationssample/app/src/main/java/com/microsoft/connecteddevices/graphnotifications/MainActivity.java b/Android/samples/graphnotificationssample/app/src/main/java/com/microsoft/connecteddevices/graphnotifications/MainActivity.java
index c894f32..ee135db 100644
--- a/Android/samples/graphnotificationssample/app/src/main/java/com/microsoft/connecteddevices/graphnotifications/MainActivity.java
+++ b/Android/samples/graphnotificationssample/app/src/main/java/com/microsoft/connecteddevices/graphnotifications/MainActivity.java
@@ -10,6 +10,7 @@ import android.app.NotificationManager;
import android.app.PendingIntent;
import android.content.Context;
import android.content.Intent;
+import android.graphics.Color;
import android.os.Bundle;
import android.support.annotation.NonNull;
import android.support.design.widget.TabLayout;
@@ -21,7 +22,6 @@ import android.support.v4.app.NotificationManagerCompat;
import android.support.v4.view.ViewPager;
import android.support.v7.app.AppCompatActivity;
import android.support.v7.widget.Toolbar;
-import android.util.ArrayMap;
import android.util.Log;
import android.view.LayoutInflater;
import android.view.Menu;
@@ -34,21 +34,13 @@ import android.widget.ListView;
import android.widget.TextView;
import android.widget.Toast;
-import com.microsoft.connecteddevices.AsyncOperation;
import com.microsoft.connecteddevices.ConnectedDevicesAccount;
import com.microsoft.connecteddevices.ConnectedDevicesAccountType;
-import com.microsoft.connecteddevices.ConnectedDevicesNotificationRegistration;
-import com.microsoft.connecteddevices.EventListener;
-import com.microsoft.connecteddevices.signinhelpers.AADSigninHelperAccount;
-import com.microsoft.connecteddevices.signinhelpers.MSASigninHelperAccount;
-import com.microsoft.connecteddevices.signinhelpers.SigninHelperAccount;
import com.microsoft.connecteddevices.userdata.UserDataFeed;
-import com.microsoft.connecteddevices.userdata.UserDataFeedSyncScope;
import com.microsoft.connecteddevices.userdata.usernotifications.UserNotification;
import com.microsoft.connecteddevices.userdata.usernotifications.UserNotificationChannel;
import com.microsoft.connecteddevices.userdata.usernotifications.UserNotificationReadState;
import com.microsoft.connecteddevices.userdata.usernotifications.UserNotificationReader;
-import com.microsoft.connecteddevices.userdata.usernotifications.UserNotificationReaderOptions;
import com.microsoft.connecteddevices.userdata.usernotifications.UserNotificationStatus;
import com.microsoft.connecteddevices.userdata.usernotifications.UserNotificationUpdateResult;
import com.microsoft.connecteddevices.userdata.usernotifications.UserNotificationUserActionState;
@@ -59,8 +51,8 @@ import java.io.FileReader;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
+import java.util.EventObject;
import java.util.List;
-import java.util.Map;
import java.util.concurrent.CountDownLatch;
public class MainActivity extends AppCompatActivity {
@@ -82,150 +74,50 @@ public class MainActivity extends AppCompatActivity {
*/
private ViewPager mViewPager;
- private static SigninHelperAccount sMSAHelperAccount;
- private static SigninHelperAccount sAADHelperAccount;
- private static ConnectedDevicesAccount sLoggedInAccount;
- private static ConnectedDevicesNotificationRegistration sNotificationRegistration;
+ static ConnectedDevicesManager sConnectedDevicesManager;
+ static UserNotificationsManager sNotificationsManager;
+ static final ArrayList sActiveNotifications = new ArrayList<>();
- private static UserNotificationReader sReader;
private static CountDownLatch sLatch;
- private static final ArrayList sNotifications = new ArrayList<>();
-
- static final String CHANNEL_NAME = "GraphNotificationsChannel001";
- private static final String NOTIFICATION_ID = "ID";
-
- private enum LoginState {
- LOGGED_IN_MSA,
- LOGGED_IN_AAD,
- LOGGED_OUT
- }
-
- private static LoginState sState = LoginState.LOGGED_OUT;
-
- private static synchronized LoginState getAndUpdateLoginState()
- {
- if (sMSAHelperAccount == null || sAADHelperAccount == null || sLoggedInAccount == null) {
- sState = LoginState.LOGGED_OUT;
- } else if (sMSAHelperAccount.isSignedIn() && (sLoggedInAccount.getType() == ConnectedDevicesAccountType.MSA)) {
- sState = LoginState.LOGGED_IN_MSA;
- } else if (sAADHelperAccount.isSignedIn() && (sLoggedInAccount.getType() == ConnectedDevicesAccountType.AAD)) {
- sState = LoginState.LOGGED_IN_AAD;
- } else {
- sState = LoginState.LOGGED_OUT;
- }
-
- return sState;
- }
-
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
+ // Set up the tool bar
Toolbar toolbar = findViewById(R.id.toolbar);
setSupportActionBar(toolbar);
- // Create the adapter that will return a fragment for each of the three
- // primary sections of the activity.
- mSectionsPagerAdapter = new SectionsPagerAdapter(getSupportFragmentManager());
- // Set up the ViewPager with the sections adapter.
+ // Set up the tabs with adapter to manage the fragments
+ TabLayout tabLayout = findViewById(R.id.tabs);
+ mSectionsPagerAdapter = new SectionsPagerAdapter(getSupportFragmentManager());
mViewPager = findViewById(R.id.container);
mViewPager.setAdapter(mSectionsPagerAdapter);
-
- TabLayout tabLayout = findViewById(R.id.tabs);
-
mViewPager.addOnPageChangeListener(new TabLayout.TabLayoutOnPageChangeListener(tabLayout));
tabLayout.addOnTabSelectedListener(new TabLayout.ViewPagerOnTabSelectedListener(mViewPager));
- if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
- NotificationChannel channel = new NotificationChannel(CHANNEL_NAME, CHANNEL_NAME, NotificationManager.IMPORTANCE_HIGH);
- channel.setDescription("Graph Notifications Channel");
- getSystemService(NotificationManager.class).createNotificationChannel(channel);
- }
-
- if (sMSAHelperAccount == null) {
- final Map msaScopeOverrides = new ArrayMap<>();
- msaScopeOverrides.put("https://activity.windows.com/UserActivity.ReadWrite.CreatedByApp",
- new String[] { "https://activity.windows.com/UserActivity.ReadWrite.CreatedByApp",
- "https://activity.windows.com/Notifications.ReadWrite.CreatedByApp"});
- sMSAHelperAccount = new MSASigninHelperAccount(Secrets.MSA_CLIENT_ID, msaScopeOverrides, getApplicationContext());
- }
-
- if (sAADHelperAccount == null) {
- sAADHelperAccount = new AADSigninHelperAccount(Secrets.AAD_CLIENT_ID, Secrets.AAD_REDIRECT_URI, getApplicationContext());
- }
+ // Create the ConnectedDevicesManager
+ sConnectedDevicesManager = ConnectedDevicesManager.getConnectedDevicesManager(this);
sLatch = new CountDownLatch(1);
- if (PlatformManager.getInstance().getPlatform() == null) {
- PlatformManager.getInstance().createPlatform(getApplicationContext());
- }
-
- PlatformManager.getInstance().getPlatform().getNotificationRegistrationManager().notificationRegistrationStateChanged().subscribe((connectedDevicesNotificationRegistrationManager, connectedDevicesNotificationRegistrationStateChangedEventArgs) -> {
- Log.i(TAG, "NotificationRegistrationState changed to " + connectedDevicesNotificationRegistrationStateChangedEventArgs.getState().toString());
- });
-
- tryGetNotificationRegistration();
Intent intent = getIntent();
if (intent != null) {
- final String id = intent.getStringExtra(NOTIFICATION_ID);
- if (id != null && id.equals("")) {
+ final String id = intent.getStringExtra(UserNotificationsManager.NOTIFICATION_ID);
+ if (id != null && !id.isEmpty()) {
new Thread(() -> {
try {
sLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
-
- dismissNotification(id);
+ activateNotification(id);
}).start();
}
}
}
- static void tryGetNotificationRegistration() {
- if (sNotificationRegistration != null) {
- Log.i(TAG, "Already have notification registration");
- return;
- }
-
- RomeNotificationReceiver receiver = PlatformManager.getInstance().getNotificationReceiver();
- if (receiver != null) {
- receiver.getNotificationRegistrationAsync().whenComplete((connectedDevicesNotificationRegistration, throwable) -> {
- Log.i(TAG, "Got new notification registration");
- sNotificationRegistration = connectedDevicesNotificationRegistration;
- });
- } else {
- Log.i(TAG, "No notification receiver!");
- }
- }
-
- @Override
- protected void onNewIntent(Intent intent) {
- String id = intent.getStringExtra(NOTIFICATION_ID);
- dismissNotification(id);
- }
-
- private void dismissNotification(String id) {
- synchronized (sNotifications) {
- boolean found = false;
- for (UserNotification notification : sNotifications) {
- if (notification.getId().equals(id)) {
- notification.setUserActionState(UserNotificationUserActionState.ACTIVATED);
- notification.saveAsync();
- found = true;
- break;
- }
- }
-
- if (!found) {
- Log.w(TAG, "Attempted to dismiss missing notification!");
- }
- }
- }
-
-
@Override
public boolean onCreateOptionsMenu(Menu menu) {
// Inflate the menu; this adds items to the action bar if it is present.
@@ -240,133 +132,182 @@ public class MainActivity extends AppCompatActivity {
// as you specify a parent activity in AndroidManifest.xml.
int id = item.getItemId();
- //noinspection SimplifiableIfStatement
+ // Request a feed sync, all channels will get updated
if (id == R.id.action_refresh) {
- return true;
+ if (sNotificationsManager != null){
+ sNotificationsManager.refresh();
+ }
}
return super.onOptionsItemSelected(item);
}
+ @Override
+ protected void onNewIntent(Intent intent) {
+ String id = intent.getStringExtra(UserNotificationsManager.NOTIFICATION_ID);
+ activateNotification(id);
+ }
- private static class RunnableManager {
- private static Runnable sNotificationsUpdated;
+ private void activateNotification(String id) {
+ if (sNotificationsManager != null){
+ boolean found = false;
+ for (UserNotification notification : sActiveNotifications) {
+ if (notification.getId().equals(id)) {
+ sNotificationsManager.activate(notification);
+ found = true;
+ break;
+ }
+ }
+
+ if (!found) {
+ Log.w(TAG, "Attempted to dismiss notification!");
+ }
+ }
+ }
+
+ static class RunnableManager {
+ private static Runnable sNotificationsUpdated = null;
static void setNotificationsUpdated(Runnable runnable) {
sNotificationsUpdated = runnable;
}
- static Runnable getNotificationsUpdated() {
- return sNotificationsUpdated;
+ static void runNotificationsUpdated() {
+ if (sNotificationsUpdated != null) {
+ sNotificationsUpdated.run();
+ }
+ }
+ }
+
+ static void setupNotificationsManager(final Activity activity) {
+ if (sConnectedDevicesManager.getSignedInAccount() != null) {
+ Log.d(TAG, "Setup Notifications manager");
+ sNotificationsManager = sConnectedDevicesManager.getSignedInAccount().getNotificationsManager();
+ sNotificationsManager.addNotificationsUpdatedEventListener(args -> {
+ Log.d(TAG, "Notifications available!");
+
+ if (sNotificationsManager.HasNewNotifications()) {
+ activity.runOnUiThread(() -> {
+ Toast.makeText(activity, "Got new notifications", Toast.LENGTH_SHORT).show();
+ });
+ }
+
+ sActiveNotifications.clear();
+ sActiveNotifications.addAll(sNotificationsManager.HistoricalNotifications());
+
+ if (sLatch.getCount() == 1) {
+ sLatch.countDown();
+ }
+
+ RunnableManager.runNotificationsUpdated();
+ });
+ sNotificationsManager.refresh();
+ } else {
+ activity.runOnUiThread(() -> {
+ Toast.makeText(activity, "No signed-in account found!", Toast.LENGTH_SHORT).show();
+ });
}
}
public static class LoginFragment extends Fragment {
private Button mAadButton;
private Button mMsaButton;
- boolean firstCreate = true;
+
+ enum LoginState {
+ LOGGED_IN_MSA,
+ LOGGED_IN_AAD,
+ LOGGED_OUT
+ }
+
+ private LoginState mState = LoginState.LOGGED_OUT;
public LoginFragment() {
}
- public static LoginFragment newInstance() {
- return new LoginFragment();
- }
-
@Override
- public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container,
- Bundle savedInstanceState) {
- View rootView = inflater.inflate(R.layout.fragment_main, container, false);
+ public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
+ View rootView = inflater.inflate(R.layout.fragment_login, container, false);
+
mAadButton = rootView.findViewById(R.id.login_aad_button);
+ mAadButton.setOnClickListener(view -> {
+ if (getLoginState() != LoginState.LOGGED_IN_AAD) {
+ sConnectedDevicesManager.signInAadAsync(getActivity()).whenCompleteAsync((success, throwable) -> {
+ if ((throwable == null) && (success)) {
+ getActivity().runOnUiThread(() -> updateView(LoginState.LOGGED_IN_AAD));
+ setupNotificationsManager(getActivity());
+
+ } else {
+ Log.e(TAG, "AAD login failed!");
+ }
+ });
+ } else {
+ getActivity().runOnUiThread(() -> updateView(LoginState.LOGGED_OUT));
+ sConnectedDevicesManager.logout(getActivity());
+ sNotificationsManager = null;
+ sActiveNotifications.clear();
+ RunnableManager.runNotificationsUpdated();
+ }
+ });
+
mMsaButton = rootView.findViewById(R.id.login_msa_button);
- LoginState loginState = getAndUpdateLoginState();
- setState(loginState);
- if (firstCreate && (loginState != LoginState.LOGGED_OUT)) {
- firstCreate = false;
- MainActivity.setupChannel(getActivity());
+ mMsaButton.setOnClickListener(view -> {
+ if (getLoginState() != LoginState.LOGGED_IN_MSA) {
+ sConnectedDevicesManager.signInMsaAsync(getActivity()).whenCompleteAsync((success, throwable) -> {
+ if ((throwable == null) && (success)) {
+ getActivity().runOnUiThread(() -> updateView(LoginState.LOGGED_IN_MSA));
+ setupNotificationsManager(getActivity());
+ } else {
+ Log.e(TAG, "MSA login failed!");
+ }
+ });
+ } else {
+ getActivity().runOnUiThread(() -> updateView(LoginState.LOGGED_OUT));
+ sConnectedDevicesManager.logout(getActivity());
+ sNotificationsManager = null;
+ sActiveNotifications.clear();
+ RunnableManager.runNotificationsUpdated();
+ }
+ });
+
+ LoginState currentState = LoginState.LOGGED_OUT;
+ if (sConnectedDevicesManager.getSignedInAccount() != null) {
+ currentState = sConnectedDevicesManager.getSignedInAccount().getAccount().getType() == ConnectedDevicesAccountType.AAD ? LoginState.LOGGED_IN_AAD : LoginState.LOGGED_IN_MSA;
+ setupNotificationsManager(getActivity());
}
+ updateView(currentState);
return rootView;
}
- void setState(LoginState loginState) {
- switch (loginState) {
+ synchronized LoginState getLoginState()
+ {
+ return mState;
+ }
+
+ synchronized void updateLoginState(LoginState state)
+ {
+ mState = state;
+ }
+
+ void updateView(LoginState state) {
+ updateLoginState(state);
+
+ switch (state) {
case LOGGED_OUT:
mAadButton.setEnabled(true);
mAadButton.setText(R.string.login_aad);
- mAadButton.setOnClickListener(view -> sAADHelperAccount.signIn(getActivity()).whenCompleteAsync((connectedDevicesAccount, throwable) -> {
- if ((throwable == null) && (connectedDevicesAccount != null)) {
- sLoggedInAccount = connectedDevicesAccount;
- PlatformManager.getInstance().getPlatform().getAccountManager().accessTokenRequested().subscribe((accountManager, args)->
- {
- sAADHelperAccount.getAccessTokenAsync(args.getRequest().getScopes()).whenCompleteAsync((token, t) -> args.getRequest().completeWithAccessToken(token));
- });
-
- PlatformManager.getInstance().getPlatform().getAccountManager().accessTokenInvalidated().subscribe((connectedDevicesAccountManager, args) -> {
- // Don't need to do anything here for now
- });
-
- PlatformManager.getInstance().getPlatform().start();
-
- tryGetNotificationRegistration();
-
- PlatformManager.getInstance().getPlatform().getAccountManager().addAccountAsync(sLoggedInAccount).whenCompleteAsync((connectedDevicesAddAccountResult, throwable12) -> PlatformManager.getInstance().getPlatform().getNotificationRegistrationManager().registerForAccountAsync(sLoggedInAccount, sNotificationRegistration).whenCompleteAsync((aBoolean, throwable1) -> {
- getActivity().runOnUiThread(()-> setState(getAndUpdateLoginState()));
- MainActivity.setupChannel(getActivity());
- }));
-
- }
- }));
-
mMsaButton.setEnabled(true);
mMsaButton.setText(R.string.login_msa);
- mMsaButton.setOnClickListener(view -> sMSAHelperAccount.signIn(getActivity()).whenCompleteAsync((connectedDevicesAccount, throwable) -> {
- if (throwable == null && connectedDevicesAccount != null) {
- sLoggedInAccount = connectedDevicesAccount;
- PlatformManager.getInstance().getPlatform().getAccountManager().accessTokenRequested().subscribe((accountManager, args)-> {
- sMSAHelperAccount.getAccessTokenAsync(args.getRequest().getScopes()).whenCompleteAsync((token, t) -> {
- args.getRequest().completeWithAccessToken(token);
- });
- });
-
- PlatformManager.getInstance().getPlatform().getAccountManager().accessTokenInvalidated().subscribe((connectedDevicesAccountManager, args) -> {
- // Don't need to do anything here for now
- });
-
- PlatformManager.getInstance().getPlatform().start();
-
- tryGetNotificationRegistration();
-
- PlatformManager.getInstance().getPlatform().getAccountManager().addAccountAsync(sLoggedInAccount).whenCompleteAsync((connectedDevicesAddAccountResult, throwable12) -> {
- PlatformManager.getInstance().getPlatform().getNotificationRegistrationManager().registerForAccountAsync(sLoggedInAccount, sNotificationRegistration).whenCompleteAsync((aBoolean, throwable1) -> {
- getActivity().runOnUiThread(()-> setState(getAndUpdateLoginState()));
- MainActivity.setupChannel(getActivity());
- });
- });
- }
- }));
break;
case LOGGED_IN_AAD:
mAadButton.setText(R.string.logout);
- mAadButton.setOnClickListener(view -> PlatformManager.getInstance().getPlatform().getAccountManager().removeAccountAsync(sLoggedInAccount).whenCompleteAsync((connectedDevicesRemoveAccountResult, throwable) -> {
- sAADHelperAccount.signOut(getActivity()).whenCompleteAsync((connectedDevicesAccount, throwable13) -> {
- sLoggedInAccount = null;
- getActivity().runOnUiThread(()-> setState(getAndUpdateLoginState()));
- });
- }));
mMsaButton.setEnabled(false);
break;
case LOGGED_IN_MSA:
mAadButton.setEnabled(false);
mMsaButton.setText(R.string.logout);
- mMsaButton.setOnClickListener(view -> PlatformManager.getInstance().getPlatform().getAccountManager().removeAccountAsync(sLoggedInAccount).whenCompleteAsync((connectedDevicesRemoveAccountResult, throwable) -> {
- sMSAHelperAccount.signOut(getActivity()).whenCompleteAsync((connectedDevicesAccount, throwable14) -> {
- sLoggedInAccount = null;
- getActivity().runOnUiThread(()-> setState(getAndUpdateLoginState()));
- });
- }));
break;
}
}
@@ -374,51 +315,50 @@ public class MainActivity extends AppCompatActivity {
static class NotificationArrayAdapter extends ArrayAdapter {
private final Activity mActivity;
- NotificationArrayAdapter(Context context, List items, Activity activity) {
+
+ public NotificationArrayAdapter(Context context, List items, Activity activity) {
super(context, R.layout.notifications_list_item, items);
mActivity = activity;
}
@Override
public View getView(int position, View convertView, ViewGroup parent) {
- final UserNotification notification = sNotifications.get(position);
+ final UserNotification notification = sActiveNotifications.get(position);
if (convertView == null) {
convertView = LayoutInflater.from(getContext()).inflate(R.layout.notifications_list_item, parent, false);
}
- TextView idView = convertView.findViewById(R.id.notification_id);
+ final TextView idView = convertView.findViewById(R.id.notification_id);
idView.setText(notification.getId());
- TextView textView = convertView.findViewById(R.id.notification_text);
- String content = notification.getContent();
- textView.setText(content);
+ final TextView contentView = convertView.findViewById(R.id.notification_content);
+ contentView.setText(notification.getContent());
- TextView userActionStateView = convertView.findViewById(R.id.notification_useractionstate);
- userActionStateView.setText((notification.getUserActionState() == UserNotificationUserActionState.NO_INTERACTION)
- ? "NO_INTERACTION" : "ACTIVATED");
+ final TextView userActionStateView = convertView.findViewById(R.id.notification_useractionstate);
+ userActionStateView.setText(notification.getUserActionState().toString());
final Button readButton = convertView.findViewById(R.id.notification_read);
if (notification.getReadState() == UserNotificationReadState.UNREAD) {
+ idView.setTextColor(Color.GREEN);
readButton.setEnabled(true);
readButton.setOnClickListener(view -> {
readButton.setEnabled(false);
- notification.setReadState(UserNotificationReadState.READ);
- notification.saveAsync().whenCompleteAsync((userNotificationUpdateResult, throwable) -> {
- if (throwable == null && userNotificationUpdateResult != null && userNotificationUpdateResult.getSucceeded()) {
- Log.d(TAG, "Successfully marked notification as read");
- }
- });
+ sNotificationsManager.markRead(notification);
});
} else {
+ idView.setTextColor(Color.RED);
readButton.setEnabled(false);
}
+ final Button deleteButton = convertView.findViewById(R.id.notification_delete);
+ deleteButton.setOnClickListener(view -> {
+ sNotificationsManager.delete(notification);
+ });
+
if (notification.getUserActionState() == UserNotificationUserActionState.NO_INTERACTION) {
convertView.setOnClickListener(view -> {
- clearNotification(mActivity, notification.getId());
- notification.setUserActionState(UserNotificationUserActionState.ACTIVATED);
- notification.saveAsync();
+ sNotificationsManager.activate(notification);
});
} else {
convertView.setOnClickListener(null);
@@ -429,28 +369,20 @@ public class MainActivity extends AppCompatActivity {
}
public static class NotificationsFragment extends Fragment {
- NotificationArrayAdapter mNotificationArrayAdapter;
- public NotificationsFragment() {
- }
+ private NotificationArrayAdapter mNotificationArrayAdapter;
- /**
- * Returns a new instance of this fragment for the given section
- * number.
- */
- public static NotificationsFragment newInstance() {
- return new NotificationsFragment();
+ public NotificationsFragment() {
}
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
- mNotificationArrayAdapter = new NotificationArrayAdapter(getContext(), sNotifications, getActivity());
- RunnableManager.setNotificationsUpdated(() -> {
- if (getAndUpdateLoginState() != LoginState.LOGGED_OUT) {
- Toast.makeText(getContext(), "Got a new notification update!", Toast.LENGTH_SHORT).show();
- }
+ mNotificationArrayAdapter = new NotificationArrayAdapter(getContext(), sActiveNotifications, getActivity());
- mNotificationArrayAdapter.notifyDataSetChanged();
+ RunnableManager.setNotificationsUpdated(() -> {
+ getActivity().runOnUiThread(() -> {
+ mNotificationArrayAdapter.notifyDataSetChanged();
+ });
});
View rootView = inflater.inflate(R.layout.fragment_notifications, container, false);
@@ -459,6 +391,7 @@ public class MainActivity extends AppCompatActivity {
return rootView;
}
}
+
public static class LogFragment extends Fragment {
private View mRootView;
private TextView mTextView;
@@ -469,10 +402,6 @@ public class MainActivity extends AppCompatActivity {
public LogFragment() {}
- public static LogFragment newInstance() {
- return new LogFragment();
- }
-
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
Bundle savedInstanceState) {
@@ -543,12 +472,12 @@ public class MainActivity extends AppCompatActivity {
* A {@link FragmentPagerAdapter} that returns a fragment corresponding to
* one of the sections/tabs/pages.
*/
- private class SectionsPagerAdapter extends FragmentPagerAdapter {
- LoginFragment mLoginFragment;
- NotificationsFragment mNotificationFragment;
- LogFragment mLogFragment;
+ class SectionsPagerAdapter extends FragmentPagerAdapter {
+ private LoginFragment mLoginFragment;
+ private NotificationsFragment mNotificationFragment;
+ private LogFragment mLogFragment;
- SectionsPagerAdapter(FragmentManager fm) {
+ public SectionsPagerAdapter(FragmentManager fm) {
super(fm);
}
@@ -558,19 +487,19 @@ public class MainActivity extends AppCompatActivity {
{
case 0:
if (mLoginFragment == null) {
- mLoginFragment = LoginFragment.newInstance();
+ mLoginFragment = new LoginFragment();
}
return mLoginFragment;
case 1:
if (mNotificationFragment == null) {
- mNotificationFragment = NotificationsFragment.newInstance();
+ mNotificationFragment = new NotificationsFragment();
}
return mNotificationFragment;
case 2:
if (mLogFragment == null) {
- mLogFragment = LogFragment.newInstance();
+ mLogFragment = new LogFragment();
}
return mLogFragment;
@@ -585,84 +514,4 @@ public class MainActivity extends AppCompatActivity {
return 3;
}
}
- static void handleNotifications(final List userNotifications, final Activity activity) {
- activity.runOnUiThread(() -> {
- synchronized (sNotifications) {
- for (final UserNotification notification : userNotifications) {
- for (int i = 0; i < sNotifications.size(); i++) {
- if (sNotifications.get(i).getId().equals(notification.getId())) {
- sNotifications.remove(i);
- break;
- }
- }
-
- if (notification.getStatus() == UserNotificationStatus.ACTIVE) {
- sNotifications.add(0, notification);
-
- if (notification.getUserActionState() == UserNotificationUserActionState.NO_INTERACTION && notification.getReadState() == UserNotificationReadState.UNREAD) {
- addNotification(activity, notification.getContent(), notification.getId());
- } else {
- clearNotification(activity, notification.getId());
- }
- } else {
- clearNotification(activity, notification.getId());
- }
- }
-
- if (RunnableManager.getNotificationsUpdated() != null) {
- RunnableManager.getNotificationsUpdated().run();
- }
- }
- });
- }
-
- static void setupChannel(final Activity activity) {
- if (getAndUpdateLoginState() == LoginState.LOGGED_OUT) {
- return;
- }
-
- UserDataFeed dataFeed = UserDataFeed.getForAccount(sLoggedInAccount, PlatformManager.getInstance().getPlatform(), Secrets.APP_HOST_NAME);
- dataFeed.subscribeToSyncScopesAsync(Arrays.asList(UserNotificationChannel.getSyncScope())).whenCompleteAsync((success, throwable) -> {
- if (success) {
- dataFeed.startSync();
- UserNotificationChannel channel = new UserNotificationChannel(dataFeed);
- UserNotificationReaderOptions options = new UserNotificationReaderOptions();
- sReader = channel.createReaderWithOptions(options);
- sReader.readBatchAsync(Long.MAX_VALUE).thenAccept(userNotifications -> {
- handleNotifications(userNotifications, activity);
- if (sLatch.getCount() == 1) {
- sLatch.countDown();
- }
- });
-
- sReader.dataChanged().subscribe((userNotificationReader, aVoid) -> userNotificationReader.readBatchAsync(Long.MAX_VALUE).thenAccept(userNotifications -> {
- handleNotifications(userNotifications, activity);
- }));
- } else {
- activity.runOnUiThread(() -> Toast.makeText(activity.getApplicationContext(), "Failed to subscribe to sync scopes", Toast.LENGTH_SHORT));
- }
- });
- }
-
- static void addNotification(Activity activity, String message, String notificationId) {
- Intent intent = new Intent(activity, MainActivity.class);
- intent.putExtra(NOTIFICATION_ID, notificationId);
- intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
- PendingIntent pendingIntent = PendingIntent.getActivity(activity, 0, intent, PendingIntent.FLAG_ONE_SHOT);
-
- NotificationCompat.Builder builder = new NotificationCompat.Builder(activity, MainActivity.CHANNEL_NAME)
- .setSmallIcon(R.mipmap.ic_launcher_round)
- .setContentTitle("New MSGraph Notification!")
- .setContentText(message)
- .setPriority(NotificationCompat.PRIORITY_MAX)
- .setAutoCancel(true)
- .setContentIntent(pendingIntent);
-
- NotificationManagerCompat.from(activity).notify(notificationId.hashCode(), builder.build());
- }
-
- static void clearNotification(Activity activity, String notificationId) {
- ((NotificationManager)activity.getSystemService(NOTIFICATION_SERVICE)).cancel(notificationId.hashCode());
- }
-
}
diff --git a/Android/samples/graphnotificationssample/app/src/main/java/com/microsoft/connecteddevices/graphnotifications/PlatformManager.java b/Android/samples/graphnotificationssample/app/src/main/java/com/microsoft/connecteddevices/graphnotifications/PlatformManager.java
deleted file mode 100644
index 0991a4f..0000000
--- a/Android/samples/graphnotificationssample/app/src/main/java/com/microsoft/connecteddevices/graphnotifications/PlatformManager.java
+++ /dev/null
@@ -1,72 +0,0 @@
-//
-// Copyright (c) Microsoft Corporation. All rights reserved.
-//
-
-package com.microsoft.connecteddevices.graphnotifications;
-
-import android.content.Context;
-import android.util.Log;
-
-import com.microsoft.connecteddevices.ConnectedDevicesPlatform;
-
-final class PlatformManager {
- private RomeNotificationReceiver mNotificationReceiver;
- private ConnectedDevicesPlatform mPlatform;
- private static PlatformManager sInstance;
-
- private static final String TAG = PlatformManager.class.getName();
-
- public static synchronized PlatformManager getInstance()
- {
- if (sInstance == null) {
- sInstance = new PlatformManager();
- }
-
- return sInstance;
- }
-
- public synchronized ConnectedDevicesPlatform createPlatform(Context context) {
- if (mPlatform != null) {
- return mPlatform;
- }
-
- try {
- mPlatform = new ConnectedDevicesPlatform(context);
- } catch (Exception e) {
- return null;
- }
-
- // Subscribe to ConnectedDevicesNotificationRegistrationManager's event for when the registration state changes for a given account.
- mPlatform.getNotificationRegistrationManager().notificationRegistrationStateChanged().subscribe(
- (notificationRegistrationManager, args) -> {
- // TODO: Future - Identity-V3: give this to the signin helpers? Not exactly sure how to handle this...
- });
-
- return mPlatform;
- }
-
- public synchronized void startPlatform() {
- // Ensure we have created the Platform since there will not be an object to call start on otherwise.
- if (mPlatform == null) {
- return;
- }
-
- try {
- mPlatform.start();
- } catch (Exception e) {
- Log.e(TAG, "Failed to start platform with exception: " + e.getMessage());
- }
- }
-
- public synchronized ConnectedDevicesPlatform getPlatform() {
- return mPlatform;
- }
-
- public synchronized void createNotificationReceiver(Context context) {
- mNotificationReceiver = new RomeNotificationReceiver(context);
- }
-
- public synchronized RomeNotificationReceiver getNotificationReceiver() {
- return mNotificationReceiver;
- }
-}
diff --git a/Android/samples/graphnotificationssample/app/src/main/java/com/microsoft/connecteddevices/graphnotifications/RomeNotificationReceiver.java b/Android/samples/graphnotificationssample/app/src/main/java/com/microsoft/connecteddevices/graphnotifications/RomeNotificationReceiver.java
index 75de64d..8414bff 100644
--- a/Android/samples/graphnotificationssample/app/src/main/java/com/microsoft/connecteddevices/graphnotifications/RomeNotificationReceiver.java
+++ b/Android/samples/graphnotificationssample/app/src/main/java/com/microsoft/connecteddevices/graphnotifications/RomeNotificationReceiver.java
@@ -13,59 +13,37 @@ import android.util.Log;
import com.microsoft.connecteddevices.AsyncOperation;
import com.microsoft.connecteddevices.ConnectedDevicesNotificationRegistration;
-import com.microsoft.connecteddevices.ConnectedDevicesNotificationType;
-import com.microsoft.connecteddevices.EventListener;
-
-import java.util.HashMap;
-import java.util.Map;
public class RomeNotificationReceiver extends BroadcastReceiver {
private static final String TAG = RomeNotificationReceiver.class.getName();
- private Map> mListenerMap;
- private Long mNextListenerId = 0L;
- private ConnectedDevicesNotificationRegistration mNotificationRegistration;
- private AsyncOperation mAsync;
- private Context mContext;
private static final String RegistrationComplete = "registrationComplete";
- RomeNotificationReceiver(Context context) {
- mListenerMap = new HashMap<>();
- mContext = context;
+ private static AsyncOperation sNotificationRegistrationOperation;
+ private Context mContext;
+
+ RomeNotificationReceiver(Context context) {
+ mContext = context;
registerFCMBroadcastReceiver();
}
- /**
- * This function returns Notification Registration after it completes async operation.
- * @return Notification Registration.
- */
- public synchronized AsyncOperation getNotificationRegistrationAsync() {
- if (mAsync == null) {
- mAsync = new AsyncOperation<>();
+ public static synchronized void setNotificationRegistration(ConnectedDevicesNotificationRegistration registration) {
+ // Create the registration operation if it has not been requested already
+ if (sNotificationRegistrationOperation == null) {
+ sNotificationRegistrationOperation = new AsyncOperation<>();
}
- if (mNotificationRegistration != null) {
- mAsync.complete(mNotificationRegistration);
- }
- return mAsync;
+
+ // Complete the operation with the registration, to be fetched later
+ sNotificationRegistrationOperation.complete(registration);
}
- /**
- * This function adds new event listener to notification provider.
- * @param listener the EventListener.
- * @return id next event listener id.
- */
- public synchronized long addNotificationProviderChangedListener(
- EventListener listener) {
- mListenerMap.put(mNextListenerId, listener);
- return mNextListenerId++;
- }
+ public static synchronized AsyncOperation getNotificationRegistrationAsync() {
+ // Create the registration operation if it the registration has not been received yet
+ if (sNotificationRegistrationOperation == null) {
+ sNotificationRegistrationOperation = new AsyncOperation<>();
+ }
- /**
- * This function removes the event listener.
- * @param id the id corresponds to the event listener that would be removed.
- */
- public synchronized void removeNotificationProviderChangedListener(long id) {
- mListenerMap.remove(id);
+ return sNotificationRegistrationOperation;
}
/**
@@ -75,47 +53,30 @@ public class RomeNotificationReceiver extends BroadcastReceiver {
*/
@Override
public void onReceive(Context context, Intent intent) {
- String token = null;
String action = intent.getAction();
+ Log.i(TAG, "Broadcast received: " + action);
- Log.i("Receiver", "Broadcast received: " + action);
-
+ String token = null;
if (action.equals(RegistrationComplete)) {
token = intent.getExtras().getString("TOKEN");
}
- if (token == null) {
- Log.e("GraphNotifications",
- "Got notification that FCM had been registered, but token is null. Was app ID set in FCMRegistrationIntentService?");
+ if (token == null || token.isEmpty()) {
+ Log.e(TAG, "RomeNotificationReceiver gained a token however it is null/empty, check FCMRegistrationIntentService");
+ } else {
+ Log.i(TAG, "RomeNotificationReceiver gained a token: " + token);
+ ConnectedDevicesManager.getConnectedDevicesManager(context).setNotificationRegistration(token);
}
- synchronized (this) {
- mNotificationRegistration = new ConnectedDevicesNotificationRegistration();
- mNotificationRegistration.setType(ConnectedDevicesNotificationType.FCM);
- mNotificationRegistration.setToken(token);
- mNotificationRegistration.setAppId(Secrets.FCM_SENDER_ID);
- mNotificationRegistration.setAppDisplayName("OneRomanApp");
-
- if (mAsync == null) {
- mAsync = new AsyncOperation<>();
- }
- mAsync.complete(mNotificationRegistration);
- mAsync = new AsyncOperation<>();
-
- for (EventListener event : mListenerMap.values()) {
- event.onEvent(this, mNotificationRegistration);
- }
-
- Log.e(TAG, "Successfully completed FCM registration");
- }
+ mContext.startService(new Intent(mContext, FCMListenerService.class));
}
+
/**
* This function is called to start FCM registration service.
* Start FCMRegistrationIntentService to register with FCM.
*/
- private void startService() {
- Log.e(TAG, "Starting FCMListenerService");
- Intent registrationIntentService = new Intent(mContext, FCMListenerService.class);
+ private void startFCMRegistrationIntentService() {
+ Intent registrationIntentService = new Intent(mContext, FCMRegistrationIntentService.class);
mContext.startService(registrationIntentService);
}
@@ -124,6 +85,6 @@ public class RomeNotificationReceiver extends BroadcastReceiver {
*/
private void registerFCMBroadcastReceiver() {
LocalBroadcastManager.getInstance(mContext).registerReceiver(this, new IntentFilter(RegistrationComplete));
- startService();
+ startFCMRegistrationIntentService();
}
}
diff --git a/Android/samples/graphnotificationssample/app/src/main/java/com/microsoft/connecteddevices/graphnotifications/UserNotificationsManager.java b/Android/samples/graphnotificationssample/app/src/main/java/com/microsoft/connecteddevices/graphnotifications/UserNotificationsManager.java
new file mode 100644
index 0000000..bac54ac
--- /dev/null
+++ b/Android/samples/graphnotificationssample/app/src/main/java/com/microsoft/connecteddevices/graphnotifications/UserNotificationsManager.java
@@ -0,0 +1,227 @@
+package com.microsoft.connecteddevices.graphnotifications;
+
+import android.app.Activity;
+import android.app.NotificationChannel;
+import android.app.NotificationManager;
+import android.app.PendingIntent;
+import android.content.Context;
+import android.content.Intent;
+import android.support.annotation.NonNull;
+import android.support.v4.app.NotificationCompat;
+import android.support.v4.app.NotificationManagerCompat;
+import android.util.Log;
+
+import com.microsoft.connecteddevices.AsyncOperation;
+import com.microsoft.connecteddevices.ConnectedDevicesAccount;
+import com.microsoft.connecteddevices.ConnectedDevicesPlatform;
+import com.microsoft.connecteddevices.userdata.UserDataFeed;
+import com.microsoft.connecteddevices.userdata.usernotifications.UserNotification;
+import com.microsoft.connecteddevices.userdata.usernotifications.UserNotificationChannel;
+import com.microsoft.connecteddevices.userdata.usernotifications.UserNotificationReadState;
+import com.microsoft.connecteddevices.userdata.usernotifications.UserNotificationReader;
+import com.microsoft.connecteddevices.userdata.usernotifications.UserNotificationStatus;
+import com.microsoft.connecteddevices.userdata.usernotifications.UserNotificationUserActionState;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.EventObject;
+import java.util.List;
+
+import static android.content.Context.NOTIFICATION_SERVICE;
+
+public class UserNotificationsManager {
+ private static final String TAG = UserNotificationsManager.class.getName();
+
+ public static final String CHANNEL_NAME = "GraphNotificationsChannel001";
+ public static final String NOTIFICATION_ID = "ID";
+
+ public interface NotificationsUpdatedEventListener {
+ void onEvent(EventObject args);
+ }
+
+ private ArrayList mListeners = new ArrayList<>();
+
+ private Context mContext;
+ private UserDataFeed mFeed;
+ private UserNotificationChannel mChannel;
+ private UserNotificationReader mReader;
+
+ private final ArrayList mHistoricalNotifications = new ArrayList<>();
+ private final ArrayList mNewNotifications = new ArrayList<>();
+
+ public UserNotificationsManager(@NonNull Context context, @NonNull ConnectedDevicesAccount account, @NonNull ConnectedDevicesPlatform platform)
+ {
+ mContext = context;
+ mFeed = UserDataFeed.getForAccount(account, platform, Secrets.APP_HOST_NAME);
+ mChannel = new UserNotificationChannel(mFeed);
+ mReader = mChannel.createReader();
+ mReader.dataChanged().subscribe((reader, aVoid) -> readFromCache(reader));
+
+ if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.O) {
+ NotificationChannel channel = new NotificationChannel(CHANNEL_NAME, CHANNEL_NAME, NotificationManager.IMPORTANCE_HIGH);
+ channel.setDescription("GraphNotificationsSample Channel");
+ ((NotificationManager)context.getSystemService(NOTIFICATION_SERVICE)).createNotificationChannel(channel);
+ }
+ }
+
+ public AsyncOperation registerForAccountAsync()
+ {
+ return mFeed.subscribeToSyncScopesAsync(Arrays.asList(UserNotificationChannel.getSyncScope())).thenComposeAsync((success) -> {
+ mFeed.startSync();
+ readFromCache(mReader);
+ return AsyncOperation.completedFuture(success);
+ });
+ }
+
+ public synchronized void addNotificationsUpdatedEventListener(NotificationsUpdatedEventListener listener) {
+ mListeners.add(listener);
+ }
+
+ public synchronized void removeNotificationsUpdatedEventListener(NotificationsUpdatedEventListener listener) {
+ mListeners.remove(listener);
+ }
+
+ public List HistoricalNotifications() {
+ return mHistoricalNotifications;
+ }
+
+ public boolean HasNewNotifications() {
+ return !mNewNotifications.isEmpty();
+ }
+
+ public void refresh()
+ {
+ mFeed.startSync();
+ readFromCache(mReader);
+ }
+
+ public void activate(UserNotification notification)
+ {
+ notification.setUserActionState(UserNotificationUserActionState.ACTIVATED);
+ notification.saveAsync().whenCompleteAsync((userNotificationUpdateResult, throwable) -> {
+ if (throwable == null && userNotificationUpdateResult != null && userNotificationUpdateResult.getSucceeded()) {
+ Log.d(TAG, "Successfully activated the notification");
+ }
+ });
+ clearNotification(mContext.getApplicationContext(), notification.getId());
+ }
+
+ public void dismiss(UserNotification notification)
+ {
+ notification.setUserActionState(UserNotificationUserActionState.DISMISSED);
+ notification.saveAsync().whenCompleteAsync((userNotificationUpdateResult, throwable) -> {
+ if (throwable == null && userNotificationUpdateResult != null && userNotificationUpdateResult.getSucceeded()) {
+ Log.d(TAG, "Successfully dismissed the notification");
+ }
+ });
+ clearNotification(mContext.getApplicationContext(), notification.getId());
+ }
+
+ public void markRead(UserNotification notification)
+ {
+ notification.setReadState(UserNotificationReadState.READ);
+ notification.saveAsync().whenCompleteAsync((userNotificationUpdateResult, throwable) -> {
+ if (throwable == null && userNotificationUpdateResult != null && userNotificationUpdateResult.getSucceeded()) {
+ Log.d(TAG, "Successfully marked the notification as read");
+ }
+ });
+ }
+
+ public void delete(UserNotification notification)
+ {
+ mChannel.deleteUserNotificationAsync(notification.getId()).whenCompleteAsync((userNotificationUpdateResult, throwable) -> {
+ if (throwable == null && userNotificationUpdateResult != null && userNotificationUpdateResult.getSucceeded()) {
+ Log.d(TAG, "Successfully deleted the notification");
+ }
+ });
+ }
+
+ private void NotifyNotificationsUpdated() {
+ Log.d(TAG, "Notifying listeners");
+ List listeners = new ArrayList<>();
+ synchronized (this) {
+ listeners.addAll(mListeners);
+ }
+ for (NotificationsUpdatedEventListener listener : listeners) {
+ listener.onEvent(new EventObject(this));
+ }
+ }
+
+ /**
+ * Replacement for the java.util.function.Predicate to support pre Java 8 / API 24.
+ */
+ interface Predicate {
+ public boolean test(T t);
+ }
+
+ /**
+ * Replacement for list.removeIf to support pre Java 8 / API 24.
+ * @param list List to search
+ * @param predicate Predicate to use against the given list
+ * @return True if removed item matching the given predicate, false if none found
+ */
+ private static boolean removeIf(List list, Predicate super T> predicate) {
+ for (T item : list) {
+ if (predicate.test(item)) {
+ list.remove(item);
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ private void readFromCache(final UserNotificationReader reader)
+ {
+ Log.d(TAG, "Read notifications from cache");
+ reader.readBatchAsync(Long.MAX_VALUE).thenAccept(notifications -> {
+ synchronized (this) {
+ for (final UserNotification notification : notifications) {
+ if (notification.getStatus() == UserNotificationStatus.ACTIVE) {
+ removeIf(mNewNotifications, item -> notification.getId().equals(item.getId()));
+
+ if (notification.getUserActionState() == UserNotificationUserActionState.NO_INTERACTION) {
+ mNewNotifications.add(notification);
+ if (notification.getReadState() != UserNotificationReadState.READ) {
+ clearNotification(mContext.getApplicationContext(), notification.getId());
+ addNotification(mContext.getApplicationContext(), notification.getContent(), notification.getId());
+ }
+ } else {
+ clearNotification(mContext.getApplicationContext(), notification.getId());
+ }
+
+ removeIf(mHistoricalNotifications, item -> notification.getId().equals(item.getId()));
+ mHistoricalNotifications.add(0, notification);
+ } else {
+ removeIf(mNewNotifications, item -> notification.getId().equals(item.getId()));
+ removeIf(mHistoricalNotifications, item -> notification.getId().equals(item.getId()));
+ clearNotification(mContext.getApplicationContext(), notification.getId());
+ }
+ }
+ }
+
+ NotifyNotificationsUpdated();
+ });
+ }
+
+ static void addNotification(Context ctx, String message, String notificationId) {
+ Intent intent = new Intent(ctx, MainActivity.class);
+ intent.putExtra(NOTIFICATION_ID, notificationId);
+ intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP);
+ PendingIntent pendingIntent = PendingIntent.getActivity(ctx, 0, intent, PendingIntent.FLAG_ONE_SHOT);
+
+ NotificationCompat.Builder builder = new NotificationCompat.Builder(ctx, CHANNEL_NAME)
+ .setSmallIcon(R.mipmap.ic_launcher_round)
+ .setContentTitle("New UserNotification!")
+ .setContentText(message)
+ .setPriority(NotificationCompat.PRIORITY_MAX)
+ .setAutoCancel(true)
+ .setContentIntent(pendingIntent);
+
+ NotificationManagerCompat.from(ctx).notify(notificationId.hashCode(), builder.build());
+ }
+
+ static void clearNotification(Context ctx, String notificationId) {
+ ((NotificationManager)ctx.getSystemService(NOTIFICATION_SERVICE)).cancel(notificationId.hashCode());
+ }
+}
diff --git a/Android/samples/graphnotificationssample/app/src/main/res/layout/fragment_log.xml b/Android/samples/graphnotificationssample/app/src/main/res/layout/fragment_log.xml
index 416e1fd..1cc7eb6 100644
--- a/Android/samples/graphnotificationssample/app/src/main/res/layout/fragment_log.xml
+++ b/Android/samples/graphnotificationssample/app/src/main/res/layout/fragment_log.xml
@@ -1,23 +1,16 @@
-
-
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical">
-
-
-
-
-
+ android:text="" />
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/Android/samples/graphnotificationssample/app/src/main/res/layout/fragment_login.xml b/Android/samples/graphnotificationssample/app/src/main/res/layout/fragment_login.xml
new file mode 100644
index 0000000..ae02863
--- /dev/null
+++ b/Android/samples/graphnotificationssample/app/src/main/res/layout/fragment_login.xml
@@ -0,0 +1,23 @@
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/Android/samples/graphnotificationssample/app/src/main/res/layout/fragment_main.xml b/Android/samples/graphnotificationssample/app/src/main/res/layout/fragment_main.xml
deleted file mode 100644
index a325c16..0000000
--- a/Android/samples/graphnotificationssample/app/src/main/res/layout/fragment_main.xml
+++ /dev/null
@@ -1,33 +0,0 @@
-
-
-
-
-
-
\ No newline at end of file
diff --git a/Android/samples/graphnotificationssample/app/src/main/res/layout/fragment_notifications.xml b/Android/samples/graphnotificationssample/app/src/main/res/layout/fragment_notifications.xml
index fe3664b..5000cad 100644
--- a/Android/samples/graphnotificationssample/app/src/main/res/layout/fragment_notifications.xml
+++ b/Android/samples/graphnotificationssample/app/src/main/res/layout/fragment_notifications.xml
@@ -1,8 +1,9 @@
+ android:id="@+id/fragment_notifications"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:orientation="vertical">
-
+ android:orientation="vertical">
-
+ android:orientation="horizontal">
+
+
+
-
\ No newline at end of file
+
\ No newline at end of file
diff --git a/Android/samples/graphnotificationssample/app/src/main/res/values/strings.xml b/Android/samples/graphnotificationssample/app/src/main/res/values/strings.xml
index 4324d18..644541e 100644
--- a/Android/samples/graphnotificationssample/app/src/main/res/values/strings.xml
+++ b/Android/samples/graphnotificationssample/app/src/main/res/values/strings.xml
@@ -8,5 +8,6 @@
Login with AAD
Login with MSA
Log Out
- Read
+ Mark Read
+ Delete