diff --git a/sdk/communication/azure-communication-common/CHANGELOG.md b/sdk/communication/azure-communication-common/CHANGELOG.md new file mode 100644 index 000000000..08a0a01a4 --- /dev/null +++ b/sdk/communication/azure-communication-common/CHANGELOG.md @@ -0,0 +1,39 @@ +# Release History + +## 1.0.0-beta.6 (Unreleased) +### Breaking Changes +- Removed `CommunicationTokenCredential(Callable tokenRefresher)`, ` CommunicationTokenCredential(Callable tokenRefresher, String initialToken)`, `CommunicationTokenCredential(Callable tokenRefresher, boolean refreshProactively)`, `CommunicationTokenCredential(Callable tokenRefresher, boolean refreshProactively, String initialToken)`, and added `CommunicationTokenCredential(CommunicationTokenRefreshOptions tokenRefreshOptions)` + +## 1.0.0-beta.5 (2021-02-08) +### Breaking Changes +- Removed 'CallingApplicationIdentifier'. +- Removed 'getId' method in 'CommunicationIdentifier' class. + +### New Features +- Added a new 'MicrosoftTeamsUserIdentifier' constructor that takes a non-null CommunicationCloudEnvironment parameter. +- Added class 'CommunicationCloudEnvironment'. +- Added class 'CommunicationCloudEnvironmentModel'. +- Added class 'CommunicationIdentifierSerializer'. +- Added class 'CommunicationIdentifierModel'. +- Added class 'MicrosoftTeamsUserIdentifierModel'. +- Added class 'PhoneNumberIdentifierModel'. +- Added class 'CommunicationUserIdentifierModel'. + +## 1.0.0-beta.4 (2021-01-28) +### Breaking Changes +- Renamed `CommunicationUserCredential` to `CommunicationTokenCredential` +- Renamed `PhoneNumber` to `PhoneNumberIdentifier` +- Renamed `CommunicationUser` to `CommunicationUserIdentifier ` +- Renamed `CallingApplication` to `CallingApplicationIdentifier` + +### Added +- Added class `MicrosoftTeamsUserIdentifier` + +## 1.0.0-beta.1 (2020-09-22) +This package contains common code for Azure Communication Service libraries. For more information, please see the [README][read_me] and [documentation][documentation]. + +This is a Public Preview version, so breaking changes are possible in subsequent releases as we improve the product. To provide feedback, please submit an issue in our [Azure SDK for Java GitHub repo](https://github.com/Azure/azure-sdk-for-java/issues). + + +[read_me]: https://github.com/Azure/azure-sdk-for-android/blob/master/sdk/communication/azure-communication-common/README.md +[documentation]: https://docs.microsoft.com/azure/communication-services/quickstarts/chat/get-started?pivots=programming-language-java diff --git a/sdk/communication/azure-communication-common/README.md b/sdk/communication/azure-communication-common/README.md new file mode 100644 index 000000000..cd11a8f8f --- /dev/null +++ b/sdk/communication/azure-communication-common/README.md @@ -0,0 +1,154 @@ +# Azure Communication Common client library for Android + +This package contains common code for Azure Communication Service libraries. + +[Source code](https://github.com/Azure/azure-sdk-for-android/tree/master/sdk/communication/azure-communication-common) +| [API reference documentation](https://azure.github.io/azure-sdk-for-android/sdk/communication/azure-communication-common/azure-communication-common/index.html) +| [Product documentation](https://docs.microsoft.com/azure/communication-services/overview) + +## Getting started + +### Prerequisites +* You must have an [Azure subscription](https://azure.microsoft.com/free/) and a + [Communication Services resource](https://docs.microsoft.com/azure/communication-services/quickstarts/create-communication-resource) to use this library. +* The client libraries natively target Android API level 21. Your application's `minSdkVersion` must be set to 21 or + higher to use this library. +* The library is written in Java 8. Your application must be built with Android Gradle Plugin 3.0.0 or later, and must + be configured to + [enable Java 8 language desugaring](https://developer.android.com/studio/write/java8-support.html#supported_features) + to use this library. Java 8 language features that require a target API level >21 are not used, nor are any Java 8+ + APIs that would require the Java 8+ API desugaring provided by Android Gradle plugin 4.0.0. + +### Versions available +The current version of this library is **1.0.0-beta.5**. + +> Note: The SDK is currently in **beta**. The API surface and feature sets are subject to change at any time before they become generally available. We do not currently recommend them for production use. + +### Install the library +To install the Azure client libraries for Android, add them as dependencies within your +[Gradle](#add-a-dependency-with-gradle) or +[Maven](#add-a-dependency-with-maven) build scripts. + +#### Add a dependency with Gradle +To import the library into your project using the [Gradle](https://gradle.org/) build system, follow the instructions in [Add build dependencies](https://developer.android.com/studio/build/dependencies): + +Add an `implementation` configuration to the `dependencies` block of your app's `build.gradle` or `build.gradle.kts` file, specifying the library's name and the version you wish to use: + +```gradle +// build.gradle +dependencies { + ... + implementation "com.azure.android:azure-communication-common:1.0.0-beta.5" +} + +// build.gradle.kts +dependencies { + ... + implementation("com.azure.android:azure-communication-common:1.0.0-beta.5") +} +``` + +#### Add a dependency with Maven +To import the library into your project using the [Maven](https://maven.apache.org/) build system, add it to the `dependencies` section of your app's `pom.xml` file, specifying its artifact ID and the version you wish to use: + +```xml + + com.azure.android + azure-communication-common + 1.0.0-beta.5 + +``` + +### Key concepts + +### CommunicationTokenCredential + +A `CommunicationTokenCredential` authenticates a user with Communication Services, such as Chat or Calling. It optionally +provides an auto-refresh mechanism to ensure a continuously stable authentication state during communications. User +tokens are created by the application developer using the Communication Administration SDK - once created, they are +provided to the various Communication Services client libraries by way of a `CommunicationTokenCredential` object. + +## Examples + +The following sections provide several code snippets showing different ways to use a `CommunicationTokenCredential`: + +* [Creating a credential with a static token](#creating-a-credential-with-a-static-token) +* [Creating a credential that refreshes with a Callable](#creating-a-credential-that-refreshes-with-a-callable) +* [Creating a credential that refreshes proactively](#creating-a-credential-that-refreshes-proactively) +* [Creating a credential with an initial value that refreshes proactively](#creating-a-credential-with-an-initial-value-that-refreshes-proactively) +* [Getting a token asynchronously](#getting-a-token-asynchronously) +* [Invalidating a credential](#invalidating-a-credential) + +### Creating a credential with a static token + +```java +CommunicationTokenCredential userCredential = new CommunicationTokenCredential("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjM2MDB9.adM-ddBZZlQ1WlN3pdPBOF5G4Wh9iZpxNP_fSvpF4cWs"); +``` + +### Creating a credential that refreshes with a Callable + +Here we pass an imagined `Callable` that makes a network request to retrieve a token string for user Bob. It will be called on a background thread. + +```java +Callable tokenRefresher = () -> { + return fetchtoken(); +}; + +CommunicationTokenCredential userCredential = new CommunicationTokenCredential(new CommunicationTokenRefreshOptions(tokenRefresher, false)); +``` + +### Creating a credential that refreshes proactively + +Setting `refreshProactively` to true will call your `Callable tokenRefresher` when the token is close to expiry. + +```java +CommunicationTokenCredential userCredential = new CommunicationTokenCredential(new CommunicationTokenRefreshOptions(tokenRefresher, true)); +``` + +### Creating a credential with an initial value that refreshes proactively + +Passing `initialToken` is an optional optimization to skip the first call to `Callable tokenRefresher`. You can use this to separate the boot from your application from subsequent token refresh cycles. + +```java +CommunicationTokenCredential userCredential = new CommunicationTokenCredential(new CommunicationTokenRefreshOptions(tokenRefresher, true, "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjM2MDB9.adM-ddBZZlQ1WlN3pdPBOF5G4Wh9iZpxNP_fSvpF4cWs")); +``` + +### Getting a token asynchronously + +Calling `getToken()` will return a `Future` + +```java +CommunicationTokenCredential userCredential = new CommunicationTokenCredential(new CommunicationTokenRefreshOptions(tokenRefresher, false)); +Future accessTokenFuture = userCredential.getToken(); +``` + +### Invalidating a credential + +Each `CommunicationTokenCredential` instance uses a background thread for refreshing the cached token. To free up resources and facilitate garbage collection, `dispose()` must be called when the `CommunicationTokenCredential` instance is no longer used. + +```java +userCredential.dispose(); +``` + +## Troubleshooting + +If you run into issues while using this library, please feel free to +[file an issue](https://github.com/Azure/azure-sdk-for-android/issues/new). + +## Next steps +* Read more about Communication [user access tokens](https://docs.microsoft.com/azure/communication-services/concepts/authentication). + +## Contributing +This project welcomes contributions and suggestions. Most contributions require you to agree to a Contributor License +Agreement (CLA) declaring that you have the right to, and actually do, grant us the rights to use your contribution. For +details, visit https://cla.microsoft.com. + +When you submit a pull request, a CLA-bot will automatically determine whether you need to provide a CLA and decorate +the PR appropriately (e.g., label, comment). Simply follow the instructions provided by the bot. You will only need to +do this once across all repos using our CLA. + +This project has adopted the [Microsoft Open Source Code of Conduct](https://opensource.microsoft.com/codeofconduct/). +For more information see the [Code of Conduct FAQ](https://opensource.microsoft.com/codeofconduct/faq/) or contact +[opencode@microsoft.com](mailto:opencode@microsoft.com) with any additional questions or comments. + +![Impressions](https://azure-sdk-impressions.azurewebsites.net/api/impressions/azure-sdk-for-android%2Fsdk%2Fcommunication%2Fazure-communication-common%2FREADME.png) diff --git a/sdk/communication/azure-communication-common/build.gradle b/sdk/communication/azure-communication-common/build.gradle new file mode 100644 index 000000000..c832d1393 --- /dev/null +++ b/sdk/communication/azure-communication-common/build.gradle @@ -0,0 +1,19 @@ +ext.publishName = "Microsoft Azure Android Client Library common code For Communication Service" +description = "This package contains the Android client library common code for Microsoft Azure Communication Service." +version = "1.0.0-beta.6" +ext.versionCode = 1 + +android { + defaultConfig { + versionCode project.versionCode + versionName project.version + } +} + +dependencies { + api "com.azure.android:azure-core:1.0.0-beta.3" + implementation "com.fasterxml.jackson.core:jackson-databind:$jacksonVersion" + implementation "com.jakewharton.threetenabp:threetenabp:$threeTenAbpVersion" + testImplementation "junit:junit:$jUnitVersion" + testImplementation "org.threeten:threetenbp:$threeTenBpVersion" +} diff --git a/sdk/communication/azure-communication-common/module.md b/sdk/communication/azure-communication-common/module.md new file mode 100644 index 000000000..e1ef30bbe --- /dev/null +++ b/sdk/communication/azure-communication-common/module.md @@ -0,0 +1,3 @@ +# Module azure-communication-common + +This package contains common code for Azure Communication Service libraries. diff --git a/sdk/communication/azure-communication-common/src/main/AndroidManifest.xml b/sdk/communication/azure-communication-common/src/main/AndroidManifest.xml new file mode 100644 index 000000000..a08d1524f --- /dev/null +++ b/sdk/communication/azure-communication-common/src/main/AndroidManifest.xml @@ -0,0 +1,5 @@ + + + + diff --git a/sdk/communication/azure-communication-common/src/main/java/com/azure/android/communication/common/AutoRefreshUserCredential.java b/sdk/communication/azure-communication-common/src/main/java/com/azure/android/communication/common/AutoRefreshUserCredential.java new file mode 100644 index 000000000..1c120d8da --- /dev/null +++ b/sdk/communication/azure-communication-common/src/main/java/com/azure/android/communication/common/AutoRefreshUserCredential.java @@ -0,0 +1,183 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.android.communication.common; + +import com.azure.android.core.credential.AccessToken; +import com.azure.android.core.util.logging.ClientLogger; + +import java.util.Timer; +import java.util.TimerTask; +import java.util.concurrent.Callable; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.Future; +import java.util.concurrent.FutureTask; + +class AutoRefreshUserCredential extends UserCredential { + private static final int ON_DEMAND_REFRESH_BUFFER_SECS = 120; + private static final int PROACTIVE_REFRESH_BUFFER_SECS = 600; + + private final ClientLogger logger = ClientLogger.getDefault(AutoRefreshUserCredential.class); + + private Callable tokenRefresher; + private Callable accessTokenCallable; + private FutureTask tokenFuture; + private Timer timer; + private TimerTask proactiveRefreshTask; + + AutoRefreshUserCredential(Callable tokenRefresher) { + this(tokenRefresher, false); + } + + AutoRefreshUserCredential(Callable tokenRefresher, String initialToken) { + this(tokenRefresher, false, initialToken); + } + + AutoRefreshUserCredential(Callable tokenRefresher, boolean refreshProactively) { + this(tokenRefresher, refreshProactively, null); + } + + AutoRefreshUserCredential(Callable tokenRefresher, boolean refreshProactively, String initialToken) { + this.tokenRefresher = tokenRefresher; + this.timer = new Timer(); + this.accessTokenCallable = setupAccessTokenCallable(refreshProactively); + + AccessToken initialAccessToken = null; + + if (initialToken != null) { + initialAccessToken = TokenParser.createAccessToken(initialToken); + this.tokenFuture = setupInitialTokenFuture(initialAccessToken); + } + + if (refreshProactively) { + this.scheduleProactiveRefresh(initialAccessToken); + } + } + + @Override + public Future getToken() { + if (shouldRefreshTokenOnDemand()) { + this.updateTokenFuture(); + } + + return this.tokenFuture; + } + + @Override + public void dispose() { + if (this.tokenFuture != null) { + this.tokenFuture.cancel(true); + } + + if (this.proactiveRefreshTask != null) { + this.proactiveRefreshTask.cancel(); + } + + this.timer.cancel(); + this.timer.purge(); + + this.tokenRefresher = null; + this.tokenFuture = null; + this.proactiveRefreshTask = null; + + super.dispose(); + } + + private FutureTask setupInitialTokenFuture(AccessToken initialAccessToken) { + FutureTask initialTokenFuture = new FutureTask<>(() -> initialAccessToken); + initialTokenFuture.run(); + return initialTokenFuture; + } + + private Callable setupAccessTokenCallable(boolean refreshProactively) { + if (!refreshProactively) { + return this::refreshAccessToken; + } + + return () -> { + AccessToken accessToken = this.refreshAccessToken(); + this.scheduleProactiveRefresh(accessToken); + return accessToken; + }; + } + + private AccessToken refreshAccessToken() throws Exception { + String tokenStr = this.tokenRefresher.call(); + AccessToken accessToken = TokenParser.createAccessToken(tokenStr); + return accessToken; + } + + private boolean shouldRefreshTokenOnDemand() { + boolean shouldRefreshTokenOnDemand = false; + if (this.tokenFuture == null || this.tokenFuture.isCancelled()) { + shouldRefreshTokenOnDemand = true; + } else if (this.tokenFuture.isDone()) { + try { + AccessToken accessToken = this.tokenFuture.get(); + long refreshEpochSecond = accessToken.getExpiresAt().toEpochSecond() - ON_DEMAND_REFRESH_BUFFER_SECS; + long currentEpochSecond = System.currentTimeMillis() / 1000; + shouldRefreshTokenOnDemand = currentEpochSecond >= refreshEpochSecond; + } catch (ExecutionException | InterruptedException e) { + shouldRefreshTokenOnDemand = true; + } + } + + return shouldRefreshTokenOnDemand; + } + + private synchronized void updateTokenFuture() { + // Ignore update if disposed + if (this.isDisposed()) { + return; + } + + // Ignore update if tokenFuture in progress + if (this.tokenFuture != null && !this.tokenFuture.isDone() && !this.tokenFuture.isCancelled()) { + return; + } + + FutureTask futureTask = new FutureTask<>(this.accessTokenCallable); + this.tokenFuture = futureTask; + + ScheduledTask scheduledTask = new ScheduledTask(futureTask::run); + try { + this.timer.schedule(scheduledTask, 0); + } catch (IllegalStateException e) { + logger.warning("AutoRefreshUserCredential has been disposed. Unable to schedule token refresh.", e); + } + } + + private synchronized void scheduleProactiveRefresh(AccessToken accessToken) { + if (this.isDisposed()) { + return; + } + + long delayMs = 0; + + if (accessToken != null) { + long refreshEpochSecond = accessToken.getExpiresAt().toEpochSecond() - PROACTIVE_REFRESH_BUFFER_SECS; + long currentEpochSecond = System.currentTimeMillis() / 1000; + delayMs = Math.max((refreshEpochSecond - currentEpochSecond) * 1000, 0); + } + + if (this.proactiveRefreshTask != null) { + this.proactiveRefreshTask.cancel(); + } + + this.proactiveRefreshTask = new ScheduledTask(this::updateTokenFuture); + this.timer.schedule(this.proactiveRefreshTask, delayMs); + } + + private final class ScheduledTask extends TimerTask { + private Runnable runnable; + + ScheduledTask(Runnable runnable) { + this.runnable = runnable; + } + + @Override + public void run() { + this.runnable.run(); + } + } +} diff --git a/sdk/communication/azure-communication-common/src/main/java/com/azure/android/communication/common/CommunicationCloudEnvironment.java b/sdk/communication/azure-communication-common/src/main/java/com/azure/android/communication/common/CommunicationCloudEnvironment.java new file mode 100644 index 000000000..6681da966 --- /dev/null +++ b/sdk/communication/azure-communication-common/src/main/java/com/azure/android/communication/common/CommunicationCloudEnvironment.java @@ -0,0 +1,63 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.android.communication.common; + +import java.util.Objects; + +/** + * The cloud that the identifier belongs to. + */ +public class CommunicationCloudEnvironment { + private static final String PUBLIC_VALUE = "public"; + private static final String DOD_VALUE = "dod"; + private static final String GCCH_VALUE = "gcch"; + + private final String environmentValue; + + /** + * Create CommunicationCloudEnvironment with name string + * @param environmentValue name of hte cloud environment + */ + public CommunicationCloudEnvironment(String environmentValue) { + Objects.requireNonNull(environmentValue); + this.environmentValue = environmentValue; + } + + static CommunicationCloudEnvironment fromModel(CommunicationCloudEnvironmentModel environmentModel) { + return new CommunicationCloudEnvironment(environmentModel.toString()); + } + + /** + * Represent Azure public cloud + */ + public static final CommunicationCloudEnvironment PUBLIC = new CommunicationCloudEnvironment(PUBLIC_VALUE); + + /** + * Represent Azure Dod cloud + */ + public static final CommunicationCloudEnvironment DOD = new CommunicationCloudEnvironment(DOD_VALUE); + + /** + * Represent Azure Gcch cloud + */ + public static final CommunicationCloudEnvironment GCCH = new CommunicationCloudEnvironment(GCCH_VALUE); + + @Override + public boolean equals(Object that) { + if (this == that) { + return true; + } + return that != null && this.environmentValue.equals(that.toString()); + } + + @Override + public String toString() { + return environmentValue; + } + + @Override + public int hashCode() { + return toString().hashCode(); + } + +} diff --git a/sdk/communication/azure-communication-common/src/main/java/com/azure/android/communication/common/CommunicationCloudEnvironmentModel.java b/sdk/communication/azure-communication-common/src/main/java/com/azure/android/communication/common/CommunicationCloudEnvironmentModel.java new file mode 100644 index 000000000..52dddb6f7 --- /dev/null +++ b/sdk/communication/azure-communication-common/src/main/java/com/azure/android/communication/common/CommunicationCloudEnvironmentModel.java @@ -0,0 +1,37 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +// Code generated by Microsoft (R) AutoRest Code Generator. + +package com.azure. android.communication.common; + +import com.azure.android.core.util.ExpandableStringEnum; +import com.fasterxml.jackson.annotation.JsonCreator; +import java.util.Collection; + +/** Defines values for CommunicationCloudEnvironmentModel. */ +public final class CommunicationCloudEnvironmentModel extends ExpandableStringEnum { + /** Static value public for CommunicationCloudEnvironmentModel. */ + public static final CommunicationCloudEnvironmentModel PUBLIC = fromString("public"); + + /** Static value dod for CommunicationCloudEnvironmentModel. */ + public static final CommunicationCloudEnvironmentModel DOD = fromString("dod"); + + /** Static value gcch for CommunicationCloudEnvironmentModel. */ + public static final CommunicationCloudEnvironmentModel GCCH = fromString("gcch"); + + /** + * Creates or finds a CommunicationCloudEnvironmentModel from its string representation. + * + * @param name a name to look for. + * @return the corresponding CommunicationCloudEnvironmentModel. + */ + @JsonCreator + public static CommunicationCloudEnvironmentModel fromString(String name) { + return fromString(name, CommunicationCloudEnvironmentModel.class); + } + + /** @return known CommunicationCloudEnvironmentModel values. */ + public static Collection values() { + return values(CommunicationCloudEnvironmentModel.class); + } +} diff --git a/sdk/communication/azure-communication-common/src/main/java/com/azure/android/communication/common/CommunicationIdentifier.java b/sdk/communication/azure-communication-common/src/main/java/com/azure/android/communication/common/CommunicationIdentifier.java new file mode 100644 index 000000000..0c185cec1 --- /dev/null +++ b/sdk/communication/azure-communication-common/src/main/java/com/azure/android/communication/common/CommunicationIdentifier.java @@ -0,0 +1,9 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.android.communication.common; + +/** + * Common communication identifier for Communication Services + */ +public abstract class CommunicationIdentifier { +} diff --git a/sdk/communication/azure-communication-common/src/main/java/com/azure/android/communication/common/CommunicationIdentifierModel.java b/sdk/communication/azure-communication-common/src/main/java/com/azure/android/communication/common/CommunicationIdentifierModel.java new file mode 100644 index 000000000..bf2dc24ab --- /dev/null +++ b/sdk/communication/azure-communication-common/src/main/java/com/azure/android/communication/common/CommunicationIdentifierModel.java @@ -0,0 +1,116 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +// Code generated by Microsoft (R) AutoRest Code Generator. + +package com.azure.android.communication.common; + +import com.azure.android.core.annotation.Fluent; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** The CommunicationIdentifierModel model. */ +@Fluent +public final class CommunicationIdentifierModel { + /* + * Raw Id of the identifier. Optional in requests, required in responses. + */ + @JsonProperty(value = "rawId") + private String rawId; + + /* + * The communication user. + */ + @JsonProperty(value = "communicationUser") + private CommunicationUserIdentifierModel communicationUser; + + /* + * The phone number. + */ + @JsonProperty(value = "phoneNumber") + private PhoneNumberIdentifierModel phoneNumber; + + /* + * The Microsoft Teams user. + */ + @JsonProperty(value = "microsoftTeamsUser") + private MicrosoftTeamsUserIdentifierModel microsoftTeamsUser; + + /** + * Get the rawId property: Raw Id of the identifier. Optional in requests, required in responses. + * + * @return the rawId value. + */ + public String getRawId() { + return this.rawId; + } + + /** + * Set the rawId property: Raw Id of the identifier. Optional in requests, required in responses. + * + * @param rawId the rawId value to set. + * @return the CommunicationIdentifierModel object itself. + */ + public CommunicationIdentifierModel setRawId(String rawId) { + this.rawId = rawId; + return this; + } + + /** + * Get the communicationUser property: The communication user. + * + * @return the communicationUser value. + */ + public CommunicationUserIdentifierModel getCommunicationUser() { + return this.communicationUser; + } + + /** + * Set the communicationUser property: The communication user. + * + * @param communicationUser the communicationUser value to set. + * @return the CommunicationIdentifierModel object itself. + */ + public CommunicationIdentifierModel setCommunicationUser(CommunicationUserIdentifierModel communicationUser) { + this.communicationUser = communicationUser; + return this; + } + + /** + * Get the phoneNumber property: The phone number. + * + * @return the phoneNumber value. + */ + public PhoneNumberIdentifierModel getPhoneNumber() { + return this.phoneNumber; + } + + /** + * Set the phoneNumber property: The phone number. + * + * @param phoneNumber the phoneNumber value to set. + * @return the CommunicationIdentifierModel object itself. + */ + public CommunicationIdentifierModel setPhoneNumber(PhoneNumberIdentifierModel phoneNumber) { + this.phoneNumber = phoneNumber; + return this; + } + + /** + * Get the microsoftTeamsUser property: The Microsoft Teams user. + * + * @return the microsoftTeamsUser value. + */ + public MicrosoftTeamsUserIdentifierModel getMicrosoftTeamsUser() { + return this.microsoftTeamsUser; + } + + /** + * Set the microsoftTeamsUser property: The Microsoft Teams user. + * + * @param microsoftTeamsUser the microsoftTeamsUser value to set. + * @return the CommunicationIdentifierModel object itself. + */ + public CommunicationIdentifierModel setMicrosoftTeamsUser(MicrosoftTeamsUserIdentifierModel microsoftTeamsUser) { + this.microsoftTeamsUser = microsoftTeamsUser; + return this; + } +} diff --git a/sdk/communication/azure-communication-common/src/main/java/com/azure/android/communication/common/CommunicationIdentifierSerializer.java b/sdk/communication/azure-communication-common/src/main/java/com/azure/android/communication/common/CommunicationIdentifierSerializer.java new file mode 100644 index 000000000..fabfdb9a6 --- /dev/null +++ b/sdk/communication/azure-communication-common/src/main/java/com/azure/android/communication/common/CommunicationIdentifierSerializer.java @@ -0,0 +1,112 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.android.communication.common; + +import android.text.TextUtils; + +import java.util.ArrayList; +import java.util.Objects; + +class CommunicationIdentifierSerializer { + /** + * Deserialize CommunicationIdentifierModel into CommunicationIdentifier + * @param identifier CommunicationIdentifierModel to be deserialized + * @return deserialized CommunicationIdentifier + */ + public static CommunicationIdentifier deserialize(CommunicationIdentifierModel identifier) { + Objects.requireNonNull(identifier, "'identifier' cannot be null"); + assertSingleType(identifier); + String rawId = identifier.getRawId(); + + if (identifier.getCommunicationUser() != null) { + Objects.requireNonNull(identifier.getCommunicationUser().getId()); + return new CommunicationUserIdentifier(identifier.getCommunicationUser().getId()); + } + + if (identifier.getPhoneNumber() != null) { + PhoneNumberIdentifierModel phoneNumberModel = identifier.getPhoneNumber(); + Objects.requireNonNull(phoneNumberModel.getValue(), "'phoneNumber.value' cannot be null"); + Objects.requireNonNull(rawId, "'rawId' cannot be null"); + return new PhoneNumberIdentifier(phoneNumberModel.getValue()).setRawId(rawId); + } + + if (identifier.getMicrosoftTeamsUser() != null) { + MicrosoftTeamsUserIdentifierModel teamsUserIdentifierModel = identifier.getMicrosoftTeamsUser(); + Objects.requireNonNull(teamsUserIdentifierModel.getUserId()); + Objects.requireNonNull(teamsUserIdentifierModel.getCloud()); + Objects.requireNonNull(rawId); + return new MicrosoftTeamsUserIdentifier(teamsUserIdentifierModel.getUserId(), + teamsUserIdentifierModel.isAnonymous()) + .setRawId(rawId) + .setCloudEnvironment(CommunicationCloudEnvironment.fromModel(teamsUserIdentifierModel.getCloud())); + } + + Objects.requireNonNull(rawId); + return new UnknownIdentifier(rawId); + } + + private static void assertSingleType(CommunicationIdentifierModel identifier) { + CommunicationUserIdentifierModel communicationUser = identifier.getCommunicationUser(); + PhoneNumberIdentifierModel phoneNumber = identifier.getPhoneNumber(); + MicrosoftTeamsUserIdentifierModel microsoftTeamsUser = identifier.getMicrosoftTeamsUser(); + + ArrayList presentProperties = new ArrayList(); + if (communicationUser != null) { + presentProperties.add(communicationUser.getClass().getName()); + } + if (phoneNumber != null) { + presentProperties.add(phoneNumber.getClass().getName()); + } + if (microsoftTeamsUser != null) { + presentProperties.add(microsoftTeamsUser.getClass().getName()); + } + + if (presentProperties.size() > 1) { + throw new IllegalArgumentException(String.format("Only one of the identifier models in %s should be present.", + TextUtils.join(", ", presentProperties))); + } + } + + /** + * Serialize CommunicationIdentifier into CommunicationIdentifierModel + * @param identifier CommunicationIdentifier object to be serialized + * @return CommunicationIdentifierModel + * @throws IllegalArgumentException when identifier is an unknown class derived from + * CommunicationIdentifier + */ + public static CommunicationIdentifierModel serialize(CommunicationIdentifier identifier) + throws IllegalArgumentException { + + if (identifier instanceof CommunicationUserIdentifier) { + return new CommunicationIdentifierModel() + .setCommunicationUser( + new CommunicationUserIdentifierModel().setId(((CommunicationUserIdentifier) identifier).getId())); + } + + if (identifier instanceof PhoneNumberIdentifier) { + PhoneNumberIdentifier phoneNumberIdentifier = (PhoneNumberIdentifier) identifier; + return new CommunicationIdentifierModel() + .setRawId(phoneNumberIdentifier.getRawId()) + .setPhoneNumber(new PhoneNumberIdentifierModel().setValue(phoneNumberIdentifier.getPhoneNumber())); + } + + if (identifier instanceof MicrosoftTeamsUserIdentifier) { + MicrosoftTeamsUserIdentifier teamsUserIdentifier = (MicrosoftTeamsUserIdentifier) identifier; + return new CommunicationIdentifierModel() + .setRawId(teamsUserIdentifier.getRawId()) + .setMicrosoftTeamsUser(new MicrosoftTeamsUserIdentifierModel() + .setIsAnonymous(teamsUserIdentifier.isAnonymous()) + .setUserId(teamsUserIdentifier.getUserId()) + .setCloud(CommunicationCloudEnvironmentModel.fromString( + teamsUserIdentifier.getCloudEnvironment().toString()))); + } + + if (identifier instanceof UnknownIdentifier) { + return new CommunicationIdentifierModel() + .setRawId(((UnknownIdentifier) identifier).getId()); + } + + throw new IllegalArgumentException(String.format("Unknown identifier class '%s'", identifier.getClass().getName())); + } +} diff --git a/sdk/communication/azure-communication-common/src/main/java/com/azure/android/communication/common/CommunicationTokenCredential.java b/sdk/communication/azure-communication-common/src/main/java/com/azure/android/communication/common/CommunicationTokenCredential.java new file mode 100644 index 000000000..9cb0e197a --- /dev/null +++ b/sdk/communication/azure-communication-common/src/main/java/com/azure/android/communication/common/CommunicationTokenCredential.java @@ -0,0 +1,105 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.android.communication.common; + +import com.azure.android.core.credential.AccessToken; + +import java.util.concurrent.Callable; +import java.util.concurrent.CancellationException; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; + +/** + * The Azure Communication Services User token credential. + *

