Merged PR 9211: Prepare Android samples for 0.12 release
Prepare Android samples for 0.12 release
This commit is contained in:
Родитель
75f8b90422
Коммит
5ba15314ff
|
@ -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 <fields>;
|
||||
}
|
||||
-keepclasseswithmembers class * {
|
||||
@android.support.annotation.Keep <methods>;
|
||||
}
|
||||
|
||||
-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 <methods>;
|
||||
}
|
||||
|
||||
-keep class com.microsoft.connecteddevices.RomeException {
|
||||
*;
|
||||
}
|
|
@ -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'
|
||||
}
|
||||
|
|
|
@ -1,2 +0,0 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<manifest package="com.microsoft.connecteddevices.sampleaccountproviders" />
|
|
@ -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<Long, EventListener<UserAccountProvider, Void>> 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<TokenCacheItem> 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<Void> notifyListenersAsync() {
|
||||
return AsyncOperation.supplyAsync(new AsyncOperation.Supplier<Void>() {
|
||||
@Override
|
||||
public Void get() {
|
||||
for (EventListener<UserAccountProvider, Void> 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<Boolean> signIn() throws IllegalStateException {
|
||||
if (isSignedIn()) {
|
||||
throw new IllegalStateException("AADAccountProvider: Already signed in!");
|
||||
}
|
||||
|
||||
final AsyncOperation<Boolean> 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<AuthenticationResult>() {
|
||||
@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<Boolean>() {
|
||||
@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<AccessTokenResult> 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<AccessTokenResult> ret = new AsyncOperation<>();
|
||||
mAuthContext.acquireTokenSilentAsync(scopes[0], mClientId, mAccount.getId(), new AuthenticationCallback<AuthenticationResult>() {
|
||||
@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<AuthenticationResult> 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<UserAccountProvider, Void> 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.");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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<UserAccountProvider, Void> mListener;
|
||||
|
||||
private final Map<Long, EventListener<UserAccountProvider, Void>> 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<UserAccountProvider, Void>() {
|
||||
@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<Void> notifyListenersAsync() {
|
||||
return AsyncOperation.supplyAsync(new AsyncOperation.Supplier<Void>() {
|
||||
@Override
|
||||
public Void get() {
|
||||
for (EventListener<UserAccountProvider, Void> 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<Boolean> 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<Boolean> 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<AccessTokenResult> getAccessTokenForUserAccountAsync(
|
||||
final String userAccountId, final String[] scopes) {
|
||||
UserAccountProvider provider = AADMSAAccountProvider.this.getSignedInProvider();
|
||||
if (provider != null) {
|
||||
return provider.getAccessTokenForUserAccountAsync(userAccountId, scopes);
|
||||
}
|
||||
|
||||
AsyncOperation<AccessTokenResult> ret = new AsyncOperation<AccessTokenResult>();
|
||||
ret.completeExceptionally(new IllegalStateException("Not currently signed in!"));
|
||||
return ret;
|
||||
}
|
||||
|
||||
@Override
|
||||
public synchronized long addUserAccountChangedListener(EventListener<UserAccountProvider, Void> 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!");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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<Long, EventListener<UserAccountProvider, Void>> 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<Void> notifyListenersAsync() {
|
||||
return AsyncOperation.supplyAsync(new AsyncOperation.Supplier<Void>() {
|
||||
@Override
|
||||
public Void get() {
|
||||
for (EventListener<UserAccountProvider, Void> 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<AccessTokenResult> 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<String, AsyncOperation<MSATokenRequest.Result>>() {
|
||||
@Override
|
||||
public AsyncOperation<MSATokenRequest.Result> apply(String refreshToken) {
|
||||
return MSATokenRequest.requestAsync(mClientId, MSATokenRequest.GrantType.REFRESH, scope, null, refreshToken);
|
||||
}
|
||||
})
|
||||
.thenApplyAsync(new AsyncOperation.ResultFunction<MSATokenRequest.Result, AccessTokenResult>() {
|
||||
@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<Boolean> 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<String> authCodeOperation = new AsyncOperation<>();
|
||||
final AsyncOperation<Boolean> 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<String, AsyncOperation<MSATokenRequest.Result>>() {
|
||||
@Override
|
||||
public AsyncOperation<MSATokenRequest.Result> apply(String authCode) {
|
||||
return MSATokenRequest.requestAsync(mClientId, MSATokenRequest.GrantType.CODE, null, REDIRECT_URL, authCode);
|
||||
}
|
||||
})
|
||||
.thenAcceptAsync(new AsyncOperation.ResultConsumer<MSATokenRequest.Result>() {
|
||||
@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<AccessTokenResult> 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<String, AsyncOperation<AccessTokenResult>>() {
|
||||
@Override
|
||||
public AsyncOperation<AccessTokenResult> 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<UserAccountProvider, Void> 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
|
||||
}
|
|
@ -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<String> 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<String> _getTokenAsyncInternal(final AsyncOperation<String> 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<String, AsyncOperation<MSATokenRequest.Result>>() {
|
||||
@Override
|
||||
public AsyncOperation<MSATokenRequest.Result> apply(String refreshToken) {
|
||||
return mRefreshRequest.requestAsync(refreshToken);
|
||||
}
|
||||
})
|
||||
.thenAcceptAsync(new AsyncOperation.ResultConsumer<MSATokenRequest.Result>() {
|
||||
@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<String> getTokenAsync() {
|
||||
AsyncOperation<String> ret = new AsyncOperation<String>();
|
||||
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<String> 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<String, MSATokenCacheItem> mCachedAccessTokens = new ArrayMap<>();
|
||||
|
||||
private final Collection<Listener> 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<String> getRefreshTokenAsync() {
|
||||
if (mCachedRefreshToken != null) {
|
||||
return mCachedRefreshToken.getTokenAsync();
|
||||
} else {
|
||||
return AsyncOperation.completedFuture(null);
|
||||
}
|
||||
}
|
||||
|
||||
public synchronized AsyncOperation<String> 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<String> 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();
|
||||
}
|
||||
}
|
|
@ -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<Pair<String, String>> params) throws UnsupportedEncodingException {
|
||||
StringBuilder queryStringBuilder = new StringBuilder();
|
||||
boolean isFirstParam = true;
|
||||
for (Pair<String, String> 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<MSATokenRequest.Result> 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<MSATokenRequest.Result>() {
|
||||
@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<Pair<String, String>> 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<MSATokenRequest.Result> requestAsync(String token) {
|
||||
return requestAsync(mClientId, mGrantType, mScope, mRedirectUri, token);
|
||||
}
|
||||
}
|
|
@ -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<String, String[]> msaScopeOverrides, String aadClientId, String aadRedirectUri, Context context) {
|
||||
|
||||
// Chain the inner events to the event provided by this helper
|
||||
mListener = new EventListener<UserAccountProvider, Void>() {
|
||||
|
@ -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()) {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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<String, String[]> 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<String, String[]> 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<String> getAuthScopes(final String[] incoming) {
|
||||
ArrayList<String> authScopes = new ArrayList<String>();
|
||||
|
||||
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<Void> notifyListenersAsync() {
|
||||
return AsyncOperation.supplyAsync(new AsyncOperation.Supplier<Void>() {
|
||||
@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<String> authCodeOperation = new AsyncOperation<>();
|
||||
final AsyncOperation<Boolean> signInOperation = new AsyncOperation<>();
|
||||
mSignInSignOutInProgress = true;
|
||||
|
@ -396,7 +417,7 @@ public final class MSAAccountProvider implements UserAccountProvider, MSATokenCa
|
|||
public synchronized AsyncOperation<AccessTokenResult> 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<String, AsyncOperation<AccessTokenResult>>() {
|
||||
|
@ -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);
|
||||
|
|
|
@ -2,6 +2,7 @@
|
|||
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
|
||||
package="com.microsoft.rome.onesdksample_android">
|
||||
<application
|
||||
android:name=".StaticContextApp"
|
||||
android:allowBackup="true"
|
||||
android:icon="@mipmap/ic_launcher"
|
||||
android:label="@string/app_name"
|
||||
|
|
|
@ -6,6 +6,7 @@ package com.microsoft.rome.onesdksample_android;
|
|||
|
||||
import android.app.Activity;
|
||||
import android.content.Context;
|
||||
import android.util.ArrayMap;
|
||||
import android.util.Log;
|
||||
|
||||
import com.microsoft.connecteddevices.base.AsyncOperation;
|
||||
|
@ -22,8 +23,8 @@ public class AccountProviderBroker {
|
|||
|
||||
public AccountProviderBroker(Context context) {
|
||||
// Create sign-in helper from helper lib, which does user account and access token management for us
|
||||
// Takes two parameters: a client id for msa, and a client id for aad, which are just strings we register with
|
||||
mSignInHelper = new MSAAccountProvider(Secrets.MSA_CLIENT_ID, context);
|
||||
// Takes three parameters: a client id for msa, a map of requested auto scopes to override, and the context
|
||||
mSignInHelper = new MSAAccountProvider(Secrets.MSA_CLIENT_ID, new ArrayMap<String, String[]>(), context);
|
||||
}
|
||||
|
||||
public void signIn(Activity activity, AsyncOperation.ResultBiConsumer<Boolean, Throwable> signInCompletionHandler) {
|
||||
|
|
|
@ -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<PlatformCreationResult> 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<PlatformCreationResult, Throwable>() {
|
||||
raiseToast("Completed Rome initialization, starting registration...");
|
||||
|
||||
ArrayList<AppServiceProvider> appServiceProviders = new ArrayList<>();
|
||||
appServiceProviders.add(new PingPongService(this));
|
||||
appServiceProviders.add(new EchoService(this));
|
||||
|
||||
PlatformBroker.register(this, appServiceProviders, new SimpleLaunchHandler(this), new EventListener<UserAccount, CloudRegistrationStatus>() {
|
||||
@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.
|
||||
*/
|
||||
|
|
|
@ -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<PlatformCreationResult> 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<AppServiceProvider> appServiceProviders, LaunchUriProvider launchUriProvider, EventListener<UserAccount, CloudRegistrationStatus> 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;
|
||||
}
|
||||
|
|
|
@ -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 = "<<Google Cloud Messaging sender ID goes here>>";
|
||||
|
||||
// Your application host name from: https://apps.dev.microsoft.com/
|
||||
static final String APP_HOST_NAME = "<<App host name goes here>>";
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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<UserDataFeed, Void> 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<UserDataFeed, Void>() {
|
||||
@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() {
|
||||
|
|
|
@ -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 ArrayAdapter<UserActivitySessionHis
|
|||
|
||||
if (history != null) {
|
||||
UserActivity activity = history.getUserActivity();
|
||||
TextView id = (TextView)convertView.findViewById(R.id.activity_id);
|
||||
|
||||
TextView id = convertView.findViewById(R.id.activity_id);
|
||||
id.setText(activity.getActivityId().trim());
|
||||
TextView displayText = (TextView)convertView.findViewById(R.id.activity_displaytext);
|
||||
|
||||
TextView displayText = convertView.findViewById(R.id.activity_displaytext);
|
||||
displayText.setText(activity.getVisualElements().getDisplayText());
|
||||
TextView activationUri = (TextView)convertView.findViewById(R.id.activity_activationuri);
|
||||
|
||||
TextView activationUri = convertView.findViewById(R.id.activity_activationuri);
|
||||
activationUri.setText(activity.getActivationUri());
|
||||
TextView activationIconUri = (TextView)convertView.findViewById(R.id.activity_activationiconuri);
|
||||
activationIconUri.setText(activity.getVisualElements().getAttribution().getIconUri());
|
||||
TextView start = (TextView)convertView.findViewById(R.id.activity_start);
|
||||
|
||||
String iconUri = "";
|
||||
UserActivityAttribution attribution = activity.getVisualElements().getAttribution();
|
||||
if (attribution != null) {
|
||||
iconUri = attribution.getIconUri();
|
||||
}
|
||||
TextView activationIconUri = convertView.findViewById(R.id.activity_activationiconuri);
|
||||
activationIconUri.setText(iconUri);
|
||||
|
||||
TextView start = convertView.findViewById(R.id.activity_start);
|
||||
start.setText(history.getStartTime().toString());
|
||||
TextView end = (TextView)convertView.findViewById(R.id.activity_end);
|
||||
end.setText(history.getEndTime().toString());
|
||||
|
||||
String endTimeValue = "";
|
||||
Date endTime = history.getEndTime();
|
||||
if (endTime != null) {
|
||||
endTimeValue = endTime.toString();
|
||||
}
|
||||
TextView end = convertView.findViewById(R.id.activity_end);
|
||||
end.setText(endTimeValue);
|
||||
}
|
||||
|
||||
return convertView;
|
||||
|
|
|
@ -71,6 +71,7 @@
|
|||
<string name="status_activities_initialize_pending">UserActivityFeed not yet initialized. Please wait…</string>
|
||||
<string name="status_activities_initialize">Initializing UserActivityFeed…</string>
|
||||
<string name="status_activities_initialize_complete">UserActivityFeed initialized</string>
|
||||
<string name="status_activities_initialize_failed">UserActivityFeed initialization failed</string>
|
||||
<string name="status_activities_create_activity">Creating UserActivity…</string>
|
||||
<string name="status_activities_get_channel">Getting UserActivityChannel…</string>
|
||||
<string name="status_activities_get_channel_success">Got UserActivityChannel successfully</string>
|
||||
|
|
|
@ -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'
|
||||
}
|
||||
|
|
|
@ -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<String, String[]> msaScopeOverrides, String aadClientId, String aadRedirectUri, Context context) {
|
||||
|
||||
// Chain the inner events to the event provided by this helper
|
||||
mListener = new EventListener<UserAccountProvider, Void>() {
|
||||
|
@ -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()) {
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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<String, String[]> 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<String, String[]> 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<String> getAuthScopes(final String[] incoming) {
|
||||
ArrayList<String> authScopes = new ArrayList<String>();
|
||||
|
||||
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<Void> notifyListenersAsync() {
|
||||
return AsyncOperation.supplyAsync(new AsyncOperation.Supplier<Void>() {
|
||||
@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<String> authCodeOperation = new AsyncOperation<>();
|
||||
final AsyncOperation<Boolean> signInOperation = new AsyncOperation<>();
|
||||
mSignInSignOutInProgress = true;
|
||||
|
@ -396,7 +417,7 @@ public final class MSAAccountProvider implements UserAccountProvider, MSATokenCa
|
|||
public synchronized AsyncOperation<AccessTokenResult> 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<String, AsyncOperation<AccessTokenResult>>() {
|
||||
|
@ -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);
|
||||
|
|
Загрузка…
Ссылка в новой задаче