diff --git a/Android/samples/ProGuard_Rules_for_Android_Rome_SDK.txt b/Android/samples/ProGuard_Rules_for_Android_Rome_SDK.txt deleted file mode 100644 index 111b53d..0000000 --- a/Android/samples/ProGuard_Rules_for_Android_Rome_SDK.txt +++ /dev/null @@ -1,78 +0,0 @@ -# To enable ProGuard in your project, edit project.properties -# to define the proguard.config property as described in that file. -# -# Add project specific ProGuard rules here. -# By default, the flags in this file are appended to flags specified -# in ${sdk.dir}/tools/proguard/proguard-android.txt -# You can edit the include path and order by changing the ProGuard -# include property in project.properties. -# -# For more details, see -# http://developer.android.com/guide/developing/tools/proguard.html - -# Add any project specific keep options here: - -# If your project uses WebView with JS, uncomment the following -# and specify the fully qualified class name to the JavaScript interface -# class: -#-keepclassmembers class fqcn.of.javascript.interface.for.webview { -# public *; -#} - --keepnames class * implements android.os.Parcelable --keepclassmembers class * implements android.os.Parcelable { -public static final *** CREATOR; -} - --keep @interface android.support.annotation.Keep --keep @android.support.annotation.Keep class * --keepclasseswithmembers class * { -@android.support.annotation.Keep ; -} --keepclasseswithmembers class * { -@android.support.annotation.Keep ; -} - --keep class com.microsoft.cdp.internal.** { -*; -} - --keepclassmembers class com.microsoft.connecteddevices.RemoteSystemWatcher { -void onDeviceAdded(long, java.lang.String, java.lang.String, int, int, int[], java.lang.String); -void onDeviceUpdated(long, java.lang.String, java.lang.String, int, int, int[], java.lang.String); -void onDeviceRemoved(java.lang.String); -void onDiscoveryError(int); -void onDiscoveryComplete(); -} - --keepclassmembers class com.microsoft.connecteddevices.DeviceInternal { -void onConnecting(); -void onConnected(); -void onDisconnecting(); -void onDisconnected(); -void onConnectError(java.lang.String); -} - --keepclassmembers class com.microsoft.connecteddevices.AppControlClient { -void onComplete(long); -void onError(long, int); -void onTimeout(long); -} - --keepclassmembers class com.microsoft.connecteddevices.BinaryClientInternal { -void onData(byte[]); -} - --keepclassmembers class * implements com.microsoft.connecteddevices.IWebAccountProvider { -java.lang.String getToken(java.lang.String); -java.lang.String getStableUserId(); -java.lang.String getDeviceId(); -} - --keepclasseswithmembernames,includedescriptorclasses class * { -native ; -} - --keep class com.microsoft.connecteddevices.RomeException { -*; -} diff --git a/Android/samples/account-provider-sample/build.gradle b/Android/samples/account-provider-sample/build.gradle index 69a2aec..e769c7f 100644 --- a/Android/samples/account-provider-sample/build.gradle +++ b/Android/samples/account-provider-sample/build.gradle @@ -1,10 +1,12 @@ apply plugin: 'com.android.library' android { - compileSdkVersion 25 + compileSdkVersion 27 + buildToolsVersion '27.0.3' + defaultConfig { minSdkVersion 19 - targetSdkVersion 25 + targetSdkVersion 27 versionCode 1 versionName "1.0" } @@ -20,7 +22,7 @@ android { } dependencies { - implementation 'com.android.support:appcompat-v7:25.3.1' + implementation 'com.android.support:support-annotations:27.1.1' implementation('com.microsoft.aad:adal:1.13.1') { exclude group: 'com.android.support' } diff --git a/Android/samples/account-provider-sample/src/AndroidManifest.xml b/Android/samples/account-provider-sample/src/AndroidManifest.xml deleted file mode 100644 index ce5728f..0000000 --- a/Android/samples/account-provider-sample/src/AndroidManifest.xml +++ /dev/null @@ -1,2 +0,0 @@ - - diff --git a/Android/samples/account-provider-sample/src/com/microsoft/connecteddevices/sampleaccountproviders/AADAccountProvider.java b/Android/samples/account-provider-sample/src/com/microsoft/connecteddevices/sampleaccountproviders/AADAccountProvider.java deleted file mode 100644 index 3688227..0000000 --- a/Android/samples/account-provider-sample/src/com/microsoft/connecteddevices/sampleaccountproviders/AADAccountProvider.java +++ /dev/null @@ -1,288 +0,0 @@ -// -// Copyright (c) Microsoft Corporation. All rights reserved. -// - -package com.microsoft.connecteddevices.sampleaccountproviders; - -import android.app.Activity; -import android.content.Context; -import android.content.Intent; -import android.os.Handler; -import android.os.Looper; -import android.support.annotation.Keep; -import android.support.annotation.Nullable; -import android.text.TextUtils; -import android.util.ArrayMap; -import android.util.Log; -import android.webkit.CookieManager; -import android.webkit.CookieSyncManager; -import android.webkit.ValueCallback; - -import com.microsoft.aad.adal.ADALError; -import com.microsoft.aad.adal.AuthenticationCallback; -import com.microsoft.aad.adal.AuthenticationContext; -import com.microsoft.aad.adal.AuthenticationException; -import com.microsoft.aad.adal.AuthenticationResult; -import com.microsoft.aad.adal.AuthenticationResult.AuthenticationStatus; -import com.microsoft.aad.adal.PromptBehavior; -import com.microsoft.aad.adal.TokenCacheItem; - -import com.microsoft.connecteddevices.core.AccessTokenRequestStatus; -import com.microsoft.connecteddevices.core.AccessTokenResult; -import com.microsoft.connecteddevices.base.AsyncOperation; -import com.microsoft.connecteddevices.base.EventListener; -import com.microsoft.connecteddevices.core.UserAccountProvider; -import com.microsoft.connecteddevices.core.UserAccount; -import com.microsoft.connecteddevices.core.UserAccountType; - -import java.lang.InterruptedException; -import java.util.Iterator; -import java.util.Map; - -/** - * Sign in helper that provides an UserAccountProvider implementation for AAD using the ADAL library. - * To use this class, call signIn()/signOut(), then use the standard UserAccountProvider functions. - * - * Notes about AAD/ADAL: - * - Resource An Azure web service/app, such as https://graph.windows.net, or a CDP service. - * - Scope Individual permissions within a resource - * - Access Token A standard JSON web token for a given scope. - * This is the actual token/user ticket used to authenticate with CDP services. - * https://oauth.net/2/ - * https://www.oauth.com/oauth2-servers/access-tokens/ - * - Refresh token: A standard OAuth refresh token. - * Lasts longer than access tokens, and is used to request new access tokens/refresh access tokens when they expire. - * ADAL manages this automatically. - * https://oauth.net/2/grant-types/refresh-token/ - * - MRRT Multiresource refresh token. A refresh token that can be used to fetch access tokens for more than one resource. - * Getting one requires the user consent to all the covered resources. ADAL manages this automatically. - */ -@Keep -public final class AADAccountProvider implements UserAccountProvider { - private static final String TAG = AADAccountProvider.class.getName(); - - private final String mClientId; - private final String mRedirectUri; - private final AuthenticationContext mAuthContext; - - private UserAccount mAccount; // Initialized when signed in - - private final Map> mListenerMap = new ArrayMap<>(); - private long mNextListenerId = 1L; - - /** - * @param clientId id of the app's registration in the Azure portal - * @param redirectUri redirect uri the app is registered with in the Azure portal - * @param context - */ - public AADAccountProvider(String clientId, String redirectUri, Context context) { - mClientId = clientId; - mRedirectUri = redirectUri; - - if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.LOLLIPOP) { - CookieSyncManager.createInstance(context); - } - - mAuthContext = new AuthenticationContext(context, "https://login.microsoftonline.com/common", false); - - Log.i(TAG, "Checking if previous AADAccountProvider session can be loaded..."); - Iterator tokenCacheItems = mAuthContext.getCache().getAll(); - while (tokenCacheItems.hasNext()) { - TokenCacheItem item = tokenCacheItems.next(); - if (item.getIsMultiResourceRefreshToken() && item.getClientId().equals(mClientId)) { - mAccount = new UserAccount(item.getUserInfo().getUserId(), UserAccountType.AAD); - break; - } - } - - if (mAccount != null) { - Log.i(TAG, "Loaded previous AADAccountProvider session, starting as signed in."); - } else { - Log.i(TAG, "No previous AADAccountProvider session could be loaded, starting as signed out."); - } - } - - private AsyncOperation notifyListenersAsync() { - return AsyncOperation.supplyAsync(new AsyncOperation.Supplier() { - @Override - public Void get() { - for (EventListener listener : mListenerMap.values()) { - listener.onEvent(AADAccountProvider.this, null); - } - return null; - } - }); - } - - public String getClientId() { - return mClientId; - } - - public synchronized boolean isSignedIn() { - return mAccount != null; - } - - public synchronized AsyncOperation signIn() throws IllegalStateException { - if (isSignedIn()) { - throw new IllegalStateException("AADAccountProvider: Already signed in!"); - } - - final AsyncOperation ret = new AsyncOperation<>(); - - // If the user has not previously consented for this default resource for this app, - // the interactive flow will ask for user consent for all resources used by the app. - // If the user previously consented to this resource on this app, and more resources are added to the app later on, - // a consent prompt for all app resources will be raised when an access token for a new resource is requested - - // see getAccessTokenForUserAccountAsync() - final String defaultResource = "https://graph.windows.net"; - - mAuthContext.acquireToken( // - defaultResource, // resource - mClientId, // clientId - mRedirectUri, // redirectUri - null, // loginHint - PromptBehavior.Auto, // promptBehavior - null, // extraQueryParameters - new AuthenticationCallback() { - @Override - public void onError(Exception e) { - Log.e(TAG, "acquireToken encountered an exception: " + e.toString() + ". This may be transient."); - ret.complete(false); - } - - @Override - public void onSuccess(AuthenticationResult result) { - if (result == null || result.getStatus() != AuthenticationStatus.Succeeded || result.getUserInfo() == null) { - ret.complete(false); - } else { - mAccount = new UserAccount(result.getUserInfo().getUserId(), UserAccountType.AAD); - ret.complete(true); - notifyListenersAsync(); - } - } - }); - - return ret; - } - - public synchronized void signOut() throws IllegalStateException { - if (!isSignedIn()) { - throw new IllegalStateException("AADAccountProvider: Not currently signed in!"); - } - - // Delete cookies - final CookieManager cookieManager = CookieManager.getInstance(); - - if (android.os.Build.VERSION.SDK_INT < android.os.Build.VERSION_CODES.LOLLIPOP) { - cookieManager.removeAllCookie(); - CookieSyncManager.getInstance().sync(); - } else { - cookieManager.removeAllCookies(new ValueCallback() { - @Override - public void onReceiveValue(Boolean value) { - cookieManager.flush(); - } - }); - } - - mAccount = null; - mAuthContext.getCache().removeAll(); - notifyListenersAsync(); - } - - @Override - public synchronized UserAccount[] getUserAccounts() { - if (mAccount != null) { - return new UserAccount[] { mAccount }; - } - - return new UserAccount[0]; - } - - @Override - public synchronized AsyncOperation getAccessTokenForUserAccountAsync( - final String userAccountId, final String[] scopes) { - if (mAccount == null || !mAccount.getId().equals(userAccountId)) { - return AsyncOperation.completedFuture(new AccessTokenResult(AccessTokenRequestStatus.TRANSIENT_ERROR, null)); - } - - final AsyncOperation ret = new AsyncOperation<>(); - mAuthContext.acquireTokenSilentAsync(scopes[0], mClientId, mAccount.getId(), new AuthenticationCallback() { - @Override - public void onError(Exception e) { - if ((e instanceof AuthenticationException) && - ((AuthenticationException)e).getCode() == ADALError.AUTH_REFRESH_FAILED_PROMPT_NOT_ALLOWED) { - // This error only returns from acquireTokenSilentAsync when an interactive prompt is needed. - // ADAL has an MRRT, but the user has not consented for this resource/the MRRT does not cover this resource. - // Usually, users consent for all resources the app needs during the interactive flow in signIn(). - // However, if the app adds new resources after the user consented previously, signIn() will not prompt. - // Escalate to the UI thread and do an interactive flow, - // which should raise a new consent prompt for all current app resources. - Log.i(TAG, "A resource was requested that the user did not previously consent to. " - + "Attempting to raise an interactive consent prompt."); - - final AuthenticationCallback reusedCallback = this; // reuse this callback - new Handler(Looper.getMainLooper()) - .post(new Runnable() { - @Override - public void run() { - mAuthContext.acquireToken( - scopes[0], mClientId, mRedirectUri, null, PromptBehavior.Auto, null, reusedCallback); - } - }); - return; - } - - Log.e(TAG, "getAccessTokenForUserAccountAsync hit an exception: " + e.toString() + ". This may be transient."); - ret.complete(new AccessTokenResult(AccessTokenRequestStatus.TRANSIENT_ERROR, null)); - } - - @Override - public void onSuccess(AuthenticationResult result) { - if (result == null || result.getStatus() != AuthenticationStatus.Succeeded || TextUtils.isEmpty(result.getAccessToken())) { - - ret.complete(new AccessTokenResult(AccessTokenRequestStatus.TRANSIENT_ERROR, null)); - } else { - ret.complete(new AccessTokenResult(AccessTokenRequestStatus.SUCCESS, result.getAccessToken())); - } - } - }); - - return ret; - } - - @Override - public synchronized long addUserAccountChangedListener(EventListener listener) { - long id = mNextListenerId++; - mListenerMap.put(id, listener); - return id; - } - - @Override - public synchronized void removeUserAccountChangedListener(long id) { - mListenerMap.remove(id); - } - - @Override - public synchronized void onAccessTokenError(String userAccountId, String[] scopes, boolean isPermanentError) { - if (mAccount != null && mAccount.getId().equals(userAccountId)) { - if (isPermanentError) { - try { - signOut(); - } catch (IllegalStateException e) { - // Already signed out in between checking if signed in and now. No need to do anything. - Log.e(TAG, "Already signed out in onAccessTokenError. This error is most likely benign: " + e.toString()); - } - } else { - // If not a permanent error, try to refresh the tokens - try { - mAuthContext.acquireTokenSilentSync(scopes[0], mClientId, userAccountId); - } catch (AuthenticationException e) { - Log.e(TAG, "Exception in ADAL when trying to refresh token: \'" + e.toString() + "\'"); - } catch (InterruptedException e) { Log.e(TAG, "Interrupted while trying to refresh token: \'" + e.toString() + "\'"); } - } - } else { - Log.e(TAG, "onAccessTokenError was called, but AADAccountProvider was not signed in."); - } - } -} \ No newline at end of file diff --git a/Android/samples/account-provider-sample/src/com/microsoft/connecteddevices/sampleaccountproviders/AADMSAAccountProvider.java b/Android/samples/account-provider-sample/src/com/microsoft/connecteddevices/sampleaccountproviders/AADMSAAccountProvider.java deleted file mode 100644 index 90cd354..0000000 --- a/Android/samples/account-provider-sample/src/com/microsoft/connecteddevices/sampleaccountproviders/AADMSAAccountProvider.java +++ /dev/null @@ -1,171 +0,0 @@ -// -// Copyright (c) Microsoft Corporation. All rights reserved. -// - -package com.microsoft.connecteddevices.sampleaccountproviders; - -import android.app.Activity; -import android.content.Context; -import android.content.Intent; -import android.support.annotation.Keep; -import android.util.Log; - -import com.microsoft.connecteddevices.base.AsyncOperation; -import com.microsoft.connecteddevices.base.EventListener; -import com.microsoft.connecteddevices.core.AccessTokenResult; -import com.microsoft.connecteddevices.core.UserAccountProvider; -import com.microsoft.connecteddevices.core.UserAccount; - -import java.util.Hashtable; -import java.util.Map; - -/** - * Sign in helper that provides an UserAccountProvider implementation that works with both AAD and MSA accounts. - * - * To use this class, call signInMSA()/signOutMSA()/signInAAD()/signOutAAD(), - * then access the UserAccountProvider through getUserAccountProvider(). - */ -@Keep -public final class AADMSAAccountProvider implements UserAccountProvider { - private static final String TAG = AADMSAAccountProvider.class.getName(); - - public enum State { - SignedOut, - SignedInMSA, - SignedInAAD, - } - - private MSAAccountProvider mMSAProvider; - private AADAccountProvider mAADProvider; - private EventListener mListener; - - private final Map> mListenerMap = new Hashtable<>(); - private long mNextListenerId = 1L; - - /** - * @param msaClientId id of the app's registration in the MSA portal - * @param aadClientId id of the app's registration in the Azure portal - * @param redirectUri redirect uri the app is registered with in the Azure portal - * @param context - */ - public AADMSAAccountProvider(String msaClientId, String aadClientId, String aadRedirectUri, Context context) { - - // Chain the inner events to the event provided by this helper - mListener = new EventListener() { - @Override - public void onEvent(UserAccountProvider provider, Void aVoid) { - notifyListenersAsync(); - } - }; - - mMSAProvider = new MSAAccountProvider(msaClientId, context); - mAADProvider = new AADAccountProvider(aadClientId, aadRedirectUri, context); - - if (mMSAProvider.isSignedIn() && mAADProvider.isSignedIn()) { - // Shouldn't ever happen, but if it does, sign AAD out - mAADProvider.signOut(); - } - - mMSAProvider.addUserAccountChangedListener(mListener); - mAADProvider.addUserAccountChangedListener(mListener); - } - - private AsyncOperation notifyListenersAsync() { - return AsyncOperation.supplyAsync(new AsyncOperation.Supplier() { - @Override - public Void get() { - for (EventListener listener : mListenerMap.values()) { - listener.onEvent(AADMSAAccountProvider.this, null); - } - return null; - } - }); - } - - public synchronized State getSignInState() { - if (mMSAProvider != null && mMSAProvider.isSignedIn()) { - return State.SignedInMSA; - } - if (mAADProvider != null && mAADProvider.isSignedIn()) { - return State.SignedInAAD; - } - return State.SignedOut; - } - - public AsyncOperation signInMSA(final Activity currentActivity) throws IllegalStateException { - if (getSignInState() != State.SignedOut) { - throw new IllegalStateException("Already signed into an account!"); - } - return mMSAProvider.signIn(currentActivity); - } - - public void signOutMSA(final Activity currentActivity) throws IllegalStateException { - if (getSignInState() != State.SignedInMSA) { - throw new IllegalStateException("Not currently signed into an MSA!"); - } - mMSAProvider.signOut(currentActivity); - } - - public AsyncOperation signInAAD() throws IllegalStateException { - if (getSignInState() != State.SignedOut) { - throw new IllegalStateException("Already signed into an account!"); - } - return mAADProvider.signIn(); - } - - public void signOutAAD() throws IllegalStateException { - if (getSignInState() != State.SignedInAAD) { - throw new IllegalStateException("Not currently signed into an AAD account!"); - } - mAADProvider.signOut(); - } - - private UserAccountProvider getSignedInProvider() { - switch (getSignInState()) { - case SignedInMSA: return mMSAProvider; - case SignedInAAD: return mAADProvider; - default: return null; - } - } - - @Override - public synchronized UserAccount[] getUserAccounts() { - UserAccountProvider provider = AADMSAAccountProvider.this.getSignedInProvider(); - return (provider != null) ? provider.getUserAccounts() : new UserAccount[0]; - } - - @Override - public synchronized AsyncOperation getAccessTokenForUserAccountAsync( - final String userAccountId, final String[] scopes) { - UserAccountProvider provider = AADMSAAccountProvider.this.getSignedInProvider(); - if (provider != null) { - return provider.getAccessTokenForUserAccountAsync(userAccountId, scopes); - } - - AsyncOperation ret = new AsyncOperation(); - ret.completeExceptionally(new IllegalStateException("Not currently signed in!")); - return ret; - } - - @Override - public synchronized long addUserAccountChangedListener(EventListener listener) { - long id = mNextListenerId++; - mListenerMap.put(id, listener); - return id; - } - - @Override - public synchronized void removeUserAccountChangedListener(long id) { - mListenerMap.remove(id); - } - - @Override - public synchronized void onAccessTokenError(String userAccountId, String[] scopes, boolean isPermanentError) { - UserAccountProvider provider = AADMSAAccountProvider.this.getSignedInProvider(); - if (provider != null) { - provider.onAccessTokenError(userAccountId, scopes, isPermanentError); - } else { - Log.e(TAG, "Not currently signed in!"); - } - } -} \ No newline at end of file diff --git a/Android/samples/account-provider-sample/src/com/microsoft/connecteddevices/sampleaccountproviders/IOUtil.java b/Android/samples/account-provider-sample/src/com/microsoft/connecteddevices/sampleaccountproviders/IOUtil.java deleted file mode 100644 index 31d6109..0000000 --- a/Android/samples/account-provider-sample/src/com/microsoft/connecteddevices/sampleaccountproviders/IOUtil.java +++ /dev/null @@ -1,53 +0,0 @@ -// -// Copyright (c) Microsoft Corporation. All rights reserved. -// - -package com.microsoft.connecteddevices.sampleaccountproviders; - -import android.support.annotation.Keep; - -import java.io.BufferedReader; -import java.io.BufferedWriter; -import java.io.InputStream; -import java.io.OutputStream; -import java.io.IOException; -import java.io.InputStreamReader; -import java.io.OutputStreamWriter; - -@Keep -final class IOUtil { - - /** - * Writes UTF-8 output data to an output stream. - * This method is synchronous, and should only be used on small data sizes. - * - * @param stream Stream to write data to - * @param data Data to write - * @throws IOException Thrown if the output stream is unavailable, or encoding the data fails - */ - static void writeUTF8Stream(OutputStream stream, String data) throws IOException { - try (BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(stream, "UTF-8"))) { - writer.write(data); - } - } - - /** - * Reads the contents of a UTF-8 input stream. - * This method is synchronous, and should only be used on small data sizes. - * - * @param stream Input stream to read from - * @return All data received from the stream - * @throws IOException Thrown if the input stream is unavailable, or decoding the data fails - */ - static String readUTF8Stream(InputStream stream) throws IOException { - StringBuilder stringBuilder = new StringBuilder(); - try (BufferedReader reader = new BufferedReader(new InputStreamReader(stream, "UTF-8"))) { - String line; - while ((line = reader.readLine()) != null) { - stringBuilder.append(line); - } - } - - return stringBuilder.toString(); - } -} \ No newline at end of file diff --git a/Android/samples/account-provider-sample/src/com/microsoft/connecteddevices/sampleaccountproviders/MSAAccountProvider.java b/Android/samples/account-provider-sample/src/com/microsoft/connecteddevices/sampleaccountproviders/MSAAccountProvider.java deleted file mode 100644 index aad8288..0000000 --- a/Android/samples/account-provider-sample/src/com/microsoft/connecteddevices/sampleaccountproviders/MSAAccountProvider.java +++ /dev/null @@ -1,449 +0,0 @@ -// -// Copyright (c) Microsoft Corporation. All rights reserved. -// - -package com.microsoft.connecteddevices.sampleaccountproviders; - -import android.app.Activity; -import android.app.Dialog; -import android.content.Context; -import android.net.Uri; -import android.support.annotation.Keep; -import android.support.annotation.Nullable; -import android.text.TextUtils; -import android.util.ArrayMap; -import android.util.Log; -import android.util.Pair; -import android.webkit.WebChromeClient; -import android.webkit.WebResourceError; -import android.webkit.WebResourceRequest; -import android.webkit.WebView; -import android.webkit.WebViewClient; - -import com.microsoft.connecteddevices.core.AccessTokenRequestStatus; -import com.microsoft.connecteddevices.core.AccessTokenResult; -import com.microsoft.connecteddevices.base.AsyncOperation; -import com.microsoft.connecteddevices.base.EventListener; -import com.microsoft.connecteddevices.core.UserAccountProvider; -import com.microsoft.connecteddevices.core.UserAccount; -import com.microsoft.connecteddevices.core.UserAccountType; - -import org.json.JSONException; -import org.json.JSONObject; - -import java.io.BufferedReader; -import java.io.BufferedWriter; -import java.io.IOException; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.io.OutputStreamWriter; -import java.io.UnsupportedEncodingException; -import java.net.HttpURLConnection; -import java.net.URL; -import java.net.URLEncoder; -import java.util.LinkedList; -import java.util.List; -import java.util.Map; -import java.util.UUID; -import java.util.concurrent.Executors; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.TimeUnit; - -import javax.net.ssl.HttpsURLConnection; - -/** - * Sample implementation of UserAccountProvider. - * Exposes a single MSA account, that the user logs into via WebView, to CDP. - * Follows OAuth2.0 protocol, but automatically refreshes tokens when they are close to expiring. - * - * Terms: - * - Scope: OAuth feature, limits what a token actually gives permissions to. - * https://www.oauth.com/oauth2-servers/scope/ - * - Access token: A standard JSON web token for a given scope. - * This is the actual token/user ticket used to authenticate with CDP services. - * https://oauth.net/2/ - * https://www.oauth.com/oauth2-servers/access-tokens/ - * - Refresh token: A standard OAuth refresh token. - * Lasts longer than access tokens, and is used to request new access tokens/refresh access tokens when they expire. - * This library caches one refresh token per user. - * As such, the refresh token must already be authorized/consented to for all CDP scopes that will be used in the app. - * https://oauth.net/2/grant-types/refresh-token/ - * - Grant type: Type of OAuth authorization request to make (ie: token, password, auth code) - * https://oauth.net/2/grant-types/ - * - Auth code: OAuth auth code, can be exchanged for a token. - * This library has the user sign in interactively for the auth code grant type, - * then retrieves the auth code from the return URL. - * https://oauth.net/2/grant-types/authorization-code/ - * - Client ID: ID of an app's registration in the MSA portal. As of the time of writing, the portal uses GUIDs. - * - * The flow of this library is described below: - * Signing in - * 1. signIn() is called (now treated as signing in) - * 2. webview is presented to the user for sign in - * 3. Use authcode returned from user's sign in to fetch refresh token - * 4. Refresh token is cached - if the user does not sign out, but the app is restarted, - * the user will not need to enter their credentials/consent again when signIn() is called. - * 4. Now treated as signed in. Account is exposed to CDP. UserAccountChangedEvent is fired. - * - * While signed in - * CDP asks for access tokens - * 1. Check if access token is in cache - * 2. If not in cache, request a new access token using the cached refresh token. - * 3. If in cache but close to expiry, the access token is refreshed using the refresh token. - * The refreshed access token is returned. - * 4. If in cache and not close to expiry, just return it. - * - * Signing out - * 1. signOut() is called - * 2. webview is quickly popped up to go through the sign out URL - * 3. Cache is cleared. - * 4. Now treated as signed out. Account is no longer exposed to CDP. UserAccountChangedEvent is fired. - */ -@Keep -public final class MSAAccountProvider implements UserAccountProvider, MSATokenCache.Listener { - - // region Constants - private static final String TAG = MSAAccountProvider.class.getName(); - - // CDP's SDK currently requires authorization for all features, otherwise platform initialization will fail. - // As such, the user must sign in/consent for the following scopes. This may change to become more modular in the future. - private static final String[] REQUIRED_SCOPES = { - "ccs.ReadWrite", // device commanding scope - "dds.read", // device discovery scope (discover other devices) - "dds.register", // device discovery scope (allow discovering this device) - "wns.connect", // notification scope - "wl.offline_access", // read and update user info at any time - "https://activity.windows.com/UserActivity.ReadWrite.CreatedByApp", // user activities scope - "asimovrome.telemetry" // asimov token scope - }; - - // OAuth URLs - private static final String REDIRECT_URL = "https://login.live.com/oauth20_desktop.srf"; - private static final String AUTHORIZE_URL = "https://login.live.com/oauth20_authorize.srf"; - private static final String LOGOUT_URL = "https://login.live.com/oauth20_logout.srf"; - // endregion - - // region Member Variables - private final String mClientId; - private UserAccount mAccount = null; - private MSATokenCache mTokenCache; - private boolean mSignInSignOutInProgress; - - private final Map> mListenerMap = new ArrayMap<>(); - private long mNextListenerId = 1L; - // endregion - - // region Constructor - /** - * @param clientId id of the app's registration in the MSA portal - * @param context - */ - public MSAAccountProvider(String clientId, Context context) { - mClientId = clientId; - mTokenCache = new MSATokenCache(clientId, context); - mTokenCache.addListener(new MSATokenCache.Listener() { - @Override - public void onTokenCachePermanentFailure() { - onAccessTokenError((mAccount != null ? mAccount.getId() : null), null, true); - } - }); - - if (mTokenCache.loadSavedRefreshToken()) { - Log.i(TAG, "Loaded previous session for MSAAccountProvider. Starting as signed in."); - mAccount = new UserAccount(UUID.randomUUID().toString(), UserAccountType.MSA); - } else { - Log.i(TAG, "No previous session could be loaded for MSAAccountProvider. Starting as signed out."); - } - } - // endregion - - // region Private Helpers - private AsyncOperation notifyListenersAsync() { - return AsyncOperation.supplyAsync(new AsyncOperation.Supplier() { - @Override - public Void get() { - for (EventListener listener : mListenerMap.values()) { - listener.onEvent(MSAAccountProvider.this, null); - } - return null; - } - }); - } - - private synchronized void addAccount() { - Log.i(TAG, "Adding an account."); - mAccount = new UserAccount(UUID.randomUUID().toString(), UserAccountType.MSA); - notifyListenersAsync(); - } - - private synchronized void removeAccount() { - if (isSignedIn()) { - Log.i(TAG, "Removing account."); - mAccount = null; - mTokenCache.clearTokens(); - notifyListenersAsync(); - } - } - - /** - * Asynchronously requests a new access token for the provided scope(s) and caches it. - * This assumes that the sign in helper is currently signed in. - */ - private AsyncOperation requestNewAccessTokenAsync(final String scope) { - // Need the refresh token first, then can use it to request an access token - return mTokenCache.getRefreshTokenAsync() - .thenComposeAsync(new AsyncOperation.ResultFunction>() { - @Override - public AsyncOperation apply(String refreshToken) { - return MSATokenRequest.requestAsync(mClientId, MSATokenRequest.GrantType.REFRESH, scope, null, refreshToken); - } - }) - .thenApplyAsync(new AsyncOperation.ResultFunction() { - @Override - public AccessTokenResult apply(MSATokenRequest.Result result) throws Throwable { - switch (result.getStatus()) { - case SUCCESS: - Log.i(TAG, "Successfully fetched access token."); - mTokenCache.setAccessToken(result.getAccessToken(), scope, result.getExpiresIn()); - return new AccessTokenResult(AccessTokenRequestStatus.SUCCESS, result.getAccessToken()); - - case TRANSIENT_FAILURE: - Log.e(TAG, "Requesting new access token failed temporarily, please try again."); - return new AccessTokenResult(AccessTokenRequestStatus.TRANSIENT_ERROR, null); - - default: // PERMANENT_FAILURE - Log.e(TAG, "Permanent error occurred while fetching access token."); - onAccessTokenError(mAccount.getId(), new String[] { scope }, true); - throw new IOException("Permanent error occurred while fetching access token."); - } - } - }); - } - // endregion - - public String getClientId() { - return mClientId; - } - - // region Interactive Sign-in/out - public synchronized boolean isSignedIn() { - return mAccount != null; - } - - /** - * Pops up a webview for the user to sign in with their MSA, then uses the auth code returned to cache a refresh token for the user. - * If a refresh token was already cached from a previous session, it will be used instead, and no webview will be displayed. - */ - public synchronized AsyncOperation signIn(final Activity currentActivity) throws IllegalStateException { - if (isSignedIn() || mSignInSignOutInProgress) { - throw new IllegalStateException(); - } - - final String signInUrl = AUTHORIZE_URL + "?redirect_uri=" + REDIRECT_URL + "&response_type=code&client_id=" + mClientId + - "&scope=" + TextUtils.join("+", REQUIRED_SCOPES); - final AsyncOperation authCodeOperation = new AsyncOperation<>(); - final AsyncOperation signInOperation = new AsyncOperation<>(); - mSignInSignOutInProgress = true; - - final Dialog dialog = new Dialog(currentActivity); - dialog.setContentView(R.layout.auth_dialog); - final WebView web = (WebView)dialog.findViewById(R.id.webv); - web.setWebChromeClient(new WebChromeClient()); - web.getSettings().setJavaScriptEnabled(true); - web.getSettings().setDomStorageEnabled(true); - - web.loadUrl(signInUrl); - web.setWebViewClient(new WebViewClient() { - @Override - public void onPageFinished(WebView view, String url) { - super.onPageFinished(view, url); - - if (url.startsWith(REDIRECT_URL)) { - final Uri uri = Uri.parse(url); - final String code = uri.getQueryParameter("code"); - final String error = uri.getQueryParameter("error"); - - dialog.dismiss(); - - if ((error != null) || (code == null) || (code.length() <= 0)) { - synchronized (MSAAccountProvider.this) { - mSignInSignOutInProgress = false; - } - - signInOperation.complete(false); - authCodeOperation.completeExceptionally( - new Exception((error != null) ? error : "Failed to authenticate with unknown error")); - } else { - authCodeOperation.complete(code); - } - } - } - - @Override - public void onReceivedError(WebView view, WebResourceRequest request, WebResourceError error) { - super.onReceivedError(view, request, error); - - Log.e(TAG, "Encountered web resource loading error while signing in: \'" + error.getDescription() + "\'"); - synchronized (MSAAccountProvider.this) { - mSignInSignOutInProgress = false; - } - - signInOperation.complete(false); - authCodeOperation.completeExceptionally(new Exception(error.getDescription().toString())); - } - }); - - authCodeOperation // chain after successfully fetching the authcode (does not execute if authCodeOperation completed exceptionally) - .thenComposeAsync(new AsyncOperation.ResultFunction>() { - @Override - public AsyncOperation apply(String authCode) { - return MSATokenRequest.requestAsync(mClientId, MSATokenRequest.GrantType.CODE, null, REDIRECT_URL, authCode); - } - }) - .thenAcceptAsync(new AsyncOperation.ResultConsumer() { - @Override - public void accept(MSATokenRequest.Result result) { - synchronized (MSAAccountProvider.this) { - mSignInSignOutInProgress = false; - } - - if (result.getStatus() == MSATokenRequest.Result.Status.SUCCESS) { - if (result.getRefreshToken() == null) { - Log.e(TAG, "Unexpected: refresh token is null despite succeeding in refresh."); - signInOperation.complete(false); - } - - Log.i(TAG, "Successfully fetched refresh token."); - mTokenCache.setRefreshToken(result.getRefreshToken()); - addAccount(); - signInOperation.complete(true); - - } else { - Log.e(TAG, "Failed to fetch refresh token using auth code."); - signInOperation.complete(false); - } - } - }); - - dialog.show(); - dialog.setCancelable(true); - - return signInOperation; - } - - /** - * Signs the user out by going through the webview, then clears the cache and current state. - */ - public synchronized void signOut(final Activity currentActivity) throws IllegalStateException { - final String signOutUrl = LOGOUT_URL + "?client_id=" + mClientId + "&redirect_uri=" + REDIRECT_URL; - mSignInSignOutInProgress = true; - - final Dialog dialog = new Dialog(currentActivity); - dialog.setContentView(R.layout.auth_dialog); - WebView web = (WebView)dialog.findViewById(R.id.webv); - - web.loadUrl(signOutUrl); - web.setWebViewClient(new WebViewClient() { - @Override - public void onPageFinished(WebView view, String url) { - super.onPageFinished(view, url); - - if (!url.contains("oauth20_desktop.srf")) { - // finishing off loading intermediate pages, - // e.g., input username/password page, consent interrupt page, wrong username/password page etc. - // no need to handle them, return early. - return; - } - - synchronized (MSAAccountProvider.this) { - mSignInSignOutInProgress = false; - } - - final Uri uri = Uri.parse(url); - final String error = uri.getQueryParameter("error"); - if (error != null) { - Log.e(TAG, "Signed out failed with error: " + error); - } - - removeAccount(); - dialog.dismiss(); - } - - @Override - public void onReceivedError(WebView view, WebResourceRequest request, WebResourceError error) { - super.onReceivedError(view, request, error); - - Log.e(TAG, "Encountered web resource loading error while signing out: \'" + error.getDescription() + "\'"); - synchronized (MSAAccountProvider.this) { - mSignInSignOutInProgress = false; - } - } - }); - } - // endregion - - // region UserAccountProvider Overrides - @Override - public synchronized UserAccount[] getUserAccounts() { - if (mAccount != null) { - return new UserAccount[] { mAccount }; - } - - return new UserAccount[0]; - } - - @Override - public synchronized AsyncOperation getAccessTokenForUserAccountAsync(final String accountId, final String[] scopes) { - if (mAccount != null && accountId != null && accountId.equals(mAccount.getId()) && scopes.length > 0) { - - final String scope = TextUtils.join(" ", scopes); - - return mTokenCache.getAccessTokenAsync(scope).thenComposeAsync( - new AsyncOperation.ResultFunction>() { - @Override - public AsyncOperation apply(String accessToken) { - if (accessToken != null) { - // token already exists in the cache, can early return - return AsyncOperation.completedFuture(new AccessTokenResult(AccessTokenRequestStatus.SUCCESS, accessToken)); - - } else { - // token does not yet exist in the cache, need to request a new one - return requestNewAccessTokenAsync(scope); - } - } - }); - } - - // No access token is available - return AsyncOperation.completedFuture(new AccessTokenResult(AccessTokenRequestStatus.TRANSIENT_ERROR, null)); - } - - @Override - public synchronized long addUserAccountChangedListener(EventListener listener) { - long id = mNextListenerId++; - mListenerMap.put(id, listener); - return id; - } - - @Override - public synchronized void removeUserAccountChangedListener(long id) { - mListenerMap.remove(id); - } - - @Override - public synchronized void onAccessTokenError(String accountId, String[] scopes, boolean isPermanentError) { - if (isPermanentError) { - removeAccount(); - } else { - mTokenCache.markAllTokensExpired(); - } - } - // endregion - - // region MSATokenCacheListener Overrides - @Override - public void onTokenCachePermanentFailure() { - onAccessTokenError(null, null, true); - } - // endregion -} diff --git a/Android/samples/account-provider-sample/src/com/microsoft/connecteddevices/sampleaccountproviders/MSATokenCache.java b/Android/samples/account-provider-sample/src/com/microsoft/connecteddevices/sampleaccountproviders/MSATokenCache.java deleted file mode 100644 index 2017af9..0000000 --- a/Android/samples/account-provider-sample/src/com/microsoft/connecteddevices/sampleaccountproviders/MSATokenCache.java +++ /dev/null @@ -1,486 +0,0 @@ -// -// Copyright (c) Microsoft Corporation. All rights reserved. -// - -package com.microsoft.connecteddevices.sampleaccountproviders; - -import android.content.Context; -import android.support.annotation.Keep; -import android.util.ArrayMap; -import android.util.Log; - -import com.microsoft.connecteddevices.base.AsyncOperation; - -import java.io.BufferedReader; -import java.io.BufferedWriter; -import java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStreamReader; -import java.io.OutputStreamWriter; -import java.text.DateFormat; -import java.text.ParseException; -import java.util.ArrayList; -import java.util.Calendar; -import java.util.Collection; -import java.util.Date; -import java.util.Map; -import java.util.Set; -import java.util.concurrent.Executors; -import java.util.concurrent.ScheduledExecutorService; -import java.util.concurrent.TimeUnit; - -import org.json.JSONException; -import org.json.JSONObject; - -/** - * Caches MSA access and refresh tokens, automatically refreshing them as needed when fetching from the cache. - * Cached refresh tokens are persisted across sessions. - */ -@Keep -final class MSATokenCache { - private static final String TAG = MSATokenCache.class.getName(); - - // Max number of times to try to refresh a token through transient failures - private static final int TOKEN_REFRESH_MAX_RETRIES = 3; - - // How quickly to retry refreshing a token when encountering a transient failure - private static final long MSA_REFRESH_TOKEN_RETRY_SECONDS = 30 * 60; // 30 minutes - private static final long MSA_ACCESS_TOKEN_RETRY_SECONDS = 3 * 60; // 3 minutes - - // How long it takes a refresh token to expire - private static final int MSA_REFRESH_TOKEN_EXPIRATION_SECONDS = 10 * 24 * 60 * 60; // 10 days - - // How long before expiry to consider a token in need of a refresh. - // (MSA_REFRESH_TOKEN_CLOSE_TO_EXPIRY_SECONDS is intended to be aggressive and keep the refresh token relatively far from expiry) - private static final int MSA_REFRESH_TOKEN_CLOSE_TO_EXPIRY_SECONDS = 7 * 24 * 60 * 60; // 7 days - private static final int MSA_ACCESS_TOKEN_CLOSE_TO_EXPIRY_SECONDS = 5 * 60; // 5 minutes - - private static final String MSA_OFFLINE_ACCESS_SCOPE = "wl.offline_access"; - - private static final ScheduledExecutorService sRetryExecutor = Executors.newSingleThreadScheduledExecutor(); - - /** - * Helper function. Returns a Date n seconds from now. - */ - private static final Date getDateSecondsAfterNow(int seconds) { - return getDateSecondsAfter(null, seconds); - } - - /** - * Helper function. Returns a Date n seconds after date. - */ - private static final Date getDateSecondsAfter(Date date, int seconds) { - Calendar calendar = Calendar.getInstance(); // sets time to current - if (date != null) { - calendar.setTime(date); - } - calendar.add(Calendar.SECOND, seconds); - return calendar.getTime(); - } - - /** - * Private helper class wrapping a cached access token. Responsible for refreshing it on-demand. - */ - private class MSATokenCacheItem { - protected String mToken; - protected Date mCloseToExpirationDate; // Actual expiration date is used less than this, so just cache this instead - protected final MSATokenRequest mRefreshRequest; - - public MSATokenCacheItem(String token, int expiresInSeconds, MSATokenRequest refreshRequest) { - mToken = token; - mCloseToExpirationDate = getDateSecondsAfterNow(expiresInSeconds - getCloseToExpirySeconds()); - mRefreshRequest = refreshRequest; - } - - /** - * Returns the number of seconds before expiry that this token is considered in need of a refresh. - */ - protected int getCloseToExpirySeconds() { - return MSA_ACCESS_TOKEN_CLOSE_TO_EXPIRY_SECONDS; // Base class expects access tokens - } - - /** - * Returns the number of seconds to wait before retrying, when a refresh fails with a transient error. - */ - protected long getRetrySeconds() { - return MSA_ACCESS_TOKEN_RETRY_SECONDS; // Base class expects access tokens - } - - /** - * Returns the refresh token to use to refresh the token held by this item. - * For access tokens, this gets the refresh token held by the cache. - * For refresh tokens, this just returns the currently-held token. - */ - protected AsyncOperation getRefreshTokenAsync() { - return MSATokenCache.this.getRefreshTokenAsync(); - } - - /** - * Steps to complete after a successful refresh. - * For access tokens, sets the new token and new expiration. - * For refresh tokens, marks current access tokens as expired, and caches the refresh token in persistent storage. - */ - protected synchronized void onSuccessfulRefresh(MSATokenRequest.Result result) { - Log.i(TAG, "Successfully refreshed access token."); - mToken = result.getAccessToken(); - mCloseToExpirationDate = getDateSecondsAfterNow(result.getExpiresIn() - getCloseToExpirySeconds()); - } - - /** - * Private helper - asynchronously fetches the token held by this item. - * If the token is close to expiry, refreshes it first. - * If this refresh fails due to transient error, recursively retries up to remainingRetries times to refresh. - * - * @param operation AsyncOperation to return the token on - * @param remainingRetries number of times to retry refreshing, in the case of transient error - * @return the operation that was passed in - */ - private AsyncOperation _getTokenAsyncInternal(final AsyncOperation operation, final int remainingRetries) { - if (!needsRefresh()) { - operation.complete(mToken); // Already have a non-stale token, can just return with it - return operation; - } - - getRefreshTokenAsync() - .thenComposeAsync(new AsyncOperation.ResultFunction>() { - @Override - public AsyncOperation apply(String refreshToken) { - return mRefreshRequest.requestAsync(refreshToken); - } - }) - .thenAcceptAsync(new AsyncOperation.ResultConsumer() { - @Override - public void accept(MSATokenRequest.Result result) { - switch (result.getStatus()) { - case SUCCESS: - onSuccessfulRefresh(result); - operation.complete(mToken); - break; - - case TRANSIENT_FAILURE: - // Recursively retry the refresh, if there are still remaining retries - if (remainingRetries <= 0) { - Log.e(TAG, "Reached max number of retries for refreshing token."); - operation.complete(null); - - } else { - Log.i(TAG, "Transient error while refreshing token, retrying in " + getRetrySeconds() + "seconds..."); - sRetryExecutor.schedule(new Runnable() { - @Override - public void run() { - _getTokenAsyncInternal(operation, remainingRetries - 1); - } - }, getRetrySeconds(), TimeUnit.SECONDS); - } - break; - - default: // PERMANENT_FAILURE - Log.e(TAG, "Permanent error occurred while refreshing token."); - MSATokenCache.this.onPermanentFailure(); - operation.complete(null); - break; - } - } - }); - - return operation; - } - - /** - * Asynchronously fetches the token held by this item, refreshing it if necessary. - */ - public AsyncOperation getTokenAsync() { - AsyncOperation ret = new AsyncOperation(); - return _getTokenAsyncInternal(ret, TOKEN_REFRESH_MAX_RETRIES); - } - - public boolean needsRefresh() { - return mCloseToExpirationDate.before(new Date()); - } - - public boolean isExpired() { - return getDateSecondsAfter(mCloseToExpirationDate, getCloseToExpirySeconds()).before(new Date()); - } - - public synchronized void markExpired() { - mCloseToExpirationDate = new Date(0); // Start of epoch - } - } - - /** - * Private helper class wrapping a cached refresh token. Responsible for refreshing it on demand. Can translate to/from json format. - */ - private final class MSARefreshTokenCacheItem extends MSATokenCacheItem { - private static final String JSON_TOKEN_KEY = "refresh_token"; - private static final String JSON_EXPIRATION_KEY = "expires"; - - public MSARefreshTokenCacheItem(String token, int expiresInSeconds, MSATokenRequest refreshRequest) { - super(token, expiresInSeconds, refreshRequest); - } - - public MSARefreshTokenCacheItem(JSONObject json) throws IOException, JSONException, ParseException { - super(null, 0, new MSATokenRequest(mClientId, MSATokenRequest.GrantType.REFRESH, MSA_OFFLINE_ACCESS_SCOPE, null)); - - mToken = json.optString(JSON_TOKEN_KEY); - String dateString = json.optString(JSON_EXPIRATION_KEY); - if (mToken == null || dateString == null) { - throw new IOException("Saved refresh token was improperly formatted."); - } - - Date expirationDate = DateFormat.getDateTimeInstance().parse(dateString); - mCloseToExpirationDate = getDateSecondsAfter(expirationDate, -MSA_REFRESH_TOKEN_CLOSE_TO_EXPIRY_SECONDS); - } - - protected int getCloseToExpirySeconds() { - return MSA_REFRESH_TOKEN_CLOSE_TO_EXPIRY_SECONDS; - } - - protected long getRetrySeconds() { - return MSA_REFRESH_TOKEN_RETRY_SECONDS; - } - - protected AsyncOperation getRefreshTokenAsync() { - return AsyncOperation.completedFuture(mToken); - } - - protected synchronized void onSuccessfulRefresh(MSATokenRequest.Result result) { - Log.i(TAG, "Successfully refreshed refresh token."); - mToken = result.getRefreshToken(); - mCloseToExpirationDate = - getDateSecondsAfterNow(MSA_REFRESH_TOKEN_EXPIRATION_SECONDS - MSA_REFRESH_TOKEN_CLOSE_TO_EXPIRY_SECONDS); - MSATokenCache.this.markAccessTokensExpired(); - MSATokenCache.this.trySaveRefreshToken(); - } - - public synchronized JSONObject toJSON() throws JSONException { - // Get the actual expiration date - Date expirationDate = getDateSecondsAfter(mCloseToExpirationDate, MSA_REFRESH_TOKEN_CLOSE_TO_EXPIRY_SECONDS); - - JSONObject ret = new JSONObject(); - ret.put(JSON_TOKEN_KEY, mToken); - ret.put(JSON_EXPIRATION_KEY, DateFormat.getDateTimeInstance().format(expirationDate)); - return ret; - } - } - - /** - * Provides callbacks when the cache encounters a permanent failure and has to wipe its state. - */ - public static interface Listener { void onTokenCachePermanentFailure(); } - - private final String mClientId; - private final Context mContext; - - private MSARefreshTokenCacheItem mCachedRefreshToken = null; - private final Map mCachedAccessTokens = new ArrayMap<>(); - - private final Collection mListeners = new ArrayList<>(); - - public MSATokenCache(String clientId, Context context) { - mClientId = clientId; - mContext = context; - } - - /** - * Returns a file in application-specific storage that's used to persist the refresh token across sessions. - */ - private File getRefreshTokenSaveFile() throws IOException { - Context appContext = mContext.getApplicationContext(); - File appDirectory = appContext.getDir(appContext.getPackageName(), Context.MODE_PRIVATE); - if (appDirectory == null) { - throw new IOException("Could not access app directory."); - } - - return new File(appDirectory, "samplemsaaccountprovider.dat"); - } - - /** - * Tries to save the current refresh token to persistent storage. - */ - private void trySaveRefreshToken() { - Log.i(TAG, "Trying to save refresh token..."); - try { - File file = getRefreshTokenSaveFile(); - JSONObject json = file.exists() ? new JSONObject(IOUtil.readUTF8Stream(new FileInputStream(file))) : new JSONObject(); - - json.put(mClientId, mCachedRefreshToken.toJSON()); - IOUtil.writeUTF8Stream(new FileOutputStream(file), json.toString()); - - Log.i(TAG, "Saved refresh token."); - - } catch (IOException | JSONException e) { - Log.e(TAG, "Exception while saving refresh token. \"" + e.getLocalizedMessage() + "\" Will not save."); - } - } - - /** - * Tries to read a saved refresh token from persistent storage, and return it as an MSARefreshTokenItem. - */ - private MSARefreshTokenCacheItem tryReadSavedRefreshToken() { - Log.i(TAG, "Trying to read saved refresh token..."); - try { - File file = getRefreshTokenSaveFile(); - - if (!file.exists()) { - Log.i(TAG, "No saved refresh token was found."); - return null; - } - - JSONObject json = new JSONObject(IOUtil.readUTF8Stream(new FileInputStream(file))); - JSONObject innerJson = json.optJSONObject(mClientId); - - if (innerJson == null) { - Log.i(TAG, "Could not read saved refresh token."); - return null; - } - - Log.i(TAG, "Read saved refresh token."); - return new MSARefreshTokenCacheItem(innerJson); - - } catch (IOException | JSONException | ParseException e) { - Log.e(TAG, "Exception reading saved refresh token. \"" + e.getLocalizedMessage() + "\""); - return null; - } - } - - /** - * Tries to delete the saved refresh token for this app in persistent storage. - */ - private void tryClearSavedRefreshToken() { - Log.i(TAG, "Trying to delete saved refresh token..."); - try { - File file = getRefreshTokenSaveFile(); - if (!file.exists()) { - Log.i(TAG, "No saved refresh token was found."); - return; - } - - try { - // Try to remove just a section of the json corresponding to client id - JSONObject json = new JSONObject(IOUtil.readUTF8Stream(new FileInputStream(file))); - json.remove(mClientId); - - if (json.length() <= 0) { - file.delete(); // Just delete the file if the json would be empty - } else { - IOUtil.writeUTF8Stream(new FileOutputStream(file), json.toString()); - } - } catch (JSONException e) { - // Failed to parse the json, just delete everything - file.delete(); - } - - Log.i(TAG, "Deleted saved refresh token."); - - } catch (IOException e) { Log.e(TAG, "Failed to delete saved refresh token. \"" + e.getLocalizedMessage() + "\""); } - } - - /** - * Marks access tokens as expired, such that a refresh is performed before returning, when the access token is next requested. - */ - private synchronized void markAccessTokensExpired() { - for (MSATokenCacheItem cachedAccessToken : mCachedAccessTokens.values()) { - cachedAccessToken.markExpired(); - } - } - - /** - * Calls back any listeners that the cache has encountered a permanent failure, and that they should perform any needed error-handling. - */ - private void onPermanentFailure() { - clearTokens(); - for (Listener listener : mListeners) { - listener.onTokenCachePermanentFailure(); - } - } - - public void setRefreshToken(String refreshToken) { - MSATokenRequest refreshRequest = new MSATokenRequest(mClientId, MSATokenRequest.GrantType.REFRESH, MSA_OFFLINE_ACCESS_SCOPE, null); - - synchronized (this) { - mCachedRefreshToken = new MSARefreshTokenCacheItem(refreshToken, MSA_REFRESH_TOKEN_EXPIRATION_SECONDS, refreshRequest); - markAccessTokensExpired(); - trySaveRefreshToken(); - } - } - - public void setAccessToken(String accessToken, String scope, int expiresInSeconds) { - MSATokenRequest refreshRequest = new MSATokenRequest(mClientId, MSATokenRequest.GrantType.REFRESH, scope, null); - - synchronized (this) { - mCachedAccessTokens.put(scope, new MSATokenCacheItem(accessToken, expiresInSeconds, refreshRequest)); - } - } - - public synchronized AsyncOperation getRefreshTokenAsync() { - if (mCachedRefreshToken != null) { - return mCachedRefreshToken.getTokenAsync(); - } else { - return AsyncOperation.completedFuture(null); - } - } - - public synchronized AsyncOperation getAccessTokenAsync(String scope) { - MSATokenCacheItem cachedAccessToken = mCachedAccessTokens.get(scope); - if (cachedAccessToken != null) { - return cachedAccessToken.getTokenAsync(); - } else { - return AsyncOperation.completedFuture(null); - } - } - - public synchronized void addListener(Listener listener) { - mListeners.add(listener); - } - - public synchronized void removeListener(Listener listener) { - mListeners.remove(listener); - } - - public Set allScopes() { - return mCachedAccessTokens.keySet(); - } - - /** - * Tries to load a saved refresh token from disk. If successful, the loaded refresh token is used as this cache's refresh token. - * @return Whether a saved refresh token was loaded successfully. - */ - public boolean loadSavedRefreshToken() { - Log.i(TAG, "Trying to load saved refresh token..."); - MSARefreshTokenCacheItem savedRefreshToken = tryReadSavedRefreshToken(); - - if (savedRefreshToken == null) { - Log.i(TAG, "Failed to load saved refresh token."); - return false; - } - - if (savedRefreshToken.isExpired()) { - Log.i(TAG, "Read saved refresh token, but was expired. Ignoring."); - return false; - } - - Log.i(TAG, "Successfully loaded saved refresh token."); - mCachedRefreshToken = savedRefreshToken; - markAllTokensExpired(); // Force a refresh on everything on first use - return true; - } - - /** - * Clears all tokens from the cache, and any saved refresh tokens belonging to this app in persistent storage. - */ - public synchronized void clearTokens() { - mCachedAccessTokens.clear(); - mCachedRefreshToken = null; - tryClearSavedRefreshToken(); - } - - /** - * Marks all tokens as expired, such that a refresh is performed before returning, when a token is next requested. - */ - public synchronized void markAllTokensExpired() { - mCachedRefreshToken.markExpired(); - markAccessTokensExpired(); - } -} \ No newline at end of file diff --git a/Android/samples/account-provider-sample/src/com/microsoft/connecteddevices/sampleaccountproviders/MSATokenRequest.java b/Android/samples/account-provider-sample/src/com/microsoft/connecteddevices/sampleaccountproviders/MSATokenRequest.java deleted file mode 100644 index 51e0ac6..0000000 --- a/Android/samples/account-provider-sample/src/com/microsoft/connecteddevices/sampleaccountproviders/MSATokenRequest.java +++ /dev/null @@ -1,204 +0,0 @@ -// -// Copyright (c) Microsoft Corporation. All rights reserved. -// - -package com.microsoft.connecteddevices.sampleaccountproviders; - -import android.support.annotation.Keep; -import android.util.Log; -import android.util.Pair; - -import com.microsoft.connecteddevices.base.AsyncOperation; - -import java.io.BufferedReader; -import java.io.BufferedWriter; -import java.io.InputStream; -import java.io.InputStreamReader; -import java.io.IOException; -import java.io.OutputStream; -import java.io.OutputStreamWriter; -import java.io.UnsupportedEncodingException; -import java.net.HttpURLConnection; -import java.net.URL; -import java.net.URLEncoder; -import java.util.LinkedList; -import java.util.List; - -import javax.net.ssl.HttpsURLConnection; - -import org.json.JSONException; -import org.json.JSONObject; - -/** - * Encapsulates a noninteractive request for an MSA token. - * This request may be performed multiple times. - */ -@Keep -final class MSATokenRequest { - - private static final String TAG = MSATokenRequest.class.getName(); - - // OAuth Token Grant Type - public static final class GrantType { - public static final String CODE = "authorization_code"; - public static final String REFRESH = "refresh_token"; - } - - /** - * Class encapsulating the result of an MSATokenRequest. - */ - public static final class Result { - public static enum Status { SUCCESS, TRANSIENT_FAILURE, PERMANENT_FAILURE } - - private final Status mStatus; - private String mAccessToken = null; - private String mRefreshToken = null; - private int mExpiresIn = 0; - - Result(Status status, JSONObject responseJson) { - mStatus = status; - - if (responseJson != null) { - mAccessToken = responseJson.optString("access_token", null); - mRefreshToken = responseJson.optString("refresh_token", null); - mExpiresIn = responseJson.optInt("expires_in"); // returns 0 if this key doesn't exist - } - } - - public Status getStatus() { - return mStatus; - } - - public String getAccessToken() { - return mAccessToken; - } - - public String getRefreshToken() { - return mRefreshToken; - } - - public int getExpiresIn() { - return mExpiresIn; - } - } - - private final String mClientId; - private final String mGrantType; - private final String mScope; - private final String mRedirectUri; - - public MSATokenRequest(String clientId, String grantType, String scope, String redirectUri) { - mClientId = clientId; - mGrantType = grantType; - mScope = scope; - mRedirectUri = redirectUri; - } - - /** - * Builds a query string from a list of name-value pairs. - * - * @param params Name-value pairs to compose the query string from - * @return A query string composed of the provided name-value pairs - * @throws UnsupportedEncodingException Thrown if encoding a name or value fails - */ - private static String getQueryString(List> params) throws UnsupportedEncodingException { - StringBuilder queryStringBuilder = new StringBuilder(); - boolean isFirstParam = true; - for (Pair param : params) { - if (isFirstParam) { - isFirstParam = false; - } else { - queryStringBuilder.append("&"); - } - - queryStringBuilder.append(URLEncoder.encode(param.first, "UTF-8")); - queryStringBuilder.append("="); - queryStringBuilder.append(URLEncoder.encode(param.second, "UTF-8")); - } - - return queryStringBuilder.toString(); - } - - /** - * Fetch Token (Access or Refresh Token). - * @param clientId - clientId of the app's registration in the MSA portal - * @param grantType - one of the MSATokenRequest.GrantType constants - * @param scope - * @param redirectUri - * @param token - authCode for GrantType.CODE, or refresh token for GrantType.REFRESH - */ - public static AsyncOperation requestAsync( - final String clientId, final String grantType, final String scope, final String redirectUri, final String token) { - if (token == null || token.length() <= 0) { - Log.e(TAG, "Refresh token or auth code for MSATokenRequest was unexpectedly empty - treating as permanent failure."); - return AsyncOperation.completedFuture(new MSATokenRequest.Result(Result.Status.PERMANENT_FAILURE, null)); - } - - return AsyncOperation.supplyAsync(new AsyncOperation.Supplier() { - @Override - public MSATokenRequest.Result get() { - HttpsURLConnection connection = null; - MSATokenRequest.Result.Status status = Result.Status.TRANSIENT_FAILURE; - JSONObject responseJson = null; - - try { - // Build the query string - List> params = new LinkedList<>(); - params.add(new Pair<>("client_id", clientId)); - params.add(new Pair<>("grant_type", grantType)); - - if (grantType.equals(GrantType.CODE)) { - params.add(new Pair<>("redirect_uri", redirectUri)); - params.add(new Pair<>("code", token)); - } else if (grantType.equals(GrantType.REFRESH)) { - params.add(new Pair<>("scope", scope)); - params.add(new Pair<>(grantType, token)); - } - - String queryString = getQueryString(params); - - // Write the query string - URL url = new URL("https://login.live.com/oauth20_token.srf"); - connection = (HttpsURLConnection)url.openConnection(); - connection.setDoOutput(true); - connection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded"); - IOUtil.writeUTF8Stream(connection.getOutputStream(), queryString); - - // Parse the response - int responseCode = connection.getResponseCode(); - if (responseCode >= 500) { - status = Result.Status.TRANSIENT_FAILURE; - } else if (responseCode >= 400) { - status = Result.Status.PERMANENT_FAILURE; - } else if ((responseCode >= 200 && responseCode < 300) || responseCode == 304) { - status = Result.Status.SUCCESS; - } else { - status = Result.Status.TRANSIENT_FAILURE; - } - - if (status == Result.Status.SUCCESS) { - responseJson = new JSONObject(IOUtil.readUTF8Stream(connection.getInputStream())); - } else { - Log.e(TAG, "Failed to get token with HTTP code: " + responseCode); - } - - } catch (IOException | JSONException e) { - Log.e(TAG, "Failed to get token: \"" + e.getLocalizedMessage() + "\""); - } finally { - if (connection != null) { - connection.disconnect(); - } - return new MSATokenRequest.Result(status, responseJson); - } - } - }); - } - - /** - * Fetch token (Access or Refresh Token). - * @param token - authCode for GrantType.CODE, or refresh token for GrantType.REFRESH - */ - public AsyncOperation requestAsync(String token) { - return requestAsync(mClientId, mGrantType, mScope, mRedirectUri, token); - } -} \ No newline at end of file diff --git a/Android/samples/account-provider-sample/src/main/java/com/microsoft/connecteddevices/sampleaccountproviders/AADMSAAccountProvider.java b/Android/samples/account-provider-sample/src/main/java/com/microsoft/connecteddevices/sampleaccountproviders/AADMSAAccountProvider.java index 90cd354..5a37487 100644 --- a/Android/samples/account-provider-sample/src/main/java/com/microsoft/connecteddevices/sampleaccountproviders/AADMSAAccountProvider.java +++ b/Android/samples/account-provider-sample/src/main/java/com/microsoft/connecteddevices/sampleaccountproviders/AADMSAAccountProvider.java @@ -44,11 +44,13 @@ public final class AADMSAAccountProvider implements UserAccountProvider { /** * @param msaClientId id of the app's registration in the MSA portal + * @param msaScopeOverrides scope overrides for the app * @param aadClientId id of the app's registration in the Azure portal - * @param redirectUri redirect uri the app is registered with in the Azure portal + * @param aadRedirectUri redirect uri the app is registered with in the Azure portal * @param context */ - public AADMSAAccountProvider(String msaClientId, String aadClientId, String aadRedirectUri, Context context) { + public AADMSAAccountProvider( + String msaClientId, final Map msaScopeOverrides, String aadClientId, String aadRedirectUri, Context context) { // Chain the inner events to the event provided by this helper mListener = new EventListener() { @@ -58,7 +60,7 @@ public final class AADMSAAccountProvider implements UserAccountProvider { } }; - mMSAProvider = new MSAAccountProvider(msaClientId, context); + mMSAProvider = new MSAAccountProvider(msaClientId, msaScopeOverrides, context); mAADProvider = new AADAccountProvider(aadClientId, aadRedirectUri, context); if (mMSAProvider.isSignedIn() && mAADProvider.isSignedIn()) { diff --git a/Android/samples/account-provider-sample/src/main/java/com/microsoft/connecteddevices/sampleaccountproviders/IOUtil.java b/Android/samples/account-provider-sample/src/main/java/com/microsoft/connecteddevices/sampleaccountproviders/IOUtil.java index 31d6109..466eae6 100644 --- a/Android/samples/account-provider-sample/src/main/java/com/microsoft/connecteddevices/sampleaccountproviders/IOUtil.java +++ b/Android/samples/account-provider-sample/src/main/java/com/microsoft/connecteddevices/sampleaccountproviders/IOUtil.java @@ -15,7 +15,7 @@ import java.io.InputStreamReader; import java.io.OutputStreamWriter; @Keep -final class IOUtil { +public final class IOUtil { /** * Writes UTF-8 output data to an output stream. @@ -25,7 +25,7 @@ final class IOUtil { * @param data Data to write * @throws IOException Thrown if the output stream is unavailable, or encoding the data fails */ - static void writeUTF8Stream(OutputStream stream, String data) throws IOException { + public static void writeUTF8Stream(OutputStream stream, String data) throws IOException { try (BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(stream, "UTF-8"))) { writer.write(data); } @@ -39,7 +39,7 @@ final class IOUtil { * @return All data received from the stream * @throws IOException Thrown if the input stream is unavailable, or decoding the data fails */ - static String readUTF8Stream(InputStream stream) throws IOException { + public static String readUTF8Stream(InputStream stream) throws IOException { StringBuilder stringBuilder = new StringBuilder(); try (BufferedReader reader = new BufferedReader(new InputStreamReader(stream, "UTF-8"))) { String line; diff --git a/Android/samples/account-provider-sample/src/main/java/com/microsoft/connecteddevices/sampleaccountproviders/MSAAccountProvider.java b/Android/samples/account-provider-sample/src/main/java/com/microsoft/connecteddevices/sampleaccountproviders/MSAAccountProvider.java index aad8288..e70fab4 100644 --- a/Android/samples/account-provider-sample/src/main/java/com/microsoft/connecteddevices/sampleaccountproviders/MSAAccountProvider.java +++ b/Android/samples/account-provider-sample/src/main/java/com/microsoft/connecteddevices/sampleaccountproviders/MSAAccountProvider.java @@ -41,6 +41,8 @@ import java.io.UnsupportedEncodingException; import java.net.HttpURLConnection; import java.net.URL; import java.net.URLEncoder; +import java.util.ArrayList; +import java.util.Arrays; import java.util.LinkedList; import java.util.List; import java.util.Map; @@ -107,14 +109,14 @@ public final class MSAAccountProvider implements UserAccountProvider, MSATokenCa // CDP's SDK currently requires authorization for all features, otherwise platform initialization will fail. // As such, the user must sign in/consent for the following scopes. This may change to become more modular in the future. - private static final String[] REQUIRED_SCOPES = { + private static final String[] KNOWN_SCOPES = { + "wl.offline_access", // read and update user info at any time "ccs.ReadWrite", // device commanding scope "dds.read", // device discovery scope (discover other devices) "dds.register", // device discovery scope (allow discovering this device) - "wns.connect", // notification scope - "wl.offline_access", // read and update user info at any time - "https://activity.windows.com/UserActivity.ReadWrite.CreatedByApp", // user activities scope - "asimovrome.telemetry" // asimov token scope + "wns.connect", // push notification scope + "asimovrome.telemetry", // asimov token scope + "https://activity.windows.com/UserActivity.ReadWrite.CreatedByApp", // default useractivities scope }; // OAuth URLs @@ -125,6 +127,7 @@ public final class MSAAccountProvider implements UserAccountProvider, MSATokenCa // region Member Variables private final String mClientId; + private final Map mScopeOverrideMap; private UserAccount mAccount = null; private MSATokenCache mTokenCache; private boolean mSignInSignOutInProgress; @@ -135,11 +138,13 @@ public final class MSAAccountProvider implements UserAccountProvider, MSATokenCa // region Constructor /** - * @param clientId id of the app's registration in the MSA portal + * @param clientId id of the app's registration in the MSA portal + * @param scopeOverrides scope overrides for the app * @param context */ - public MSAAccountProvider(String clientId, Context context) { + public MSAAccountProvider(String clientId, final Map scopeOverrides, Context context) { mClientId = clientId; + mScopeOverrideMap = scopeOverrides; mTokenCache = new MSATokenCache(clientId, context); mTokenCache.addListener(new MSATokenCache.Listener() { @Override @@ -158,6 +163,22 @@ public final class MSAAccountProvider implements UserAccountProvider, MSATokenCa // endregion // region Private Helpers + private List getAuthScopes(final String[] incoming) { + ArrayList authScopes = new ArrayList(); + + for (String scope : incoming) { + if (mScopeOverrideMap.containsKey(scope)) { + for (String replacement : mScopeOverrideMap.get(scope)) { + authScopes.add(replacement); + } + } else { + authScopes.add(scope); + } + } + + return authScopes; + } + private AsyncOperation notifyListenersAsync() { return AsyncOperation.supplyAsync(new AsyncOperation.Supplier() { @Override @@ -240,7 +261,7 @@ public final class MSAAccountProvider implements UserAccountProvider, MSATokenCa } final String signInUrl = AUTHORIZE_URL + "?redirect_uri=" + REDIRECT_URL + "&response_type=code&client_id=" + mClientId + - "&scope=" + TextUtils.join("+", REQUIRED_SCOPES); + "&scope=" + TextUtils.join("+", getAuthScopes(KNOWN_SCOPES)); final AsyncOperation authCodeOperation = new AsyncOperation<>(); final AsyncOperation signInOperation = new AsyncOperation<>(); mSignInSignOutInProgress = true; @@ -396,7 +417,7 @@ public final class MSAAccountProvider implements UserAccountProvider, MSATokenCa public synchronized AsyncOperation getAccessTokenForUserAccountAsync(final String accountId, final String[] scopes) { if (mAccount != null && accountId != null && accountId.equals(mAccount.getId()) && scopes.length > 0) { - final String scope = TextUtils.join(" ", scopes); + final String scope = TextUtils.join(" ", getAuthScopes(scopes)); return mTokenCache.getAccessTokenAsync(scope).thenComposeAsync( new AsyncOperation.ResultFunction>() { @@ -405,7 +426,6 @@ public final class MSAAccountProvider implements UserAccountProvider, MSATokenCa if (accessToken != null) { // token already exists in the cache, can early return return AsyncOperation.completedFuture(new AccessTokenResult(AccessTokenRequestStatus.SUCCESS, accessToken)); - } else { // token does not yet exist in the cache, need to request a new one return requestNewAccessTokenAsync(scope); diff --git a/Android/samples/sdksample/app/src/main/AndroidManifest.xml b/Android/samples/sdksample/app/src/main/AndroidManifest.xml index 893d980..12b4b60 100644 --- a/Android/samples/sdksample/app/src/main/AndroidManifest.xml +++ b/Android/samples/sdksample/app/src/main/AndroidManifest.xml @@ -2,6 +2,7 @@ (), context); } public void signIn(Activity activity, AsyncOperation.ResultBiConsumer signInCompletionHandler) { diff --git a/Android/samples/sdksample/app/src/main/java/com/microsoft/rome/onesdksample_android/MainActivity.java b/Android/samples/sdksample/app/src/main/java/com/microsoft/rome/onesdksample_android/MainActivity.java index cffe951..0d1c648 100644 --- a/Android/samples/sdksample/app/src/main/java/com/microsoft/rome/onesdksample_android/MainActivity.java +++ b/Android/samples/sdksample/app/src/main/java/com/microsoft/rome/onesdksample_android/MainActivity.java @@ -18,10 +18,12 @@ import android.widget.ArrayAdapter; import android.widget.ListView; import android.widget.Toast; -import com.microsoft.connecteddevices.base.AsyncOperation; +import com.microsoft.connecteddevices.base.EventListener; +import com.microsoft.connecteddevices.commanding.CloudRegistrationStatus; import com.microsoft.connecteddevices.core.Platform; -import com.microsoft.connecteddevices.core.PlatformCreationResult; -import com.microsoft.connecteddevices.core.PlatformCreationStatus; +import com.microsoft.connecteddevices.core.UserAccount; +import com.microsoft.connecteddevices.hosting.AppServiceProvider; +import com.microsoft.connecteddevices.sampleaccountproviders.MSAAccountProvider; import java.util.ArrayList; @@ -140,46 +142,50 @@ public class MainActivity extends AppCompatActivity { // endregion public void initializePlatform() { - Toast.makeText(getApplicationContext(), "Initializing the Rome platform", Toast.LENGTH_LONG).show(); + raiseToast("Initializing the Rome platform"); // Instantiate Platform using the UserAccountProvider the sign in helper provides - AsyncOperation resultOperation = PlatformBroker.start(this); + MSAAccountProvider signInHelper = AccountProviderBroker.getSignInHelper(); + GcmNotificationProvider gcmNotificationProvider = new GcmNotificationProvider(this); + mPlatform = PlatformBroker.createPlatform(this, signInHelper, gcmNotificationProvider); - // Can handle success/failure to create platform, simply give a toast - resultOperation.whenComplete(new AsyncOperation.ResultBiConsumer() { + raiseToast("Completed Rome initialization, starting registration..."); + + ArrayList appServiceProviders = new ArrayList<>(); + appServiceProviders.add(new PingPongService(this)); + appServiceProviders.add(new EchoService(this)); + + PlatformBroker.register(this, appServiceProviders, new SimpleLaunchHandler(this), new EventListener() { @Override - public void accept(PlatformCreationResult platformCreationResult, Throwable throwable) throws Throwable { - if (throwable != null) { - Log.e(TAG, "Platform init failed with exception: " + throwable.getMessage()); - throwable.printStackTrace(); - } else { - if (platformCreationResult.getStatus() == PlatformCreationStatus.FAILURE) { - Log.e(TAG, "Failed to initialize platform"); - } else { - mPlatform = platformCreationResult.getPlatform(); - Log.d(TAG, "Initialized platform successfully"); + public void onEvent(UserAccount account, CloudRegistrationStatus status) { + switch (status) { + case NOT_STARTED: + Log.d(TAG, "Registration has not started."); + break; + case IN_PROGRESS: + Log.d(TAG, "Registration in progress..."); + break; + case SUCCEEDED: + raiseToast("Completed Rome registration"); - // Inform MainActivity that the Rome platform has been initialized - platformInitializationComplete(); - } + // When the CDP platform has finished registering, initialize the UserActivity Feed + getUserActivityFragment().initializeUserActivityFeed(); + + runOnUiThread(new Runnable() { + @Override + public void run() { + navigateToPage(SDK_SELECT); + } + }); + break; + case FAILED: + raiseToast("Rome registration failed!"); + break; } } }); } - public void platformInitializationComplete() { - runOnUiThread(new Runnable() { - @Override - public void run() { - Toast.makeText(getApplicationContext(), "Completed Rome initialization", Toast.LENGTH_SHORT).show(); - navigateToPage(SDK_SELECT); - } - }); - - // When the CDP platform has finished initializing, initialize the UserActivity Feed - getUserActivityFragment().initializeUserActivityFeed(); - } - /** * Get the current selected fragment visible to the user * @return The current selected fragment visible to the user @@ -209,16 +215,16 @@ public class MainActivity extends AppCompatActivity { } // region Navigation + public void navigateToPage(String page) { + navigateToPage(getNativationPage((page))); + } + /** * Replaces the current fragment, if any, loaded in the navigation frame with the fragment * representing the selected page. * * @param position Index of the page to navigate to */ - public void navigateToPage(String page) { - navigateToPage(getNativationPage((page))); - } - private void navigateToPage(int position) { navigateToPage(mPages.get(position)); @@ -272,6 +278,15 @@ public class MainActivity extends AppCompatActivity { } } + private void raiseToast(final String message) { + runOnUiThread(new Runnable() { + @Override + public void run() { + Toast.makeText(getApplicationContext(), message, Toast.LENGTH_SHORT).show(); + } + }); + } + /** * Listener for handling navigation to the selected page on click. */ diff --git a/Android/samples/sdksample/app/src/main/java/com/microsoft/rome/onesdksample_android/PlatformBroker.java b/Android/samples/sdksample/app/src/main/java/com/microsoft/rome/onesdksample_android/PlatformBroker.java index b80d373..662c737 100644 --- a/Android/samples/sdksample/app/src/main/java/com/microsoft/rome/onesdksample_android/PlatformBroker.java +++ b/Android/samples/sdksample/app/src/main/java/com/microsoft/rome/onesdksample_android/PlatformBroker.java @@ -4,20 +4,26 @@ package com.microsoft.rome.onesdksample_android; -import android.app.Activity; import android.content.Context; import android.content.SharedPreferences; +import android.util.Log; -import com.microsoft.connecteddevices.base.AsyncOperation; +import com.microsoft.connecteddevices.base.EventListener; +import com.microsoft.connecteddevices.commanding.CloudRegistrationStatus; +import com.microsoft.connecteddevices.commanding.IRemoteSystemApplicationRegistration; +import com.microsoft.connecteddevices.core.NotificationProvider; import com.microsoft.connecteddevices.core.Platform; -import com.microsoft.connecteddevices.core.PlatformCreationResult; -import com.microsoft.connecteddevices.hosting.ApplicationRegistration; -import com.microsoft.connecteddevices.hosting.ApplicationRegistrationBuilder; -import com.microsoft.connecteddevices.sampleaccountproviders.MSAAccountProvider; +import com.microsoft.connecteddevices.core.UserAccount; +import com.microsoft.connecteddevices.core.UserAccountProvider; +import com.microsoft.connecteddevices.hosting.AppServiceProvider; +import com.microsoft.connecteddevices.hosting.LaunchUriProvider; +import com.microsoft.connecteddevices.hosting.RemoteSystemApplicationRegistrationBuilder; +import com.microsoft.connecteddevices.userdata.SyncScope; +import com.microsoft.connecteddevices.userdata.UserDataFeed; import java.text.SimpleDateFormat; +import java.util.ArrayList; import java.util.Date; -import java.util.Locale; /** * Most importantly in MainActivity is the platform initialization, happening in init() @@ -25,47 +31,81 @@ import java.util.Locale; public class PlatformBroker { // region Member Variables private static final String TAG = PlatformBroker.class.getName(); - private static final String DATE_FORMAT = "MM/dd/yyyy HH:mm:ss"; - private static final String TIMESTAMP_KEY = "TIMESTAMP_KEY"; - private static final String PACKAGE_KEY = "PACKAGE_KEY"; - private static final String PACKAGE_VALUE = "com.microsoft.rome.onesdksample_android"; - // endregion - public static AsyncOperation start(MainActivity mainActivity) { - // Register the builder to application with attributes and hosting providers . - ApplicationRegistrationBuilder builder = new ApplicationRegistrationBuilder(); - builder.addAttribute(TIMESTAMP_KEY, getInitialRegistrationDateTime(mainActivity)); - builder.addAttribute(PACKAGE_KEY, PACKAGE_VALUE); + public static final String DATE_FORMAT = "MM/dd/yyyy HH:mm:ss"; + public static final String TIMESTAMP_KEY = "TIMESTAMP_KEY"; + public static final String PACKAGE_KEY = "PACKAGE_KEY"; + public static final String GUID_KEY = "GUID_KEY"; + public static final String PACKAGE_VALUE = "com.microsoft.oneRomanApp"; - // We add 2 AppService providers. - builder.addAppServiceProvider(new PingPongService(mainActivity)); - builder.addAppServiceProvider(new EchoService(mainActivity)); - // We set the only LaunchUri provider. - builder.setLaunchUriProvider(new SimpleLaunchHandler(mainActivity)); + private static Platform sPlatform; - GcmNotificationProvider gcmNotificationProvider = new GcmNotificationProvider(mainActivity); - ApplicationRegistration applicationRegistration = builder.buildRegistration(); - MSAAccountProvider signInHelper = AccountProviderBroker.getSignInHelper(); + private PlatformBroker() { } - // Instantiate Platform using the UserAccountProvider the sign in helper provides - return Platform.createInstanceAsync(mainActivity, gcmNotificationProvider, applicationRegistration, signInHelper); + public static synchronized Platform getPlatform() { + return sPlatform; } - private static String getInitialRegistrationDateTime(Activity activity) { - SharedPreferences preferences = activity.getPreferences(Context.MODE_PRIVATE); - // Check that the SharedPreferences has the timestamp. This should be true after the first clean install -> Platform init. - if (preferences.contains(TIMESTAMP_KEY)) { - // You must provide a default value. Since we check that key exists we should never get a empty string. - String timestamp = preferences.getString(TIMESTAMP_KEY, ""); - if (timestamp.isEmpty()) { - throw new RuntimeException("Failed to get TimeStamp after verifying it exists"); - } - return timestamp; + public static synchronized Platform createPlatform(Context context, UserAccountProvider accountProvider, NotificationProvider notificationProvider) { + sPlatform = new Platform(context, accountProvider, notificationProvider); + return sPlatform; + } + + public static synchronized Platform getOrCreatePlatform(Context context, UserAccountProvider accountProvider, NotificationProvider notificationProvider) { + Platform platform = getPlatform(); + + if (platform == null) { + platform = createPlatform(context, accountProvider, notificationProvider); } - // Create the initial timestamp for RemoteSystemApplication registration and store it in SharedPreferences - String timestamp = new SimpleDateFormat(DATE_FORMAT, Locale.US).format(new Date()); - preferences.edit().putString(TIMESTAMP_KEY, timestamp).apply(); + return platform; + } + + public static void register(Context context, ArrayList appServiceProviders, LaunchUriProvider launchUriProvider, EventListener listener) { + // Initialize the platform with all possible services + RemoteSystemApplicationRegistrationBuilder builder = new RemoteSystemApplicationRegistrationBuilder(); + builder.addAttribute(TIMESTAMP_KEY, getInitialRegistrationDateTime(context)); + builder.addAttribute(PACKAGE_KEY, PACKAGE_VALUE); + + // Add the given AppService and LaunchUri Providers to the registration builder + if (appServiceProviders != null) { + for (AppServiceProvider provider : appServiceProviders) { + builder.addAppServiceProvider(provider); + } + } + if (launchUriProvider != null) { + builder.setLaunchUriProvider(launchUriProvider); + } + + IRemoteSystemApplicationRegistration registration = builder.buildRegistration(); + // Add an EventListener to handle registration completion + registration.addCloudRegistrationStatusChangedListener(listener); + registration.start(); + } + + /** + * Grab the initial registration date-time if one is found, otherwise generate a new one. + * @param context + * @return Datetime to insert into the RemoteSystemApplicationRegistrationBuilder + */ + private static 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 RemoteSystemApplication registration and store it in SharedPreferences + timestamp = new SimpleDateFormat(DATE_FORMAT).format(new Date()); + preferences.edit().putString(TIMESTAMP_KEY, timestamp).apply(); + } return timestamp; } diff --git a/Android/samples/sdksample/app/src/main/java/com/microsoft/rome/onesdksample_android/Secrets.java.example b/Android/samples/sdksample/app/src/main/java/com/microsoft/rome/onesdksample_android/Secrets.java.example index 8386c3d..077371e 100644 --- a/Android/samples/sdksample/app/src/main/java/com/microsoft/rome/onesdksample_android/Secrets.java.example +++ b/Android/samples/sdksample/app/src/main/java/com/microsoft/rome/onesdksample_android/Secrets.java.example @@ -10,4 +10,7 @@ class Secrets { // Your client's Google Cloud Messaging Sender Id from: https://console.developers.google.com/cloud-resource-manager static final String GCM_SENDER_ID = "<>"; + + // Your application host name from: https://apps.dev.microsoft.com/ + static final String APP_HOST_NAME = "<>"; } diff --git a/Android/samples/sdksample/app/src/main/java/com/microsoft/rome/onesdksample_android/StaticContextApp.java b/Android/samples/sdksample/app/src/main/java/com/microsoft/rome/onesdksample_android/StaticContextApp.java new file mode 100644 index 0000000..f72fc0f --- /dev/null +++ b/Android/samples/sdksample/app/src/main/java/com/microsoft/rome/onesdksample_android/StaticContextApp.java @@ -0,0 +1,22 @@ +// +// Copyright (c) Microsoft Corporation. All rights reserved. +// + +package com.microsoft.rome.onesdksample_android; + +import android.app.Application; +import android.content.Context; + +public class StaticContextApp extends Application { + private static Context mContext; + + @Override + public void onCreate() { + super.onCreate(); + mContext = this; + } + + public static String getStringValue(int id) { + return mContext.getString(id); + } +} diff --git a/Android/samples/sdksample/app/src/main/java/com/microsoft/rome/onesdksample_android/UserActivityFragment.java b/Android/samples/sdksample/app/src/main/java/com/microsoft/rome/onesdksample_android/UserActivityFragment.java index 642a047..dd62c0d 100644 --- a/Android/samples/sdksample/app/src/main/java/com/microsoft/rome/onesdksample_android/UserActivityFragment.java +++ b/Android/samples/sdksample/app/src/main/java/com/microsoft/rome/onesdksample_android/UserActivityFragment.java @@ -16,16 +16,23 @@ import android.widget.ListView; import android.widget.TextView; import com.microsoft.connecteddevices.base.AsyncOperation; +import com.microsoft.connecteddevices.base.EventListener; import com.microsoft.connecteddevices.core.UserAccount; import com.microsoft.connecteddevices.useractivities.UserActivity; import com.microsoft.connecteddevices.useractivities.UserActivityChannel; import com.microsoft.connecteddevices.useractivities.UserActivitySession; import com.microsoft.connecteddevices.useractivities.UserActivitySessionHistoryItem; +import com.microsoft.connecteddevices.userdata.SyncScope; +import com.microsoft.connecteddevices.userdata.UserDataFeed; +import com.microsoft.connecteddevices.userdata.UserDataSyncStatus; +import com.microsoft.connecteddevices.usernotifications.UserNotificationChannel; import java.util.ArrayList; import java.util.Collections; import java.util.UUID; +import static com.microsoft.rome.onesdksample_android.StaticContextApp.getStringValue; + /** * Creates, publishes, and reads User Activities * Create the UserActivityChannel @@ -59,7 +66,6 @@ import java.util.UUID; public class UserActivityFragment extends BaseFragment implements View.OnClickListener { private static final String TAG = UserActivityFragment.class.getName(); - // private Button mAfcInitButton; private TextView mActivityStatus; private Button mNewButton; private Button mStartButton; @@ -74,29 +80,45 @@ public class UserActivityFragment extends BaseFragment implements View.OnClickLi private UserActivity mActivity; private UserActivitySession mActivitySession; private UserActivityChannel mActivityChannel; + private UserDataFeed mUserDataFeed; + private String mStatusText; @Nullable private UserActivityChannel getUserActivityChannel() { - UserAccount[] accounts = AccountProviderBroker.getSignInHelper().getUserAccounts(); - if (accounts.length <= 0) { - setStatus(R.string.status_activities_signin_required); - return null; - } + mStatusText = getStringValue(R.string.status_activities_get_channel); + Log.d(TAG, mStatusText); - setStatus(R.string.status_activities_get_channel); UserActivityChannel channel = null; try { // Step #1 // create a UserActivityChannel for the signed in account - channel = new UserActivityChannel(accounts[0]); - setStatus(R.string.status_activities_get_channel_success); + channel = new UserActivityChannel(mUserDataFeed); + + mStatusText = getStringValue(R.string.status_activities_get_channel_success); + Log.d(TAG, mStatusText); } catch (Exception e) { e.printStackTrace(); - setStatus(R.string.status_activities_get_channel_failed); + mStatusText = getStringValue(R.string.status_activities_get_channel_failed); + Log.e(TAG, mStatusText); } return channel; } + private UserDataFeed getUserDataFeed(SyncScope[] scopes, EventListener listener) { + UserAccount[] accounts = AccountProviderBroker.getSignInHelper().getUserAccounts(); + if (accounts.length <= 0) { + mStatusText = getStringValue(R.string.status_activities_signin_required); + Log.e(TAG, mStatusText); + return null; + } + + UserDataFeed feed = UserDataFeed.getForAccount(accounts[0], PlatformBroker.getPlatform(), Secrets.APP_HOST_NAME); + feed.addSyncStatusChangedListener(listener); + feed.addSyncScopes(scopes); + feed.startSync(); + return feed; + } + private String createActivityId() { return UUID.randomUUID().toString(); } @@ -108,10 +130,12 @@ public class UserActivityFragment extends BaseFragment implements View.OnClickLi try { activity = activityOperation.get(); - setStatus(R.string.status_activities_create_activity_success); + mStatusText = getStringValue(R.string.status_activities_create_activity_success); + Log.d(TAG, mStatusText); } catch (Exception e) { e.printStackTrace(); - setStatus(R.string.status_activities_create_activity_failed); + mStatusText = getStringValue(R.string.status_activities_create_activity_failed); + Log.e(TAG, mStatusText); } return activity; } @@ -120,9 +144,24 @@ public class UserActivityFragment extends BaseFragment implements View.OnClickLi * Initializes the UserActivityFeed. */ public void initializeUserActivityFeed() { - setStatus(R.string.status_activities_initialize); + mStatusText = getStringValue(R.string.status_activities_initialize); + Log.d(TAG, mStatusText); + + SyncScope[] scopes = { UserActivityChannel.getSyncScope(), UserNotificationChannel.getSyncScope() }; + mUserDataFeed = getUserDataFeed(scopes, new EventListener() { + @Override + public void onEvent(UserDataFeed userDataFeed, Void aVoid) { + if (userDataFeed.getSyncStatus() == UserDataSyncStatus.SYNCHRONIZED) { + mStatusText = getStringValue(R.string.status_activities_initialize_complete); + Log.d(TAG, mStatusText); + } else { + mStatusText = getStringValue(R.string.status_activities_initialize_failed); + Log.e(TAG, mStatusText); + } + } + }); + mActivityChannel = getUserActivityChannel(); - setStatus(R.string.status_activities_initialize_complete); } @Override @@ -150,6 +189,8 @@ public class UserActivityFragment extends BaseFragment implements View.OnClickLi mListAdapter = new UserActivityListAdapter(getContext(), mHistoryItems); mListView.setAdapter(mListAdapter); + setStatus(mStatusText); + return rootView; } @@ -284,7 +325,14 @@ public class UserActivityFragment extends BaseFragment implements View.OnClickLi } void setStatus(int resourceId) { - final String text = getString(resourceId); + setStatus(getStringValue(resourceId)); + } + + void setStatus(final String text) { + if (text == null) { + return; + } + getActivity().runOnUiThread(new Runnable() { @Override public void run() { diff --git a/Android/samples/sdksample/app/src/main/java/com/microsoft/rome/onesdksample_android/UserActivityListAdapter.java b/Android/samples/sdksample/app/src/main/java/com/microsoft/rome/onesdksample_android/UserActivityListAdapter.java index a15f04e..5138565 100644 --- a/Android/samples/sdksample/app/src/main/java/com/microsoft/rome/onesdksample_android/UserActivityListAdapter.java +++ b/Android/samples/sdksample/app/src/main/java/com/microsoft/rome/onesdksample_android/UserActivityListAdapter.java @@ -13,8 +13,10 @@ import android.widget.ArrayAdapter; import android.widget.TextView; import com.microsoft.connecteddevices.useractivities.UserActivity; +import com.microsoft.connecteddevices.useractivities.UserActivityAttribution; import com.microsoft.connecteddevices.useractivities.UserActivitySessionHistoryItem; +import java.util.Date; import java.util.List; /** @@ -38,18 +40,34 @@ public class UserActivityListAdapter extends ArrayAdapterUserActivityFeed not yet initialized. Please wait… Initializing UserActivityFeed… UserActivityFeed initialized + UserActivityFeed initialization failed Creating UserActivity… Getting UserActivityChannel… Got UserActivityChannel successfully diff --git a/Android/samples/sdksample/sampleAccountProviders/build.gradle b/Android/samples/sdksample/sampleAccountProviders/build.gradle index 69a2aec..e769c7f 100644 --- a/Android/samples/sdksample/sampleAccountProviders/build.gradle +++ b/Android/samples/sdksample/sampleAccountProviders/build.gradle @@ -1,10 +1,12 @@ apply plugin: 'com.android.library' android { - compileSdkVersion 25 + compileSdkVersion 27 + buildToolsVersion '27.0.3' + defaultConfig { minSdkVersion 19 - targetSdkVersion 25 + targetSdkVersion 27 versionCode 1 versionName "1.0" } @@ -20,7 +22,7 @@ android { } dependencies { - implementation 'com.android.support:appcompat-v7:25.3.1' + implementation 'com.android.support:support-annotations:27.1.1' implementation('com.microsoft.aad:adal:1.13.1') { exclude group: 'com.android.support' } diff --git a/Android/samples/sdksample/sampleAccountProviders/src/main/java/com/microsoft/connecteddevices/sampleaccountproviders/AADMSAAccountProvider.java b/Android/samples/sdksample/sampleAccountProviders/src/main/java/com/microsoft/connecteddevices/sampleaccountproviders/AADMSAAccountProvider.java index 90cd354..5a37487 100644 --- a/Android/samples/sdksample/sampleAccountProviders/src/main/java/com/microsoft/connecteddevices/sampleaccountproviders/AADMSAAccountProvider.java +++ b/Android/samples/sdksample/sampleAccountProviders/src/main/java/com/microsoft/connecteddevices/sampleaccountproviders/AADMSAAccountProvider.java @@ -44,11 +44,13 @@ public final class AADMSAAccountProvider implements UserAccountProvider { /** * @param msaClientId id of the app's registration in the MSA portal + * @param msaScopeOverrides scope overrides for the app * @param aadClientId id of the app's registration in the Azure portal - * @param redirectUri redirect uri the app is registered with in the Azure portal + * @param aadRedirectUri redirect uri the app is registered with in the Azure portal * @param context */ - public AADMSAAccountProvider(String msaClientId, String aadClientId, String aadRedirectUri, Context context) { + public AADMSAAccountProvider( + String msaClientId, final Map msaScopeOverrides, String aadClientId, String aadRedirectUri, Context context) { // Chain the inner events to the event provided by this helper mListener = new EventListener() { @@ -58,7 +60,7 @@ public final class AADMSAAccountProvider implements UserAccountProvider { } }; - mMSAProvider = new MSAAccountProvider(msaClientId, context); + mMSAProvider = new MSAAccountProvider(msaClientId, msaScopeOverrides, context); mAADProvider = new AADAccountProvider(aadClientId, aadRedirectUri, context); if (mMSAProvider.isSignedIn() && mAADProvider.isSignedIn()) { diff --git a/Android/samples/sdksample/sampleAccountProviders/src/main/java/com/microsoft/connecteddevices/sampleaccountproviders/IOUtil.java b/Android/samples/sdksample/sampleAccountProviders/src/main/java/com/microsoft/connecteddevices/sampleaccountproviders/IOUtil.java index 31d6109..466eae6 100644 --- a/Android/samples/sdksample/sampleAccountProviders/src/main/java/com/microsoft/connecteddevices/sampleaccountproviders/IOUtil.java +++ b/Android/samples/sdksample/sampleAccountProviders/src/main/java/com/microsoft/connecteddevices/sampleaccountproviders/IOUtil.java @@ -15,7 +15,7 @@ import java.io.InputStreamReader; import java.io.OutputStreamWriter; @Keep -final class IOUtil { +public final class IOUtil { /** * Writes UTF-8 output data to an output stream. @@ -25,7 +25,7 @@ final class IOUtil { * @param data Data to write * @throws IOException Thrown if the output stream is unavailable, or encoding the data fails */ - static void writeUTF8Stream(OutputStream stream, String data) throws IOException { + public static void writeUTF8Stream(OutputStream stream, String data) throws IOException { try (BufferedWriter writer = new BufferedWriter(new OutputStreamWriter(stream, "UTF-8"))) { writer.write(data); } @@ -39,7 +39,7 @@ final class IOUtil { * @return All data received from the stream * @throws IOException Thrown if the input stream is unavailable, or decoding the data fails */ - static String readUTF8Stream(InputStream stream) throws IOException { + public static String readUTF8Stream(InputStream stream) throws IOException { StringBuilder stringBuilder = new StringBuilder(); try (BufferedReader reader = new BufferedReader(new InputStreamReader(stream, "UTF-8"))) { String line; diff --git a/Android/samples/sdksample/sampleAccountProviders/src/main/java/com/microsoft/connecteddevices/sampleaccountproviders/MSAAccountProvider.java b/Android/samples/sdksample/sampleAccountProviders/src/main/java/com/microsoft/connecteddevices/sampleaccountproviders/MSAAccountProvider.java index aad8288..e70fab4 100644 --- a/Android/samples/sdksample/sampleAccountProviders/src/main/java/com/microsoft/connecteddevices/sampleaccountproviders/MSAAccountProvider.java +++ b/Android/samples/sdksample/sampleAccountProviders/src/main/java/com/microsoft/connecteddevices/sampleaccountproviders/MSAAccountProvider.java @@ -41,6 +41,8 @@ import java.io.UnsupportedEncodingException; import java.net.HttpURLConnection; import java.net.URL; import java.net.URLEncoder; +import java.util.ArrayList; +import java.util.Arrays; import java.util.LinkedList; import java.util.List; import java.util.Map; @@ -107,14 +109,14 @@ public final class MSAAccountProvider implements UserAccountProvider, MSATokenCa // CDP's SDK currently requires authorization for all features, otherwise platform initialization will fail. // As such, the user must sign in/consent for the following scopes. This may change to become more modular in the future. - private static final String[] REQUIRED_SCOPES = { + private static final String[] KNOWN_SCOPES = { + "wl.offline_access", // read and update user info at any time "ccs.ReadWrite", // device commanding scope "dds.read", // device discovery scope (discover other devices) "dds.register", // device discovery scope (allow discovering this device) - "wns.connect", // notification scope - "wl.offline_access", // read and update user info at any time - "https://activity.windows.com/UserActivity.ReadWrite.CreatedByApp", // user activities scope - "asimovrome.telemetry" // asimov token scope + "wns.connect", // push notification scope + "asimovrome.telemetry", // asimov token scope + "https://activity.windows.com/UserActivity.ReadWrite.CreatedByApp", // default useractivities scope }; // OAuth URLs @@ -125,6 +127,7 @@ public final class MSAAccountProvider implements UserAccountProvider, MSATokenCa // region Member Variables private final String mClientId; + private final Map mScopeOverrideMap; private UserAccount mAccount = null; private MSATokenCache mTokenCache; private boolean mSignInSignOutInProgress; @@ -135,11 +138,13 @@ public final class MSAAccountProvider implements UserAccountProvider, MSATokenCa // region Constructor /** - * @param clientId id of the app's registration in the MSA portal + * @param clientId id of the app's registration in the MSA portal + * @param scopeOverrides scope overrides for the app * @param context */ - public MSAAccountProvider(String clientId, Context context) { + public MSAAccountProvider(String clientId, final Map scopeOverrides, Context context) { mClientId = clientId; + mScopeOverrideMap = scopeOverrides; mTokenCache = new MSATokenCache(clientId, context); mTokenCache.addListener(new MSATokenCache.Listener() { @Override @@ -158,6 +163,22 @@ public final class MSAAccountProvider implements UserAccountProvider, MSATokenCa // endregion // region Private Helpers + private List getAuthScopes(final String[] incoming) { + ArrayList authScopes = new ArrayList(); + + for (String scope : incoming) { + if (mScopeOverrideMap.containsKey(scope)) { + for (String replacement : mScopeOverrideMap.get(scope)) { + authScopes.add(replacement); + } + } else { + authScopes.add(scope); + } + } + + return authScopes; + } + private AsyncOperation notifyListenersAsync() { return AsyncOperation.supplyAsync(new AsyncOperation.Supplier() { @Override @@ -240,7 +261,7 @@ public final class MSAAccountProvider implements UserAccountProvider, MSATokenCa } final String signInUrl = AUTHORIZE_URL + "?redirect_uri=" + REDIRECT_URL + "&response_type=code&client_id=" + mClientId + - "&scope=" + TextUtils.join("+", REQUIRED_SCOPES); + "&scope=" + TextUtils.join("+", getAuthScopes(KNOWN_SCOPES)); final AsyncOperation authCodeOperation = new AsyncOperation<>(); final AsyncOperation signInOperation = new AsyncOperation<>(); mSignInSignOutInProgress = true; @@ -396,7 +417,7 @@ public final class MSAAccountProvider implements UserAccountProvider, MSATokenCa public synchronized AsyncOperation getAccessTokenForUserAccountAsync(final String accountId, final String[] scopes) { if (mAccount != null && accountId != null && accountId.equals(mAccount.getId()) && scopes.length > 0) { - final String scope = TextUtils.join(" ", scopes); + final String scope = TextUtils.join(" ", getAuthScopes(scopes)); return mTokenCache.getAccessTokenAsync(scope).thenComposeAsync( new AsyncOperation.ResultFunction>() { @@ -405,7 +426,6 @@ public final class MSAAccountProvider implements UserAccountProvider, MSATokenCa if (accessToken != null) { // token already exists in the cache, can early return return AsyncOperation.completedFuture(new AccessTokenResult(AccessTokenRequestStatus.SUCCESS, accessToken)); - } else { // token does not yet exist in the cache, need to request a new one return requestNewAccessTokenAsync(scope);