+ * This class is used to cache/refresh the access token required by Azure Communication Services. + */ +public class CommunicationTokenCredential { + private UserCredential userCredential; + + /** + * Creates a {@link CommunicationTokenCredential} from the provided token string. + *

+ * The same token will be returned whenever {@link #getToken()} is called. + * + * @param userToken token string for initialization + */ + public CommunicationTokenCredential(String userToken) { + this.userCredential = new StaticUserCredential(userToken); + } + + /** + * Creates a {@link CommunicationTokenCredential} that automatically refreshes the token + * with a provided {@link java.util.concurrent.Callable} on a background thread. + *

+ * The cached token is updated if {@link #getToken()} is called and if the difference between the current time and token expiry time is less than 120s. + *

+ * If {@code refreshProactively} is {@code true}: + *

    + *
  • The cached token will be updated in the background when the difference between the current time and token expiry time is less than 600s.
  • + *
  • The cached token will be updated immediately when the constructor is invoked and initialToken is expired
  • + *
+ * + * @param tokenRefreshOptions Options object that contains token refresher, initial token string, and refreshProactively + */ + public CommunicationTokenCredential(CommunicationTokenRefreshOptions tokenRefreshOptions) { + this.userCredential = new AutoRefreshUserCredential( + tokenRefreshOptions.getTokenRefresher(), + tokenRefreshOptions.getRefreshProactively(), + tokenRefreshOptions.getToken()); + } + + + /** + * Get Azure core access token from credential + *

+ * This method returns an asynchronous {@link java.util.concurrent.Future} with the AccessToken. + * When the {@link CommunicationTokenCredential} is constructed with a tokenRefresher {@link java.util.concurrent.Callable}, + * the AccessToken will automatically be updated as part of the {@link java.util.concurrent.Future} if the cached token exceeds the expiry threshold. + *

+ * If this method is called after {@link #dispose()} has been invoked, a cancelled {@link java.util.concurrent.Future} will be returned. + * + * @return Asynchronous {@link java.util.concurrent.Future} with the AccessToken + */ + public Future getToken() { + if (this.userCredential.isDisposed()) { + return new CancelledTokenFuture(); + } + return this.userCredential.getToken(); + } + + /** + * Invalidates the {@link CommunicationTokenCredential} instance to free up resources for garbage collection. + */ + public void dispose() { + this.userCredential.dispose(); + } + + private final class CancelledTokenFuture implements Future { + @Override + public boolean cancel(boolean mayInterruptIfRunning) { + return false; + } + + @Override + public boolean isCancelled() { + return true; + } + + @Override + public boolean isDone() { + return true; + } + + @Override + public AccessToken get() { + throw new CancellationException(); + } + + @Override + public AccessToken get(long timeout, TimeUnit unit) { + throw new CancellationException(); + } + } +} diff --git a/sdk/communication/azure-communication-common/src/main/java/com/azure/android/communication/common/CommunicationTokenRefreshOptions.java b/sdk/communication/azure-communication-common/src/main/java/com/azure/android/communication/common/CommunicationTokenRefreshOptions.java new file mode 100644 index 000000000..0860c244c --- /dev/null +++ b/sdk/communication/azure-communication-common/src/main/java/com/azure/android/communication/common/CommunicationTokenRefreshOptions.java @@ -0,0 +1,84 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.android.communication.common; + +import java.util.concurrent.Callable; + +/** + * Options for refreshing CommunicationTokenCredential + *

+ * This class is used to define how CommunicationTokenCredential should be refreshed + *

+ */ +public class CommunicationTokenRefreshOptions { + private final Callable tokenRefresher; + private final boolean refreshProactively; + private final String initialToken; + + /** + * Creates a {@link CommunicationTokenRefreshOptions} object + *

+ * Access token will be fetched on demand and may optionally enable proactive refreshing + *

+ * + * @param tokenRefresher the token refresher to provide capacity to fetch fresh token, cannot be null + * @param refreshProactively when set to true, turn on proactive fetching to call + * tokenRefresher before token expiry by minutes set + * with setCallbackOffsetMinutes or default value of + * two minutes + */ + public CommunicationTokenRefreshOptions(Callable tokenRefresher, boolean refreshProactively) { + if (tokenRefresher == null) { + throw new IllegalArgumentException("Missing required parameters 'tokenRefresher'."); + } + this.tokenRefresher = tokenRefresher; + this.refreshProactively = refreshProactively; + this.initialToken = null; + } + + /** + * Creates a {@link CommunicationTokenRefreshOptions} object + *

+ * A valid token is supplied and may optionally enable proactive refreshing + *

+ * + * @param tokenRefresher the token refresher to provide capacity to fetch fresh token, cannot be null + * @param refreshProactively when set to true, turn on proactive fetching to call + * tokenRefresher before token expiry by minutes set + * with setCallbackOffsetMinutes or default value of + * two minutes + * @param token the serialized JWT token, cannot be null + */ + public CommunicationTokenRefreshOptions(Callable tokenRefresher, boolean refreshProactively, String initialToken) { + if (tokenRefresher == null) { + throw new IllegalArgumentException("Missing required parameters 'tokenRefresher'."); + } + if (initialToken == null) { + throw new IllegalArgumentException("Missing required parameters 'initialToken'."); + } + this.tokenRefresher = tokenRefresher; + this.refreshProactively = refreshProactively; + this.initialToken = initialToken; + } + + /** + * @return the token refresher to provide capacity to fetch fresh token + */ + public Callable getTokenRefresher() { + return tokenRefresher; + } + + /** + * @return whether or not to refresh token proactively + */ + public boolean getRefreshProactively() { + return refreshProactively; + } + + /** + * @return the serialized JWT token + */ + public String getToken() { + return initialToken; + } +} diff --git a/sdk/communication/azure-communication-common/src/main/java/com/azure/android/communication/common/CommunicationUserIdentifier.java b/sdk/communication/azure-communication-common/src/main/java/com/azure/android/communication/common/CommunicationUserIdentifier.java new file mode 100644 index 000000000..70af1c06f --- /dev/null +++ b/sdk/communication/azure-communication-common/src/main/java/com/azure/android/communication/common/CommunicationUserIdentifier.java @@ -0,0 +1,53 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.android.communication.common; + + +import com.azure.android.core.util.CoreUtil; + +/** + * Communication identifier for Communication Services Users + */ +public class CommunicationUserIdentifier extends CommunicationIdentifier { + + private final String id; + + /** + * Creates a CommunicationUserIdentifier object + * + * @param id identifier of the communication user. + * @throws IllegalArgumentException thrown if id parameter fail the validation. + */ + public CommunicationUserIdentifier(String id) { + if (CoreUtil.isNullOrEmpty(id)) { + throw new IllegalArgumentException("The initialization parameter [id] cannot be null or empty."); + } + this.id = id; + } + + /** + * Get id of the communication user. + * @return id of the communication user. + */ + public String getId() { + return id; + } + + @Override + public boolean equals(Object that) { + if (this == that) { + return true; + } + + if (!(that instanceof CommunicationUserIdentifier)) { + return false; + } + + return ((CommunicationUserIdentifier) that).getId().equals(id); + } + + @Override + public int hashCode() { + return getId().hashCode(); + } +} diff --git a/sdk/communication/azure-communication-common/src/main/java/com/azure/android/communication/common/CommunicationUserIdentifierModel.java b/sdk/communication/azure-communication-common/src/main/java/com/azure/android/communication/common/CommunicationUserIdentifierModel.java new file mode 100644 index 000000000..cb5e1b7f4 --- /dev/null +++ b/sdk/communication/azure-communication-common/src/main/java/com/azure/android/communication/common/CommunicationUserIdentifierModel.java @@ -0,0 +1,38 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +// Code generated by Microsoft (R) AutoRest Code Generator. + +package com.azure.android.communication.common; + +import com.azure.android.core.annotation.Fluent; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** The CommunicationUserIdentifierModel model. */ +@Fluent +public final class CommunicationUserIdentifierModel { + /* + * The Id of the communication user. + */ + @JsonProperty(value = "id", required = true) + private String id; + + /** + * Get the id property: The Id of the communication user. + * + * @return the id value. + */ + public String getId() { + return this.id; + } + + /** + * Set the id property: The Id of the communication user. + * + * @param id the id value to set. + * @return the CommunicationUserIdentifierModel object itself. + */ + public CommunicationUserIdentifierModel setId(String id) { + this.id = id; + return this; + } +} diff --git a/sdk/communication/azure-communication-common/src/main/java/com/azure/android/communication/common/MicrosoftTeamsUserIdentifier.java b/sdk/communication/azure-communication-common/src/main/java/com/azure/android/communication/common/MicrosoftTeamsUserIdentifier.java new file mode 100644 index 000000000..7dd052044 --- /dev/null +++ b/sdk/communication/azure-communication-common/src/main/java/com/azure/android/communication/common/MicrosoftTeamsUserIdentifier.java @@ -0,0 +1,149 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.android.communication.common; + + +import com.azure.android.core.util.CoreUtil; + +import java.util.Objects; + +/** + * Communication identifier for Microsoft Teams User + */ +public class MicrosoftTeamsUserIdentifier extends CommunicationIdentifier { + + private final String userId; + private final boolean isAnonymous; + private CommunicationCloudEnvironment cloudEnvironment = CommunicationCloudEnvironment.PUBLIC; + + private String rawId; + + /** + * Creates a MicrosoftTeamsUserIdentifier object + * + * @param userId Id of the Microsoft Teams user. If the user isn't anonymous, the id is the AAD object id of the user. + * @param isAnonymous set this to true if the user is anonymous, + * for example when joining a meeting with a share link + * @param cloudEnvironment the cloud environment in which this identifier is created + * @throws IllegalArgumentException thrown if userId parameter fail the validation. + */ + public MicrosoftTeamsUserIdentifier(String userId, boolean isAnonymous, CommunicationCloudEnvironment cloudEnvironment) { + this(userId, isAnonymous); + Objects.requireNonNull(cloudEnvironment); + this.cloudEnvironment = cloudEnvironment; + } + + /** + * Creates a MicrosoftTeamsUserIdentifier object + * + * @param userId Id of the Microsoft Teams user. If the user isn't anonymous, the id is the AAD object id of the user. + * @param isAnonymous set this to true if the user is anonymous, + * for example when joining a meeting with a share link + * @throws IllegalArgumentException thrown if userId parameter fail the validation. + */ + public MicrosoftTeamsUserIdentifier(String userId, boolean isAnonymous) { + if (CoreUtil.isNullOrEmpty(userId)) { + throw new IllegalArgumentException("The initialization parameter [userId] cannot be null or empty."); + } + this.userId = userId; + this.isAnonymous = isAnonymous; + } + + /** + * Creates a MicrosoftTeamsUserIdentifier object + * + * @param userId Id of the Microsoft Teams user. If the user isn't anonymous, the id is the AAD object id of the user. + * @throws IllegalArgumentException thrown if userId parameter fail the validation. + */ + public MicrosoftTeamsUserIdentifier(String userId) { + this(userId, false); + } + + /** + * Get Teams User Id + * @return userId Id of the Microsoft Teams user. If the user isn't anonymous, the id is the AAD object id of the user. + */ + public String getUserId() { + return this.userId; + } + + /** + * @return True if the user is anonymous, for example when joining a meeting with a share link. + */ + public boolean isAnonymous() { + return this.isAnonymous; + } + + /** + * Set cloud environment of the Teams user identifier + * @param cloudEnvironment the cloud environment in which this identifier is created + * @return this object + */ + public MicrosoftTeamsUserIdentifier setCloudEnvironment(CommunicationCloudEnvironment cloudEnvironment) { + this.cloudEnvironment = cloudEnvironment; + return this; + } + + /** + * Get cloud environment of the Teams user identifier + * @return cloud environment in which this identifier is created + */ + public CommunicationCloudEnvironment getCloudEnvironment() { + return cloudEnvironment; + } + + /** + * Get full id of the identifier. This id is optional. + * @return full id of the identifier + */ + public String getRawId() { + return rawId; + } + + /** + * Set full id of the identifier + * @param rawId full id of the identifier + * @return CommunicationIdentifier object itself + */ + public MicrosoftTeamsUserIdentifier setRawId(String rawId) { + this.rawId = rawId; + return this; + } + + @Override + public boolean equals(Object that) { + if (this == that) { + return true; + } + + if (!(that instanceof MicrosoftTeamsUserIdentifier)) { + return false; + } + + MicrosoftTeamsUserIdentifier thatId = (MicrosoftTeamsUserIdentifier) that; + if (!thatId.getUserId().equals(this.getUserId()) + || thatId.isAnonymous != this.isAnonymous) { + return false; + } + + if (cloudEnvironment != null && !cloudEnvironment.equals(thatId.cloudEnvironment)) { + return false; + } + + if (thatId.cloudEnvironment != null && !thatId.cloudEnvironment.equals(this.cloudEnvironment)) { + return false; + } + + return getRawId() == null + || thatId.getRawId() == null + || thatId.getRawId().equals(this.getRawId()); + } + + + @Override + public int hashCode() { + return userId.hashCode(); + } + +} diff --git a/sdk/communication/azure-communication-common/src/main/java/com/azure/android/communication/common/MicrosoftTeamsUserIdentifierModel.java b/sdk/communication/azure-communication-common/src/main/java/com/azure/android/communication/common/MicrosoftTeamsUserIdentifierModel.java new file mode 100644 index 000000000..b67fa8d12 --- /dev/null +++ b/sdk/communication/azure-communication-common/src/main/java/com/azure/android/communication/common/MicrosoftTeamsUserIdentifierModel.java @@ -0,0 +1,95 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +// Code generated by Microsoft (R) AutoRest Code Generator. + +package com.azure.android.communication.common; + +import com.azure.android.core.annotation.Fluent; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** The MicrosoftTeamsUserIdentifierModel model. */ +@Fluent +public final class MicrosoftTeamsUserIdentifierModel { + /* + * The Id of the Microsoft Teams user. If not anonymous, this is the AAD + * object Id of the user. + */ + @JsonProperty(value = "userId", required = true) + private String userId; + + /* + * True if the Microsoft Teams user is anonymous. By default false if + * missing. + */ + @JsonProperty(value = "isAnonymous") + private Boolean isAnonymous; + + /* + * The cloud that the Microsoft Teams user belongs to. By default 'public' + * if missing. + */ + @JsonProperty(value = "cloud") + private CommunicationCloudEnvironmentModel cloud; + + /** + * Get the userId property: The Id of the Microsoft Teams user. If not anonymous, this is the AAD object Id of the + * user. + * + * @return the userId value. + */ + public String getUserId() { + return this.userId; + } + + /** + * Set the userId property: The Id of the Microsoft Teams user. If not anonymous, this is the AAD object Id of the + * user. + * + * @param userId the userId value to set. + * @return the MicrosoftTeamsUserIdentifierModel object itself. + */ + public MicrosoftTeamsUserIdentifierModel setUserId(String userId) { + this.userId = userId; + return this; + } + + /** + * Get the isAnonymous property: True if the Microsoft Teams user is anonymous. By default false if missing. + * + * @return the isAnonymous value. + */ + public Boolean isAnonymous() { + return this.isAnonymous; + } + + /** + * Set the isAnonymous property: True if the Microsoft Teams user is anonymous. By default false if missing. + * + * @param isAnonymous the isAnonymous value to set. + * @return the MicrosoftTeamsUserIdentifierModel object itself. + */ + public MicrosoftTeamsUserIdentifierModel setIsAnonymous(Boolean isAnonymous) { + this.isAnonymous = isAnonymous; + return this; + } + + /** + * Get the cloud property: The cloud that the Microsoft Teams user belongs to. By default 'public' if missing. + * + * @return the cloud value. + */ + public CommunicationCloudEnvironmentModel getCloud() { + return this.cloud; + } + + /** + * Set the cloud property: The cloud that the Microsoft Teams user belongs to. By default 'public' if missing. + * + * @param cloud the cloud value to set. + * @return the MicrosoftTeamsUserIdentifierModel object itself. + */ + public MicrosoftTeamsUserIdentifierModel setCloud(CommunicationCloudEnvironmentModel cloud) { + this.cloud = cloud; + return this; + } +} diff --git a/sdk/communication/azure-communication-common/src/main/java/com/azure/android/communication/common/PhoneNumberIdentifier.java b/sdk/communication/azure-communication-common/src/main/java/com/azure/android/communication/common/PhoneNumberIdentifier.java new file mode 100644 index 000000000..e30c41bd5 --- /dev/null +++ b/sdk/communication/azure-communication-common/src/main/java/com/azure/android/communication/common/PhoneNumberIdentifier.java @@ -0,0 +1,79 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.android.communication.common; + + +import com.azure.android.core.util.CoreUtil; + +/** + * Communication identifier for Communication Services Phone Numbers + */ +public class PhoneNumberIdentifier extends CommunicationIdentifier { + + private final String phoneNumber; + private String rawId; + + /** + * Creates a PhoneNumberIdentifier object + * + * @param phoneNumber the string identifier representing the PhoneNumber in E.164 format. + * E.164 is a phone number formatted as +[CountryCode][AreaCode][LocalNumber] eg. "+18005555555" + * @throws IllegalArgumentException thrown if phoneNumber parameter fail the validation. + */ + public PhoneNumberIdentifier(String phoneNumber) { + if (CoreUtil.isNullOrEmpty(phoneNumber)) { + throw new IllegalArgumentException("The initialization parameter [phoneNumber] cannot be null to empty."); + } + this.phoneNumber = phoneNumber; + } + + /** + * @return the string identifier representing the object identity + */ + public String getPhoneNumber() { + return phoneNumber; + } + + /** + * Get full id of the identifier. This id is optional. + * @return full id of the identifier + */ + public String getRawId() { + return rawId; + } + + /** + * Set full id of the identifier + * @param rawId full id of the identifier + * @return PhoneNumberIdentifier object itself + */ + public PhoneNumberIdentifier setRawId(String rawId) { + this.rawId = rawId; + return this; + } + + @Override + public boolean equals(Object that) { + if (this == that) { + return true; + } + + if (!(that instanceof PhoneNumberIdentifier)) { + return false; + } + + PhoneNumberIdentifier phoneId = (PhoneNumberIdentifier) that; + if (!phoneNumber.equals(phoneId.phoneNumber)) { + return false; + } + + return getRawId() == null + || phoneId.getRawId() == null + || getRawId().equals(phoneId.getRawId()); + } + + @Override + public int hashCode() { + return phoneNumber.hashCode(); + } +} diff --git a/sdk/communication/azure-communication-common/src/main/java/com/azure/android/communication/common/PhoneNumberIdentifierModel.java b/sdk/communication/azure-communication-common/src/main/java/com/azure/android/communication/common/PhoneNumberIdentifierModel.java new file mode 100644 index 000000000..3fc7423e4 --- /dev/null +++ b/sdk/communication/azure-communication-common/src/main/java/com/azure/android/communication/common/PhoneNumberIdentifierModel.java @@ -0,0 +1,38 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +// Code generated by Microsoft (R) AutoRest Code Generator. + +package com.azure.android.communication.common; + +import com.azure.android.core.annotation.Fluent; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** The PhoneNumberIdentifierModel model. */ +@Fluent +public final class PhoneNumberIdentifierModel { + /* + * The phone number in E.164 format. + */ + @JsonProperty(value = "value", required = true) + private String value; + + /** + * Get the value property: The phone number in E.164 format. + * + * @return the value value. + */ + public String getValue() { + return this.value; + } + + /** + * Set the value property: The phone number in E.164 format. + * + * @param value the value value to set. + * @return the PhoneNumberIdentifierModel object itself. + */ + public PhoneNumberIdentifierModel setValue(String value) { + this.value = value; + return this; + } +} diff --git a/sdk/communication/azure-communication-common/src/main/java/com/azure/android/communication/common/StaticUserCredential.java b/sdk/communication/azure-communication-common/src/main/java/com/azure/android/communication/common/StaticUserCredential.java new file mode 100644 index 000000000..2791f1b28 --- /dev/null +++ b/sdk/communication/azure-communication-common/src/main/java/com/azure/android/communication/common/StaticUserCredential.java @@ -0,0 +1,57 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.android.communication.common; + +import com.azure.android.core.credential.AccessToken; + +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; + +class StaticUserCredential extends UserCredential { + + private Future tokenFuture; + + StaticUserCredential(String userToken) { + AccessToken accessToken = TokenParser.createAccessToken(userToken); + this.tokenFuture = new CompletedTokenFuture(accessToken); + } + + @Override + public Future getToken() { + return this.tokenFuture; + } + + private final class CompletedTokenFuture implements Future { + private final AccessToken accessToken; + + CompletedTokenFuture(AccessToken accessToken) { + this.accessToken = accessToken; + } + + @Override + public boolean cancel(boolean mayInterruptIfRunning) { + return false; + } + + @Override + public boolean isCancelled() { + return false; + } + + @Override + public boolean isDone() { + return true; + } + + @Override + public AccessToken get() { + return this.accessToken; + } + + @Override + public AccessToken get(long timeout, TimeUnit unit) { + return accessToken; + } + } +} diff --git a/sdk/communication/azure-communication-common/src/main/java/com/azure/android/communication/common/TokenParser.java b/sdk/communication/azure-communication-common/src/main/java/com/azure/android/communication/common/TokenParser.java new file mode 100644 index 000000000..885cc910d --- /dev/null +++ b/sdk/communication/azure-communication-common/src/main/java/com/azure/android/communication/common/TokenParser.java @@ -0,0 +1,55 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.android.communication.common; + +import android.util.Base64; + +import java.nio.charset.StandardCharsets; +import java.util.Objects; + +import com.fasterxml.jackson.databind.ObjectMapper; + +import org.threeten.bp.Instant; +import org.threeten.bp.OffsetDateTime; +import org.threeten.bp.ZoneId; + +import com.azure.android.core.credential.AccessToken; +import com.fasterxml.jackson.databind.node.ObjectNode; + +/** + * Utility for Handling Access Tokens. + */ +final class TokenParser { + private static final ObjectMapper jsonMapper = new ObjectMapper(); + + private TokenParser() { + // Empty constructor to prevent instantiation of this class. + } + + /** + * Create AccessToken object from Token string + * + * @param tokenStr token string + * @return AccessToken instance + */ + static AccessToken createAccessToken(String tokenStr) { + try { + Objects.requireNonNull(tokenStr, "'tokenStr' cannot be null."); + String[] tokenParts = tokenStr.split("\\."); + String tokenPayload = tokenParts[1]; + byte[] decodedBytes = Base64.decode(tokenPayload, Base64.DEFAULT); + String decodedPayloadJson = new String(decodedBytes, StandardCharsets.UTF_8); + + ObjectNode payloadObj = jsonMapper.readValue(decodedPayloadJson, ObjectNode.class); + long expire = payloadObj.get("exp").longValue(); + OffsetDateTime offsetExpiry = OffsetDateTime.ofInstant(Instant.ofEpochMilli(expire * 1000), ZoneId.of("UTC")); + + return new AccessToken(tokenStr, offsetExpiry); + } catch (Exception e) { + throw new IllegalArgumentException("'tokenStr' is not a valid token string", e); + } + } +} + + diff --git a/sdk/communication/azure-communication-common/src/main/java/com/azure/android/communication/common/UnknownIdentifier.java b/sdk/communication/azure-communication-common/src/main/java/com/azure/android/communication/common/UnknownIdentifier.java new file mode 100644 index 000000000..3c898ea41 --- /dev/null +++ b/sdk/communication/azure-communication-common/src/main/java/com/azure/android/communication/common/UnknownIdentifier.java @@ -0,0 +1,54 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. +package com.azure.android.communication.common; + + +import com.azure.android.core.util.CoreUtil; + +/** + * Catch-all for all other Communication identifiers for Communication Services + */ +public class UnknownIdentifier extends CommunicationIdentifier { + + private final String id; + + /** + * Creates an UnknownIdentifier object + * + * @param id the string identifier representing the identity + * @throws IllegalArgumentException thrown if id parameter fail the validation. + */ + public UnknownIdentifier(String id) { + if (CoreUtil.isNullOrEmpty(id)) { + throw new IllegalArgumentException("The initialization parameter [id] cannot be null or empty."); + } + this.id = id; + } + + /** + * Get id of this identifier + * @return id of this identifier + */ + public String getId() { + return id; + } + + @Override + public boolean equals(Object that) { + if (this == that) { + return true; + } + + if (!(that instanceof UnknownIdentifier)) { + return false; + } + + UnknownIdentifier thatId = (UnknownIdentifier) that; + return this.id.equals(thatId.id); + } + + @Override + public int hashCode() { + return id.hashCode(); + } +} diff --git a/sdk/communication/azure-communication-common/src/main/java/com/azure/android/communication/common/UserCredential.java b/sdk/communication/azure-communication-common/src/main/java/com/azure/android/communication/common/UserCredential.java new file mode 100644 index 000000000..a7a9b41ab --- /dev/null +++ b/sdk/communication/azure-communication-common/src/main/java/com/azure/android/communication/common/UserCredential.java @@ -0,0 +1,22 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.android.communication.common; + +import java.util.concurrent.Future; + +import com.azure.android.core.credential.AccessToken; + +abstract class UserCredential { + private boolean isDisposed = false; + + abstract Future getToken(); + + void dispose() { + this.isDisposed = true; + } + + boolean isDisposed() { + return this.isDisposed; + } +} diff --git a/sdk/communication/azure-communication-common/src/main/java/com/azure/android/communication/common/package-info.java b/sdk/communication/azure-communication-common/src/main/java/com/azure/android/communication/common/package-info.java new file mode 100644 index 000000000..148b86141 --- /dev/null +++ b/sdk/communication/azure-communication-common/src/main/java/com/azure/android/communication/common/package-info.java @@ -0,0 +1,7 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +/** + * Common code for all Azure Communication Service libraries. + */ +package com.azure.android.communication.common; diff --git a/sdk/communication/azure-communication-common/src/test/java/android/util/Base64.java b/sdk/communication/azure-communication-common/src/test/java/android/util/Base64.java new file mode 100644 index 000000000..cd8111897 --- /dev/null +++ b/sdk/communication/azure-communication-common/src/test/java/android/util/Base64.java @@ -0,0 +1,19 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package android.util; + +/** + * Mock for android.util.Base64 + */ +public class Base64 { + public static final int DEFAULT = 0; + + public static String encodeToString(byte[] input, int flags) { + return java.util.Base64.getEncoder().encodeToString(input); + } + + public static byte[] decode(String input, int flags) { + return java.util.Base64.getDecoder().decode(input); + } +} diff --git a/sdk/communication/azure-communication-common/src/test/java/com/azure/android/communication/common/CommunicationIdentifierSerializerTests.java b/sdk/communication/azure-communication-common/src/test/java/com/azure/android/communication/common/CommunicationIdentifierSerializerTests.java new file mode 100644 index 000000000..0e1977563 --- /dev/null +++ b/sdk/communication/azure-communication-common/src/test/java/com/azure/android/communication/common/CommunicationIdentifierSerializerTests.java @@ -0,0 +1,97 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.android.communication.common; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +import java.util.Arrays; + +import static com.azure.android.communication.common.CommunicationCloudEnvironmentModel.PUBLIC; +import static junit.framework.TestCase.assertNotNull; +import static org.hamcrest.core.Is.is; +import static org.hamcrest.core.IsInstanceOf.instanceOf; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertThrows; + +public class CommunicationIdentifierSerializerTests { + + final String someId = "some id"; + final String teamsUserId = "Teams user id"; + final String rawId = "some lengthy id string"; + final String testPhoneNumber = "+12223334444"; + + @Test + public void serializeCommunicationUser() { + CommunicationIdentifierModel model = CommunicationIdentifierSerializer.serialize( + new CommunicationUserIdentifier(someId)); + + assertNotNull(model.getCommunicationUser()); + assertEquals(someId, model.getCommunicationUser().getId()); + } + + @Test + public void deserializeCommunicationUser() { + CommunicationIdentifier identifier = CommunicationIdentifierSerializer.deserialize( + new CommunicationIdentifierModel() + .setCommunicationUser(new CommunicationUserIdentifierModel().setId(someId))); + + assertEquals(identifier.getClass(), CommunicationUserIdentifier.class); + assertEquals(someId, ((CommunicationUserIdentifier) identifier).getId()); + } + + @Test + public void serializeUnknown() { + CommunicationIdentifierModel model = CommunicationIdentifierSerializer.serialize( + new UnknownIdentifier(someId)); + + assertEquals(someId, model.getRawId()); + } + + @Test + public void deserializeUnknown() { + CommunicationIdentifier unknownIdentifier = CommunicationIdentifierSerializer.deserialize( + new CommunicationIdentifierModel() + .setRawId(rawId)); + assertEquals(UnknownIdentifier.class, unknownIdentifier.getClass()); + assertEquals(rawId, ((UnknownIdentifier) unknownIdentifier).getId()); + } + + @Test + public void serializeFutureTypeShouldThrow() { + assertThrows(IllegalArgumentException.class, + () -> { + CommunicationIdentifierSerializer.serialize( + new CommunicationIdentifier() { + public String getId() { + return someId; + } + }); + }); + } + + @Test + public void serializePhoneNumber() { + final String phoneNumber = "+12223334444"; + CommunicationIdentifierModel model = CommunicationIdentifierSerializer.serialize( + new PhoneNumberIdentifier(phoneNumber).setRawId(rawId)); + + assertNotNull(model.getPhoneNumber()); + assertEquals(phoneNumber, model.getPhoneNumber().getValue()); + assertEquals(rawId, model.getRawId()); + } + + @Test + public void deserializePhoneNumber() { + CommunicationIdentifier identifier = CommunicationIdentifierSerializer.deserialize( + new CommunicationIdentifierModel() + .setRawId(rawId) + .setPhoneNumber(new PhoneNumberIdentifierModel().setValue(testPhoneNumber))); + + assertEquals(PhoneNumberIdentifier.class, identifier.getClass()); + assertEquals(testPhoneNumber, ((PhoneNumberIdentifier) identifier).getPhoneNumber()); + assertEquals(rawId, ((PhoneNumberIdentifier) identifier).getRawId()); + } +} diff --git a/sdk/communication/azure-communication-common/src/test/java/com/azure/android/communication/common/CommunicationTokenCredentialTest.java b/sdk/communication/azure-communication-common/src/test/java/com/azure/android/communication/common/CommunicationTokenCredentialTest.java new file mode 100644 index 000000000..8ef210a69 --- /dev/null +++ b/sdk/communication/azure-communication-common/src/test/java/com/azure/android/communication/common/CommunicationTokenCredentialTest.java @@ -0,0 +1,431 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.android.communication.common; + +import com.azure.android.core.credential.AccessToken; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +import java.util.ArrayList; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.concurrent.CancellationException; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; + +import static org.hamcrest.core.Is.is; +import static org.hamcrest.core.IsInstanceOf.instanceOf; +import static org.junit.Assert.*; + +public class CommunicationTokenCredentialTest { + + @Rule + public ExpectedException expectedException = ExpectedException.none(); + + @Test() + public void constructor_withStaticInitialTokenInvalid() { + expectedException.expect(is(instanceOf(IllegalArgumentException.class))); + + new CommunicationTokenCredential("This is an invalid token string"); + } + + @Test + public void constructor_withTokenRefresher_withInitialTokenInvalid() { + MockTokenRefresher mockTokenRefresher = new MockTokenRefresher(); + + expectedException.expect(is(instanceOf(IllegalArgumentException.class))); + new CommunicationTokenCredential(new CommunicationTokenRefreshOptions(mockTokenRefresher, false, "This is an invalid token string")); + } + + @Test + public void constructor_withTokenRefresher_proactiveRefresh_noInitialToken_refresh() throws InterruptedException, ExecutionException { + MockTokenRefresher mockTokenRefresher = new MockTokenRefresher(); + String refreshedToken = TokenStubHelper.createTokenStringForOffset(900); + mockTokenRefresher.setToken(refreshedToken); + CountDownLatch countDownLatch = new CountDownLatch(1); + mockTokenRefresher.setOnCallReturn(countDownLatch::countDown); + + CommunicationTokenCredential credential = new CommunicationTokenCredential(new CommunicationTokenRefreshOptions(mockTokenRefresher, true)); + countDownLatch.await(); + + AccessToken accessToken = credential.getToken().get(); + + assertEquals(refreshedToken, accessToken.getToken()); + assertEquals(1, mockTokenRefresher.getCallCount()); + } + + @Test + public void constructor_withTokenRefresher_proactiveRefresh_initialTokenPastThreshold_refresh() throws InterruptedException { + String tokenString = TokenStubHelper.createTokenStringForOffset(600); + MockTokenRefresher mockTokenRefresher = new MockTokenRefresher(); + CountDownLatch countDownLatch = new CountDownLatch(1); + mockTokenRefresher.setOnCallReturn(countDownLatch::countDown); + + new CommunicationTokenCredential(new CommunicationTokenRefreshOptions(mockTokenRefresher, true, tokenString)); + countDownLatch.await(); + + assertEquals(1, mockTokenRefresher.getCallCount()); + } + + @Test + public void constructor_withTokenRefresher_proactiveRefresh_initialTokenWithinThreshold_notRefreshed() throws InterruptedException, ExecutionException { + String tokenString = TokenStubHelper.createTokenStringForOffset(700); + MockTokenRefresher mockTokenRefresher = new MockTokenRefresher(); + + CommunicationTokenCredential credential = new CommunicationTokenCredential(new CommunicationTokenRefreshOptions(mockTokenRefresher, true, tokenString)); + AccessToken accessToken = credential.getToken().get(); + + assertEquals(tokenString, accessToken.getToken()); + assertEquals(0, mockTokenRefresher.getCallCount()); + } + + @Test + public void constructor_withTokenRefresher_proactiveRefresh_withInitialTokenInvalid() { + MockTokenRefresher mockTokenRefresher = new MockTokenRefresher(); + expectedException.expect(is(instanceOf(IllegalArgumentException.class))); + new CommunicationTokenCredential(new CommunicationTokenRefreshOptions(mockTokenRefresher, true, "This is an invalid token string")); + } + + @Test + public void constructor_withTokenRefresher_proactiveRefresh_repeats() throws InterruptedException { + // Set up MockTokenRefresher to return token with expiry that immediately needs to be refreshed + MockTokenRefresher mockTokenRefresher = new MockTokenRefresher(); + String tokenPastThreshold = TokenStubHelper.createTokenStringForOffset(600); + mockTokenRefresher.setToken(tokenPastThreshold); + + // Limit testing repeats + int numRepeats = 10; + CountDownLatch countDownLatch = new CountDownLatch(numRepeats); + mockTokenRefresher.setOnCallReturn(() -> { + if (countDownLatch.getCount() == 1) { + // Last token returned will not require immediate refresh + String tokenWithinThreshold = TokenStubHelper.createTokenStringForOffset(1200); + mockTokenRefresher.setToken(tokenWithinThreshold); + } + countDownLatch.countDown(); + }); + + new CommunicationTokenCredential(new CommunicationTokenRefreshOptions(mockTokenRefresher, true)); + countDownLatch.await(); + + assertEquals(numRepeats, mockTokenRefresher.getCallCount()); + } + + @Test + public void getToken_staticInitialTokenActive() throws ExecutionException, InterruptedException { + long expiryEpochSecond = System.currentTimeMillis() / 1000 + 60; + String tokenString = TokenStubHelper.createTokenString(expiryEpochSecond); + + CommunicationTokenCredential credential = new CommunicationTokenCredential(tokenString); + AccessToken accessToken = credential.getToken().get(); + + assertEquals(tokenString, accessToken.getToken()); + assertEquals(expiryEpochSecond, accessToken.getExpiresAt().toEpochSecond()); + assertFalse(accessToken.isExpired()); + } + + @Test + public void getToken_staticInitialTokenExpired() throws ExecutionException, InterruptedException { + long expiryEpochSecond = System.currentTimeMillis() / 1000 - 60; + String tokenString = TokenStubHelper.createTokenString(expiryEpochSecond); + + CommunicationTokenCredential credential = new CommunicationTokenCredential(tokenString); + AccessToken accessToken = credential.getToken().get(); + + assertEquals(tokenString, accessToken.getToken()); + assertEquals(expiryEpochSecond, accessToken.getExpiresAt().toEpochSecond()); + assertTrue(accessToken.isExpired()); + } + + @Test + public void getToken_staticInitialToken_isDisposed_cancelledFuture() throws ExecutionException, InterruptedException { + String tokenString = TokenStubHelper.createTokenStringForOffset(1200); + CommunicationTokenCredential credential = new CommunicationTokenCredential(tokenString); + + credential.dispose(); + Future accessTokenFuture = credential.getToken(); + + assertTrue(accessTokenFuture.isDone()); + assertTrue(accessTokenFuture.isCancelled()); + expectedException.expect(is(instanceOf(CancellationException.class))); + accessTokenFuture.get(); + } + + @Test + public void getToken_onDemandAutoRefresh_noInitialToken_firstFetch() throws ExecutionException, InterruptedException { + MockTokenRefresher mockTokenRefresher = new MockTokenRefresher(); + String refreshedToken = TokenStubHelper.createTokenStringForOffset(300); + mockTokenRefresher.setToken(refreshedToken); + CommunicationTokenCredential credential = new CommunicationTokenCredential(new CommunicationTokenRefreshOptions(mockTokenRefresher, false)); + + AccessToken accessToken = credential.getToken().get(); + + assertEquals(refreshedToken, accessToken.getToken()); + assertEquals(1, mockTokenRefresher.getCallCount()); + assertFalse(accessToken.isExpired()); + } + + @Test + public void getToken_onDemandAutoRefresh_noInitialToken_firstFetch_exception() throws ExecutionException, InterruptedException { + MockTokenRefresher mockTokenRefresher = new MockTokenRefresher(); + String refreshedToken = TokenStubHelper.createTokenStringForOffset(300); + mockTokenRefresher.setToken(refreshedToken); + RuntimeException mockTokenRefresherException = new RuntimeException("Mock Token Refresh Exception"); + mockTokenRefresher.setOnCallReturn(() -> { + throw mockTokenRefresherException; + }); + CommunicationTokenCredential credential = new CommunicationTokenCredential(new CommunicationTokenRefreshOptions(mockTokenRefresher, false)); + + try { + expectedException.expectCause(is(mockTokenRefresherException)); + credential.getToken().get(); + } finally { + assertEquals(1, mockTokenRefresher.getCallCount()); + } + } + + @Test + public void getToken_onDemandAutoRefresh_noInitialToken_firstFetch_multithreadedSameResult() throws ExecutionException, InterruptedException { + MockTokenRefresher mockTokenRefresher = new MockTokenRefresher(); + String refreshedToken = TokenStubHelper.createTokenStringForOffset(300); + mockTokenRefresher.setToken(refreshedToken); + CommunicationTokenCredential credential = new CommunicationTokenCredential(new CommunicationTokenRefreshOptions(mockTokenRefresher, false)); + + // Set up blocked multithreaded calls + Runnable blockedRefresh = this.arrangeBlockedRefresh(mockTokenRefresher); + int numCalls = 5; + List> accessTokenFutures = new ArrayList<>(); + ExecutorService executorService = Executors.newFixedThreadPool(numCalls); + for (int i = 0; i < numCalls; i++) { + Future accessTokenFuture = executorService.submit(() -> credential.getToken().get()); + accessTokenFutures.add(accessTokenFuture); + } + + // Unblock refresh and wait for results + blockedRefresh.run(); + Set accessTokenResults = new HashSet<>(); + for (Future accessTokenFuture : accessTokenFutures) { + accessTokenResults.add(accessTokenFuture.get()); + } + executorService.shutdown(); + + assertEquals(1, accessTokenResults.size()); + assertEquals(1, mockTokenRefresher.getCallCount()); + } + + @Test + public void getToken_onDemandAutoRefresh_tokenWithinThresholdNotRefreshed_singleCall() throws ExecutionException, InterruptedException { + String tokenString = TokenStubHelper.createTokenStringForOffset(130); + MockTokenRefresher mockTokenRefresher = new MockTokenRefresher(); + CommunicationTokenCredential credential = new CommunicationTokenCredential(new CommunicationTokenRefreshOptions(mockTokenRefresher, false, tokenString)); + + AccessToken accessToken = credential.getToken().get(); + + assertEquals(tokenString, accessToken.getToken()); + assertEquals(0, mockTokenRefresher.getCallCount()); + } + + @Test + public void getToken_onDemandAutoRefresh_tokenWithinThresholdNotRefreshed_multithreadedCalls() throws ExecutionException, InterruptedException { + String tokenString = TokenStubHelper.createTokenStringForOffset(130); + MockTokenRefresher mockTokenRefresher = new MockTokenRefresher(); + CommunicationTokenCredential credential = new CommunicationTokenCredential(new CommunicationTokenRefreshOptions(mockTokenRefresher, false, tokenString)); + + // Set up blocked multithreaded calls + Runnable blockedRefresh = this.arrangeBlockedRefresh(mockTokenRefresher); + int numCalls = 5; + List> accessTokenFutures = new ArrayList<>(); + ExecutorService executorService = Executors.newFixedThreadPool(numCalls); + for (int i = 0; i < numCalls; i++) { + Future accessTokenFuture = executorService.submit(() -> credential.getToken().get()); + accessTokenFutures.add(accessTokenFuture); + } + + // Unblock refresh and wait for results + blockedRefresh.run(); + Set accessTokenResults = new HashSet<>(); + for (Future accessTokenFuture : accessTokenFutures) { + accessTokenResults.add(accessTokenFuture.get()); + } + executorService.shutdown(); + + assertEquals(1, accessTokenResults.size()); + assertEquals(tokenString, accessTokenResults.iterator().next().getToken()); + assertEquals(0, mockTokenRefresher.getCallCount()); + } + + @Test + public void getToken_onDemandAutoRefresh_tokenPastThresholdRefreshed_singleCall() throws ExecutionException, InterruptedException { + String tokenString = TokenStubHelper.createTokenStringForOffset(120); + MockTokenRefresher mockTokenRefresher = new MockTokenRefresher(); + String refreshedToken = TokenStubHelper.createTokenStringForOffset(300); + mockTokenRefresher.setToken(refreshedToken); + CommunicationTokenCredential credential = new CommunicationTokenCredential(new CommunicationTokenRefreshOptions(mockTokenRefresher, false, tokenString)); + AccessToken accessToken = credential.getToken().get(); + + assertNotEquals(tokenString, accessToken.getToken()); + assertEquals(refreshedToken, accessToken.getToken()); + assertEquals(1, mockTokenRefresher.getCallCount()); + } + + @Test + public void getToken_onDemandAutoRefresh_tokenPastThresholdRefreshed_exception() throws ExecutionException, InterruptedException { + String tokenString = TokenStubHelper.createTokenStringForOffset(120); + MockTokenRefresher mockTokenRefresher = new MockTokenRefresher(); + RuntimeException mockTokenRefresherException = new RuntimeException("Mock Token Refresh Exception"); + mockTokenRefresher.setOnCallReturn(() -> { + throw mockTokenRefresherException; + }); + CommunicationTokenCredential credential = new CommunicationTokenCredential(new CommunicationTokenRefreshOptions(mockTokenRefresher, false, tokenString)); + + try { + expectedException.expectCause(is(mockTokenRefresherException)); + credential.getToken().get(); + } finally { + assertEquals(1, mockTokenRefresher.getCallCount()); + } + } + + @Test + public void getToken_onDemandAutoRefresh_tokenPastThresholdRefreshed_multipleCalls() throws ExecutionException, InterruptedException { + String tokenString = TokenStubHelper.createTokenStringForOffset(120); + MockTokenRefresher mockTokenRefresher = new MockTokenRefresher(); + String refreshedToken = TokenStubHelper.createTokenStringForOffset(300); + mockTokenRefresher.setToken(refreshedToken); + CommunicationTokenCredential credential = new CommunicationTokenCredential(new CommunicationTokenRefreshOptions(mockTokenRefresher, false, tokenString)); + + Set accessTokens = new HashSet<>(); + int numCalls = 3; + for (int i = 0; i < numCalls; i++) { + AccessToken accessToken = credential.getToken().get(); + accessTokens.add(accessToken); + } + + assertEquals(1, accessTokens.size()); + assertNotEquals(tokenString, accessTokens.iterator().next().getToken()); + assertEquals(refreshedToken, accessTokens.iterator().next().getToken()); + assertEquals(1, mockTokenRefresher.getCallCount()); + } + + @Test + public void getToken_onDemandAutoRefresh_tokenPastThresholdRefreshed_multithreadedCalls() throws ExecutionException, InterruptedException { + String tokenString = TokenStubHelper.createTokenStringForOffset(120); + MockTokenRefresher mockTokenRefresher = new MockTokenRefresher(); + String refreshedToken = TokenStubHelper.createTokenStringForOffset(300); + mockTokenRefresher.setToken(refreshedToken); + CommunicationTokenCredential credential = new CommunicationTokenCredential(new CommunicationTokenRefreshOptions(mockTokenRefresher, false, tokenString)); + + // Set up blocked multithreaded calls + Runnable blockedRefresh = this.arrangeBlockedRefresh(mockTokenRefresher); + int numCalls = 5; + List> accessTokenFutures = new ArrayList<>(); + ExecutorService executorService = Executors.newFixedThreadPool(numCalls); + for (int i = 0; i < numCalls; i++) { + Future accessTokenFuture = executorService.submit(() -> credential.getToken().get()); + accessTokenFutures.add(accessTokenFuture); + } + + // Unblock refresh and wait for results + blockedRefresh.run(); + Set accessTokenResults = new HashSet<>(); + for (Future accessTokenFuture : accessTokenFutures) { + accessTokenResults.add(accessTokenFuture.get()); + } + executorService.shutdown(); + + assertEquals(1, accessTokenResults.size()); + assertNotEquals(tokenString, accessTokenResults.iterator().next().getToken()); + assertEquals(refreshedToken, accessTokenResults.iterator().next().getToken()); + assertEquals(1, mockTokenRefresher.getCallCount()); + } + + @Test + public void getToken_onDemandAutoRefresh_isDisposed_cancelledFuture() throws ExecutionException, InterruptedException { + MockTokenRefresher mockTokenRefresher = new MockTokenRefresher(); + CommunicationTokenCredential credential = new CommunicationTokenCredential(new CommunicationTokenRefreshOptions(mockTokenRefresher, false)); + + credential.dispose(); + Future accessTokenFuture = credential.getToken(); + + assertTrue(accessTokenFuture.isDone()); + assertTrue(accessTokenFuture.isCancelled()); + expectedException.expect(is(instanceOf(CancellationException.class))); + accessTokenFuture.get(); + } + + @Test + public void getToken_whileProactiveRefresh_singleResult() throws ExecutionException, InterruptedException { + MockTokenRefresher mockTokenRefresher = new MockTokenRefresher(); + String refreshedToken = TokenStubHelper.createTokenStringForOffset(1200); + mockTokenRefresher.setToken(refreshedToken); + Runnable blockedRefresh = this.arrangeBlockedRefresh(mockTokenRefresher); + CommunicationTokenCredential credential = new CommunicationTokenCredential(new CommunicationTokenRefreshOptions(mockTokenRefresher, true)); + + Future accessTokenFuture = credential.getToken(); + blockedRefresh.run(); + AccessToken accessToken = accessTokenFuture.get(); + + assertEquals(refreshedToken, accessToken.getToken()); + assertEquals(1, mockTokenRefresher.getCallCount()); + } + + + @Test + public void dispose_onDemandAutoRefresh_inProgressCancelled() throws ExecutionException, InterruptedException { + String tokenString = TokenStubHelper.createTokenStringForOffset(120); + MockTokenRefresher mockTokenRefresher = new MockTokenRefresher(); + String refreshedToken = TokenStubHelper.createTokenStringForOffset(300); + mockTokenRefresher.setToken(refreshedToken); + Runnable blockedRefresh = this.arrangeBlockedRefresh(mockTokenRefresher); + + CommunicationTokenCredential credential = new CommunicationTokenCredential(new CommunicationTokenRefreshOptions(mockTokenRefresher, false, tokenString)); + Future accessTokenFuture = credential.getToken(); + credential.dispose(); + blockedRefresh.run(); + + assertTrue(accessTokenFuture.isCancelled()); + assertTrue(accessTokenFuture.isDone()); + + expectedException.expect(is(instanceOf(CancellationException.class))); + accessTokenFuture.get(); + } + + @Test + public void dispose_proactiveRefresh_inProgressCancelled() throws ExecutionException, InterruptedException { + MockTokenRefresher mockTokenRefresher = new MockTokenRefresher(); + String refreshedToken = TokenStubHelper.createTokenStringForOffset(1200); + mockTokenRefresher.setToken(refreshedToken); + Runnable blockedRefresh = this.arrangeBlockedRefresh(mockTokenRefresher); + + CommunicationTokenCredential credential = new CommunicationTokenCredential(new CommunicationTokenRefreshOptions(mockTokenRefresher, true)); + Future accessTokenFuture = credential.getToken(); + credential.dispose(); + blockedRefresh.run(); + + assertTrue(accessTokenFuture.isCancelled()); + assertTrue(accessTokenFuture.isDone()); + + expectedException.expect(is(instanceOf(CancellationException.class))); + accessTokenFuture.get(); + } + + private Runnable arrangeBlockedRefresh(MockTokenRefresher mockTokenRefresher) { + CountDownLatch countDownLatch = new CountDownLatch(1); + mockTokenRefresher.setOnCallReturn(() -> { + try { + countDownLatch.await(); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } + }); + + return countDownLatch::countDown; + } +} diff --git a/sdk/communication/azure-communication-common/src/test/java/com/azure/android/communication/common/IdentifierDeserializationExceptionTests.java b/sdk/communication/azure-communication-common/src/test/java/com/azure/android/communication/common/IdentifierDeserializationExceptionTests.java new file mode 100644 index 000000000..efc4b4050 --- /dev/null +++ b/sdk/communication/azure-communication-common/src/test/java/com/azure/android/communication/common/IdentifierDeserializationExceptionTests.java @@ -0,0 +1,62 @@ +package com.azure.android.communication.common; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +import java.util.Arrays; +import java.util.List; + +import static com.azure.android.communication.common.CommunicationCloudEnvironmentModel.PUBLIC; +import static org.hamcrest.core.Is.is; +import static org.hamcrest.core.IsInstanceOf.instanceOf; + +@RunWith(Parameterized.class) +public class IdentifierDeserializationExceptionTests { + static final String someId = "some id"; + static final String teamsUserId = "Teams user id"; + static final String rawId = "some lengthy id string"; + static final String testPhoneNumber = "+12223334444"; + + @Rule + public ExpectedException expectedException = ExpectedException.none(); + + private final CommunicationIdentifierModel identifierModel; + public IdentifierDeserializationExceptionTests(CommunicationIdentifierModel identifierModel) { + this.identifierModel = identifierModel; + } + + @Parameterized.Parameters + public static List cases() { + return Arrays.asList(new CommunicationIdentifierModel() + .setRawId(rawId) + .setCommunicationUser(new CommunicationUserIdentifierModel() + .setId(someId)) + .setPhoneNumber(new PhoneNumberIdentifierModel() + .setValue(testPhoneNumber)), + new CommunicationIdentifierModel() + .setRawId(rawId) + .setCommunicationUser(new CommunicationUserIdentifierModel() + .setId(someId)) + .setMicrosoftTeamsUser(new MicrosoftTeamsUserIdentifierModel() + .setUserId(teamsUserId) + .setIsAnonymous(true) + .setCloud(PUBLIC)), + new CommunicationIdentifierModel() + .setRawId(rawId) + .setPhoneNumber(new PhoneNumberIdentifierModel() + .setValue(testPhoneNumber)) + .setMicrosoftTeamsUser(new MicrosoftTeamsUserIdentifierModel() + .setUserId(teamsUserId) + .setIsAnonymous(true) + .setCloud(PUBLIC))); + } + + @Test + public void throwsOnMoreThanOneNestedObject() { + expectedException.expect(is(instanceOf(IllegalArgumentException.class))); + CommunicationIdentifierSerializer.deserialize(identifierModel); + } +} diff --git a/sdk/communication/azure-communication-common/src/test/java/com/azure/android/communication/common/IdentifierSerialzationExceptionTests.java b/sdk/communication/azure-communication-common/src/test/java/com/azure/android/communication/common/IdentifierSerialzationExceptionTests.java new file mode 100644 index 000000000..595181569 --- /dev/null +++ b/sdk/communication/azure-communication-common/src/test/java/com/azure/android/communication/common/IdentifierSerialzationExceptionTests.java @@ -0,0 +1,52 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.android.communication.common; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +import java.util.Arrays; +import java.util.List; + +import static com.azure.android.communication.common.CommunicationCloudEnvironmentModel.PUBLIC; +import static org.hamcrest.core.Is.is; +import static org.hamcrest.core.IsInstanceOf.instanceOf; + +@RunWith(Parameterized.class) +public class IdentifierSerialzationExceptionTests { + static final String teamsUserId = "Teams user id"; + static final String rawId = "some lengthy id string"; + + @Rule + public ExpectedException expectedException = ExpectedException.none(); + + private final CommunicationIdentifierModel identifierModel; + public IdentifierSerialzationExceptionTests(CommunicationIdentifierModel identifierModel) { + this.identifierModel = identifierModel; + } + + @Parameterized.Parameters + public static List cases() { + return Arrays.asList( + new CommunicationIdentifierModel(), // Missing RawId + new CommunicationIdentifierModel().setRawId(rawId).setCommunicationUser(new CommunicationUserIdentifierModel()), // Missing Id + new CommunicationIdentifierModel().setRawId(rawId).setPhoneNumber(new PhoneNumberIdentifierModel()), // Missing PhoneNumber + new CommunicationIdentifierModel().setRawId(rawId).setMicrosoftTeamsUser( + new MicrosoftTeamsUserIdentifierModel().setCloud(PUBLIC)), // Missing userId + new CommunicationIdentifierModel().setRawId(rawId).setMicrosoftTeamsUser( + new MicrosoftTeamsUserIdentifierModel().setIsAnonymous(true).setCloud(CommunicationCloudEnvironmentModel.DOD)), // Missing UserId + new CommunicationIdentifierModel().setRawId(rawId).setMicrosoftTeamsUser( + new MicrosoftTeamsUserIdentifierModel().setUserId(teamsUserId).setIsAnonymous(true)) + ); + } + + @Test + public void throwsOnMissingProperty() { + expectedException.expect(is(instanceOf(NullPointerException.class))); + CommunicationIdentifierSerializer.deserialize(identifierModel); + } +} diff --git a/sdk/communication/azure-communication-common/src/test/java/com/azure/android/communication/common/MockTokenRefresher.java b/sdk/communication/azure-communication-common/src/test/java/com/azure/android/communication/common/MockTokenRefresher.java new file mode 100644 index 000000000..d48c92002 --- /dev/null +++ b/sdk/communication/azure-communication-common/src/test/java/com/azure/android/communication/common/MockTokenRefresher.java @@ -0,0 +1,40 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.android.communication.common; + +import java.util.concurrent.Callable; + +class MockTokenRefresher implements Callable { + private String tokenString; + private Runnable onCallReturn; + private int callCount; + + public void setToken(String tokenString) { + this.tokenString = tokenString; + } + + public void setOnCallReturn(Runnable onCallReturn) { + this.onCallReturn = onCallReturn; + } + + public int getCallCount() { + return callCount; + } + + @Override + public String call() { + this.incrementCallCount(); + + if (this.onCallReturn != null) { + this.onCallReturn.run(); + } + + return this.tokenString; + } + + private synchronized void incrementCallCount() { + this.callCount++; + } +} + diff --git a/sdk/communication/azure-communication-common/src/test/java/com/azure/android/communication/common/TeamsUserIdentifierSerializationTests.java b/sdk/communication/azure-communication-common/src/test/java/com/azure/android/communication/common/TeamsUserIdentifierSerializationTests.java new file mode 100644 index 000000000..95f77b644 --- /dev/null +++ b/sdk/communication/azure-communication-common/src/test/java/com/azure/android/communication/common/TeamsUserIdentifierSerializationTests.java @@ -0,0 +1,62 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.android.communication.common; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; +import org.junit.runners.Parameterized.Parameters; + +import java.util.Arrays; +import java.util.List; + +import static junit.framework.TestCase.assertNotNull; +import static org.junit.Assert.assertEquals; + +@RunWith(Parameterized.class) +public class TeamsUserIdentifierSerializationTests { + final String someId = "some id"; + final String teamsUserId = "Teams user id"; + final String rawId = "some lengthy id string"; + + private final boolean isAnonymous; + + public TeamsUserIdentifierSerializationTests(boolean isAnonymous) { + this.isAnonymous = isAnonymous; + } + + @Parameters + public static List cases() { + return Arrays.asList(true, false); + } + + @Test + public void serializeMicrosoftTeamsUser() { + CommunicationIdentifierModel model = CommunicationIdentifierSerializer.serialize( + new MicrosoftTeamsUserIdentifier(teamsUserId, isAnonymous) + .setRawId(rawId) + .setCloudEnvironment(CommunicationCloudEnvironment.DOD)); + + assertNotNull(model.getMicrosoftTeamsUser()); + assertEquals(teamsUserId, model.getMicrosoftTeamsUser().getUserId()); + assertEquals(rawId, model.getRawId()); + assertEquals(CommunicationCloudEnvironmentModel.DOD, model.getMicrosoftTeamsUser().getCloud()); + assertEquals(isAnonymous, model.getMicrosoftTeamsUser().isAnonymous()); + } + + @Test + public void deserializeMicrosoftTeamsUser() { + MicrosoftTeamsUserIdentifier identifier = (MicrosoftTeamsUserIdentifier) CommunicationIdentifierSerializer.deserialize( + new CommunicationIdentifierModel() + .setRawId(rawId) + .setMicrosoftTeamsUser(new MicrosoftTeamsUserIdentifierModel() + .setUserId(teamsUserId).setIsAnonymous(isAnonymous).setCloud(CommunicationCloudEnvironmentModel.GCCH))); + + assertEquals(MicrosoftTeamsUserIdentifier.class, identifier.getClass()); + assertEquals(teamsUserId, identifier.getUserId()); + assertEquals(rawId, identifier.getRawId()); + assertEquals(CommunicationCloudEnvironment.GCCH, identifier.getCloudEnvironment()); + assertEquals(isAnonymous, identifier.isAnonymous()); + } +} diff --git a/sdk/communication/azure-communication-common/src/test/java/com/azure/android/communication/common/TokenStubHelper.java b/sdk/communication/azure-communication-common/src/test/java/com/azure/android/communication/common/TokenStubHelper.java new file mode 100644 index 000000000..bc853d9ea --- /dev/null +++ b/sdk/communication/azure-communication-common/src/test/java/com/azure/android/communication/common/TokenStubHelper.java @@ -0,0 +1,41 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +package com.azure.android.communication.common; + +import android.util.Base64; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; + +import java.nio.charset.StandardCharsets; +import java.util.HashMap; +import java.util.Map; +import java.util.UUID; + +final class TokenStubHelper { + private static final String STUB_TOKEN_TEMPLATE = + "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.%s.adM-ddBZZlQ1WlN3pdPBOF5G4Wh9iZpxNP_fSvpF4cWs"; + + private static final ObjectMapper jsonMapper = new ObjectMapper(); + + static String createTokenString(long expiryEpochSecond) { + Map payload = new HashMap<>(); + payload.put("exp", expiryEpochSecond); + payload.put("jti", UUID.randomUUID()); + + try { + String payloadJson = jsonMapper.writeValueAsString(payload); + String encodedPayload = Base64.encodeToString(payloadJson.getBytes(StandardCharsets.UTF_8), Base64.DEFAULT); + return String.format(STUB_TOKEN_TEMPLATE, encodedPayload); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } + } + + static String createTokenStringForOffset(long secondsFromNow) { + return createTokenString(System.currentTimeMillis() / 1000 + secondsFromNow); + } +} + + diff --git a/settings.gradle b/settings.gradle index 82427d441..371ae85d9 100644 --- a/settings.gradle +++ b/settings.gradle @@ -8,3 +8,4 @@ include ":sdk:core:azure-core-http-httpurlconnection" include ":sdk:core:azure-core-rest" include ":sdk:core:azure-core-test" include ":eng:code-quality-reports" +include ':sdk:communication:azure-communication-common'