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 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 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 @@ + + +