Merged PR 9211: Prepare Android samples for 0.12 release

Prepare Android samples for 0.12 release
This commit is contained in:
Nate Peterson 2018-06-20 21:22:02 +00:00
Родитель 75f8b90422
Коммит 5ba15314ff
25 изменённых файлов: 337 добавлений и 1871 удалений

Просмотреть файл

@ -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);