Skip to content

Replaced Volley calls with Graph SDK #50

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 5 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions app/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,9 @@ android {
versionCode 1
versionName "1.0"
}
packagingOptions {
pickFirst 'META-INF/*'
}
signingConfigs {
debug {
storeFile file("../gradle/debug.keystore")
Expand Down Expand Up @@ -78,6 +81,10 @@ android {
setIgnore(true);
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_1_8
targetCompatibility JavaVersion.VERSION_1_8
}
}

dependencies {
Expand All @@ -87,6 +94,9 @@ dependencies {
implementation "androidx.constraintlayout:constraintlayout:$rootProject.ext.constraintLayoutVersion"
implementation "androidx.legacy:legacy-support-v4:$rootProject.ext.legacySupportV4Version"
implementation 'com.android.volley:volley:1.1.1'
implementation ('com.microsoft.graph:microsoft-graph:3.1.0') {
exclude group: 'javax.activation'
}

if (findProject(':msal') != null) {
// For developer team only.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,71 +24,76 @@
package com.azuresamples.msalandroidapp;

import android.content.Context;
import android.os.Build;
import android.util.Log;

import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;

import com.android.volley.DefaultRetryPolicy;
import com.android.volley.Request;
import com.android.volley.RequestQueue;
import com.android.volley.Response;
import com.android.volley.toolbox.JsonObjectRequest;
import com.android.volley.toolbox.Volley;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.microsoft.graph.requests.GraphServiceClient;

import org.json.JSONObject;

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.CompletableFuture;

public class MSGraphRequestWrapper {
private static final String TAG = MSGraphRequestWrapper.class.getSimpleName();

// See: https://docs.microsoft.com/en-us/graph/deployments#microsoft-graph-and-graph-explorer-service-root-endpoints
public static final String MS_GRAPH_ROOT_ENDPOINT = "https://graph.microsoft.com/";
public static final String MS_GRAPH_V1_ENDPOINT = MS_GRAPH_ROOT_ENDPOINT + "v1.0";
public static final String MS_GRAPH_BETA_ENDPOINT = MS_GRAPH_ROOT_ENDPOINT + "beta";

/**
* Use Volley to make an HTTP request with
* Use Graph SDK to make an HTTP request with
* 1) a given MSGraph resource URL
* 2) an access token
* to obtain MSGraph data.
**/
public static void callGraphAPIUsingVolley(@NonNull final Context context,
@NonNull final String graphResourceUrl,
@NonNull final String accessToken,
@NonNull final Response.Listener<JSONObject> responseListener,
@NonNull final Response.ErrorListener errorListener) {
Log.d(TAG, "Starting volley request to graph");
@RequiresApi(api = Build.VERSION_CODES.N)
@SuppressWarnings("unchecked")
public static CompletableFuture<String> callGraphAPI(@NonNull final String graphResourceUrl,
@NonNull final String accessToken) {
Log.d(TAG, "Starting SDK request to graph");

/* Make sure we have a token to send to graph */
if (accessToken == null || accessToken.length() == 0) {
return;
return CompletableFuture.completedFuture(null);
}

RequestQueue queue = Volley.newRequestQueue(context);
JSONObject parameters = new JSONObject();
StaticTokenAuthProvider authProvider =
new StaticTokenAuthProvider(accessToken);

try {
parameters.put("key", "value");
} catch (Exception e) {
Log.d(TAG, "Failed to put parameters: " + e.toString());
}
GraphServiceClient graphClient = GraphServiceClient.builder()
.authenticationProvider(authProvider)
.buildClient();

JsonObjectRequest request = new JsonObjectRequest(Request.Method.GET, graphResourceUrl,
parameters, responseListener, errorListener) {
@Override
public Map<String, String> getHeaders() {
Map<String, String> headers = new HashMap<>();
headers.put("Authorization", "Bearer " + accessToken);
Copy link
Contributor

@rpdome rpdome Apr 1, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The purpose of doing this is to show how exactly the token can be used to access any resources
(We picked Graph because Graph's User.Read is accessible to every accounts).

This change would encapsulate that.

I'd suggest we have an extra page (fragment) for Graph SDK, and you're free to bring in all the best practices with the Graph SDK in that one.

Copy link
Contributor

@rpdome rpdome Apr 1, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If all they need to access MSGraph, and Graph SDK already wraps around the authentication part, I'd say we probably won't even need MSAL in that page

or you could have a demo of both - with, and without MSAL like what you did here

cc @hamiltonha

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Graph SDK does not "do" authentication - it relies on an auth provider. It can use the provider from the Azure Identity SDK, but that SDK isn't supported in Android. So, I wrote a custom provider that wraps the token you guys already get from MSAL.

My goal here was not to undo any of the token acquisition work you've already done, just to remove the HTTP calls to Graph (which we do not want to encourage). cc @baywet

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@rpdome I think there are multiple conflicting goals here:

  • Showcase how to call a custom API with native http client
  • Showcase how to call Microsoft Graph with code that is closer to real life for customers

We, as Microsoft, don't want to encourage people to call Microsoft Graph with a native client. Of course they technically can do it, but they shouldn't as they as loosing a lot of value from the different handlers to the types and the fluent APIs. Not using the SDK to call Microsoft Graph makes their applications less resilient and is not what is recommended.

Now, I understand you also want to showcase calling an API using the native HTTP client, this is fine, just pick a different API that doesn't have an SDK, or send the request to localhost with a mock.

I hope that makes sense and we can merge this PR in? We're on the identity call tomorrow and it'd be great if things were aligned.

Copy link
Contributor

@rpdome rpdome Apr 8, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In that case, what about having both options in the code, but comment out the manual part?

We can explicitly call out something along the line of "If you're going to use MSGraph in your app, Please use the SDK. The manual way is kept here - in case you're trying to call other APIs"

We don't want to mock the request - because we still want the customer to 'play with the real thing' - we want to let them know that if they want to do it manually (if you don't have other options), here is how.

This sample app is supposed to be a playground after all, and MSGraph is the only SDK that are available to everyone by default.

return headers;
}
};
String relativeUrl = "";

Log.d(TAG, "Adding HTTP GET to Queue, Request: " + request.toString());
if (graphResourceUrl.toLowerCase().contains(MS_GRAPH_V1_ENDPOINT)) {
relativeUrl = graphResourceUrl.substring(MS_GRAPH_V1_ENDPOINT.length());
} else if (graphResourceUrl.toLowerCase().contains(MS_GRAPH_BETA_ENDPOINT)) {
relativeUrl = graphResourceUrl.substring(MS_GRAPH_BETA_ENDPOINT.length());
}

request.setRetryPolicy(new DefaultRetryPolicy(
3000,
DefaultRetryPolicy.DEFAULT_MAX_RETRIES,
DefaultRetryPolicy.DEFAULT_BACKOFF_MULT));
queue.add(request);
// The customRequest builder allows you to call any relative URL
// (like "/me") and get back a generic object
// For an example that uses the SDK's fluent API to return strongly-typed objects,
// see https://github.com/microsoftgraph/msgraph-training-android
// or https://docs.microsoft.com/en-us/graph/tutorials/android
return graphClient.customRequest(relativeUrl)
.buildRequest()
.getAsync()
.thenApply(response -> response.toString());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,11 @@

package com.azuresamples.msalandroidapp;

import android.os.Build;
import android.os.Bundle;

import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
import androidx.fragment.app.Fragment;

import android.util.Log;
Expand Down Expand Up @@ -230,6 +232,7 @@ public void onError(MsalException exception) {
private SilentAuthenticationCallback getAuthSilentCallback() {
return new SilentAuthenticationCallback() {

@RequiresApi(api = Build.VERSION_CODES.N)
@Override
public void onSuccess(IAuthenticationResult authenticationResult) {
Log.d(TAG, "Successfully authenticated");
Expand Down Expand Up @@ -304,26 +307,20 @@ public void onCancel() {
* If you're developing an app for sovereign cloud users, please change the Microsoft Graph Resource URL accordingly.
* https://docs.microsoft.com/en-us/graph/deployments#microsoft-graph-and-graph-explorer-service-root-endpoints
*/
@RequiresApi(api = Build.VERSION_CODES.N)
private void callGraphAPI(final IAuthenticationResult authenticationResult) {
MSGraphRequestWrapper.callGraphAPIUsingVolley(
getContext(),
graphResourceTextView.getText().toString(),
authenticationResult.getAccessToken(),
new Response.Listener<JSONObject>() {
@Override
public void onResponse(JSONObject response) {
/* Successfully called graph, process data and send to UI */
Log.d(TAG, "Response: " + response.toString());
displayGraphResult(response);
}
},
new Response.ErrorListener() {
@Override
public void onErrorResponse(VolleyError error) {
Log.d(TAG, "Error: " + error.toString());
displayError(error);
}
});
MSGraphRequestWrapper.callGraphAPI(
graphResourceTextView.getText().toString(),
authenticationResult.getAccessToken())
.thenAccept(json -> {
Log.d(TAG, "Response: " + json);
displayGraphResult(json);
})
.exceptionally(exception -> {
Log.d(TAG, "Error: " + exception.toString());
displayError(exception);
return null;
});
}

//
Expand All @@ -338,14 +335,14 @@ public void onErrorResponse(VolleyError error) {
/**
* Display the graph response
*/
private void displayGraphResult(@NonNull final JSONObject graphResponse) {
logTextView.setText(graphResponse.toString());
private void displayGraphResult(@NonNull final String graphResponse) {
logTextView.setText(graphResponse);
}

/**
* Display the error message
*/
private void displayError(@NonNull final Exception exception) {
private void displayError(@NonNull final Throwable exception) {
logTextView.setText(exception.toString());
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,12 @@

package com.azuresamples.msalandroidapp;

import android.os.Build;
import android.os.Bundle;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.RequiresApi;
import androidx.fragment.app.Fragment;

import android.util.Log;
Expand Down Expand Up @@ -257,6 +259,7 @@ public void onError(@NonNull MsalException exception) {
private SilentAuthenticationCallback getAuthSilentCallback() {
return new SilentAuthenticationCallback() {

@RequiresApi(api = Build.VERSION_CODES.N)
@Override
public void onSuccess(IAuthenticationResult authenticationResult) {
Log.d(TAG, "Successfully authenticated");
Expand Down Expand Up @@ -290,6 +293,7 @@ public void onError(MsalException exception) {
private AuthenticationCallback getAuthInteractiveCallback() {
return new AuthenticationCallback() {

@RequiresApi(api = Build.VERSION_CODES.N)
@Override
public void onSuccess(IAuthenticationResult authenticationResult) {
/* Successfully got a token, use it to call a protected resource - MSGraph */
Expand Down Expand Up @@ -328,26 +332,20 @@ public void onCancel() {
/**
* Make an HTTP request to obtain MSGraph data
*/
@RequiresApi(api = Build.VERSION_CODES.N)
private void callGraphAPI(final IAuthenticationResult authenticationResult) {
MSGraphRequestWrapper.callGraphAPIUsingVolley(
getContext(),
graphResourceTextView.getText().toString(),
authenticationResult.getAccessToken(),
new Response.Listener<JSONObject>() {
@Override
public void onResponse(JSONObject response) {
/* Successfully called graph, process data and send to UI */
Log.d(TAG, "Response: " + response.toString());
displayGraphResult(response);
}
},
new Response.ErrorListener() {
@Override
public void onErrorResponse(VolleyError error) {
Log.d(TAG, "Error: " + error.toString());
displayError(error);
}
});
MSGraphRequestWrapper.callGraphAPI(
graphResourceTextView.getText().toString(),
authenticationResult.getAccessToken())
.thenAccept(json -> {
Log.d(TAG, "Response: " + json);
displayGraphResult(json);
})
.exceptionally(exception -> {
Log.d(TAG, "Error: " + exception.toString());
displayError(exception);
return null;
});
}

//
Expand All @@ -362,14 +360,14 @@ public void onErrorResponse(VolleyError error) {
/**
* Display the graph response
*/
private void displayGraphResult(@NonNull final JSONObject graphResponse) {
logTextView.setText(graphResponse.toString());
private void displayGraphResult(@NonNull final String graphResponse) {
logTextView.setText(graphResponse);
}

/**
* Display the error message
*/
private void displayError(@NonNull final Exception exception) {
private void displayError(@NonNull final Throwable exception) {
logTextView.setText(exception.toString());
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
// Copyright (c) Microsoft Corporation.
// All rights reserved.
//
// This code is licensed under the MIT License.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files(the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and / or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions :
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
// THE SOFTWARE.

package com.azuresamples.msalandroidapp;

import android.os.Build;

import androidx.annotation.RequiresApi;

import com.microsoft.graph.authentication.BaseAuthenticationProvider;

import java.net.URL;
import java.util.concurrent.CompletableFuture;

import javax.annotation.Nonnull;

/**
* Implementation sample for a simplistic authentication provider
* <p>
* The Microsoft Graph SDK client accepts an authentication provider
* that is responsible for providing access tokens to authenticate API
* calls. Because this sample focuses on acquiring tokens manually, this
* provider simply returns the provided token.
* <p>
* Real-world providers would take advantage of MSAL's caching abilities
* to always get a valid token, refresh the token when expired, etc.
*/
public class StaticTokenAuthProvider extends BaseAuthenticationProvider {

private final String mAccessToken;

public StaticTokenAuthProvider(String accessToken) {
mAccessToken = accessToken;
}

@RequiresApi(api = Build.VERSION_CODES.N)
@Nonnull
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
@Nonnull
@Nullable

Since this method can return null

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm - maybe it would be better to return CompletableFuture.completedFuture(""). @baywet what would the SDK handle more gracefully?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done

@Override
public CompletableFuture<String> getAuthorizationTokenAsync(@Nonnull URL requestUrl) {
if (shouldAuthenticateRequestWithUrl(requestUrl)) {
return CompletableFuture.completedFuture(mAccessToken);
}
return CompletableFuture.completedFuture(null);
}
}
4 changes: 2 additions & 2 deletions gradle/versions.gradle
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
// Variables for entire project
ext {
// SDK
minSdkVersion = 16
minSdkVersion = 26
automationAppMinSDKVersion = 21
targetSdkVersion = 28
compileSdkVersion = 28
buildToolsVersion = "28.0.3"

// Plugins
gradleVersion = '4.0.2'
gradleVersion = '4.1.3'
androidMavenGradlePluginVersion = "1.4.1"

// Libraries
Expand Down