Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
68 changes: 24 additions & 44 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,8 @@ This library allows you to use accounts as well as the network stack provided by

- [How to use this library](#how-to-use-this-library)
- [1) Add this library to your project](#1-add-this-library-to-your-project)
- [2) To choose an account, include the following code in your login dialog](#2-to-choose-an-account-include-the-following-code-in-your-login-dialog)
- [3) To handle the result of the Account Chooser, include the following](#3-to-handle-the-result-of-the-account-chooser-include-the-following)
- [2) To choose an account, register an `ActivityResultContract` in your `Activity` or `Fragment`](#2-to-choose-an-account-register-an-activityresultcontract-in-your-activity-or-fragment)
- [3) Handle the result of the `ActivityResultContract`](#3-handle-the-result-of-the-activityresultcontract)
- [4) How to get account information?](#4-how-to-get-account-information)
- [5) How to make a network request?](#5-how-to-make-a-network-request)
- [5.1) Using Retrofit](#51-using-retrofit)
Expand Down Expand Up @@ -54,58 +54,38 @@ dependencies {
}
```

### 2) To choose an account, include the following code in your login dialog
### 2) To choose an account, register an [`ActivityResultContract`](https://developer.android.com/training/basics/intents/result) in your `Activity` or `Fragment`

```java
private void openAccountChooser() {
try {
AccountImporter.pickNewAccount(activityOrFragment);
} catch (NextcloudFilesAppNotInstalledException | AndroidGetAccountsPermissionNotGranted e) {
UiExceptionManager.showDialogForException(this, e);
}
}
private final ActivityResultLauncher<Void> mImportAccountLauncher = registerForActivityResult(new ImportSsoAccount(), this::handleResult);
```

### 3) To handle the result of the Account Chooser, include the following
### 3) Handle the result of the [`ActivityResultContract`](https://developer.android.com/training/basics/intents/result)

```java
@Override
public void onActivityResult(int requestCode, int resultCode, Intent data) {
super.onActivityResult(requestCode, resultCode, data);
AccountImporter.onActivityResult(requestCode, resultCode, data, this, new AccountImporter.IAccountAccessGranted() {

@Override
public void accountAccessGranted(SingleSignOnAccount account) {
final var context = getApplicationContext();

// As this library supports multiple accounts we created some helper methods if you only want to use one.
// The following line stores the selected account as the "default" account which can be queried by using
// the SingleAccountHelper.getCurrentSingleSignOnAccount(context) method
SingleAccountHelper.commitCurrentAccount(context, account.name);

// Get the "default" account
SingleSignOnAccount ssoAccount = null;
try {
ssoAccount = SingleAccountHelper.getCurrentSingleSignOnAccount(context);
} catch (NextcloudFilesAppAccountNotFoundException | NoCurrentAccountSelectedException e) {
UiExceptionManager.showDialogForException(context, e);
}

final var nextcloudAPI = new NextcloudAPI(context, ssoAccount, new GsonBuilder().create());
private void handleResult(SingleSignOnAccount ssoAccount) {
if (ssoAccount == null) {
// Import flow was canceled by user or an error
return;
}

// TODO … (see code in section 4 and below)
}
});
}
// As this library supports multiple accounts we created some helper methods if you only want to use one.
// The following line stores the selected account as the "default" account which can be queried by using
// the SingleAccountHelper.getCurrentSingleSignOnAccount(context) method
SingleAccountHelper.commitCurrentAccount(context, account.name);

@Override
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
AccountImporter.onRequestPermissionsResult(requestCode, permissions, grantResults, this);
}
// Get the "default" account
SingleSignOnAccount ssoAccount = null;
try {
ssoAccount = SingleAccountHelper.getCurrentSingleSignOnAccount(context);
} catch (NextcloudFilesAppAccountNotFoundException | NoCurrentAccountSelectedException e) {
UiExceptionManager.showDialogForException(context, e);
}

// Complete example: https://github.com/nextcloud/news-android/blob/890828441ba0c8a9b90afe56f3e08ed63366ece5/News-Android-App/src/main/java/de/luhmer/owncloudnewsreader/LoginDialogActivity.java#L470-L475
final var nextcloudAPI = new NextcloudAPI(context, ssoAccount, new GsonBuilder().create());

// TODO … (see code in section 4 and below)
}
```

### 4) How to get account information?
Expand Down
7 changes: 7 additions & 0 deletions lib/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
Expand Up @@ -18,4 +18,11 @@
<package android:name="com.nextcloud.android.beta" />
</queries>

<application>
<activity
android:name=".ImportSsoAccountActivity"
android:exported="false"
android:theme="@style/Theme.Sso.Translucent" />
</application>

</manifest>
102 changes: 83 additions & 19 deletions lib/src/main/java/com/nextcloud/android/sso/AccountImporter.java
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@
import android.util.Log;
import android.widget.Toast;

import androidx.annotation.NonNull;
import androidx.annotation.RestrictTo;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
import androidx.fragment.app.Fragment;
Expand All @@ -49,8 +51,8 @@
import java.io.IOException;
import java.util.ArrayList;
import java.util.List;
import java.util.function.Consumer;

import io.reactivex.annotations.NonNull;
import io.reactivex.annotations.Nullable;

public class AccountImporter {
Expand All @@ -69,6 +71,16 @@ public static boolean accountsToImportAvailable(Context context) {
return findAccounts(context).size() > 0;
}

/**
* Prompt dialog to select existing or create a new account
* As soon as an account has been imported, we will continue in #onActivityResult()
* <p>
* In real live applications, you won't import an account on each app start, but remember the imported account via SingleAccountHelper.
*
* @see <a href="https://developer.android.com/training/basics/intents/result"><code>ActivityResultContract</code></a>
* @deprecated Use {@link ImportSsoAccount} - this method will be private in the future
*/
@Deprecated(forRemoval = true)
public static void pickNewAccount(Activity activity) throws NextcloudFilesAppNotInstalledException,
AndroidGetAccountsPermissionNotGranted {
checkAndroidAccountPermissions(activity);
Expand All @@ -82,6 +94,16 @@ public static void pickNewAccount(Activity activity) throws NextcloudFilesAppNot
}
}

/**
* Prompt dialog to select existing or create a new account
* As soon as an account has been imported, we will continue in #onActivityResult()
* <p>
* In real live applications, you won't import an account on each app start, but remember the imported account via SingleAccountHelper.
*
* @see <a href="https://developer.android.com/training/basics/intents/result"><code>ActivityResultContract</code></a>
* @deprecated Use {@link ImportSsoAccount}
*/
@Deprecated(forRemoval = true)
public static void pickNewAccount(Fragment fragment) throws NextcloudFilesAppNotInstalledException,
AndroidGetAccountsPermissionNotGranted {
checkAndroidAccountPermissions(fragment.getContext());
Expand All @@ -95,12 +117,18 @@ public static void pickNewAccount(Fragment fragment) throws NextcloudFilesAppNot
}
}

/**
* @see <a href="https://developer.android.com/training/basics/intents/result"><code>ActivityResultContract</code></a>
* @deprecated Use {@link ImportSsoAccount}
*/
@Deprecated(forRemoval = true)
public static void requestAndroidAccountPermissionsAndPickAccount(Activity activity) {
ActivityCompat.requestPermissions(activity, new String[]{Manifest.permission.GET_ACCOUNTS},
REQUEST_GET_ACCOUNTS_PERMISSION);
}

private static void checkAndroidAccountPermissions(Context context) throws AndroidGetAccountsPermissionNotGranted {
@RestrictTo(RestrictTo.Scope.LIBRARY)
public static void checkAndroidAccountPermissions(Context context) throws AndroidGetAccountsPermissionNotGranted {
// https://developer.android.com/reference/android/accounts/AccountManager#getAccountsByType(java.lang.String)
// Caller targeting API level below Build.VERSION_CODES.O that have not been granted the
// Manifest.permission.GET_ACCOUNTS permission, will only see those accounts managed by
Expand Down Expand Up @@ -131,7 +159,9 @@ private static boolean appInstalledOrNot(Context context) {
return returnValue;
}

// Find all currently installed nextcloud accounts on the phone
/**
* Find all currently installed nextcloud accounts on the phone
*/
public static List<Account> findAccounts(final Context context) {
final AccountManager accMgr = AccountManager.get(context);
final Account[] accounts = accMgr.getAccounts();
Expand All @@ -147,7 +177,6 @@ public static List<Account> findAccounts(final Context context) {
return accountsAvailable;
}


public static Account getAccountForName(Context context, String name) {
for (Account account : findAccounts(context)) {
if (account.name.equals(name)) {
Expand Down Expand Up @@ -181,6 +210,10 @@ public static SingleSignOnAccount getSingleSignOnAccount(Context context, final
throw new NextcloudFilesAppAccountNotFoundException(context, accountName);
}

/**
* @deprecated Visibility will be private in the future
*/
@Deprecated(forRemoval = true)
public static SingleSignOnAccount extractSingleSignOnAccountFromResponse(Intent intent, Context context) {
Bundle future = intent.getBundleExtra(NEXTCLOUD_SSO);

Expand All @@ -205,23 +238,38 @@ public static SingleSignOnAccount extractSingleSignOnAccountFromResponse(Intent
return ssoAccount;
}


/**
* @see <a href="https://developer.android.com/training/basics/intents/result"><code>ActivityResultContract</code></a>
* @deprecated Use {@link ImportSsoAccount} - Visibility will be private in the future.
*/
@Deprecated
public interface IAccountAccessGranted {
void accountAccessGranted(SingleSignOnAccount singleSignOnAccount);
}

/**
* @see <a href="https://developer.android.com/training/basics/intents/result"><code>ActivityResultContract</code></a>
* @deprecated Use {@link ImportSsoAccount}
*/
@Deprecated
public static void onActivityResult(int requestCode, int resultCode, Intent data, Activity activity,
IAccountAccessGranted callback) throws AccountImportCancelledException {
onActivityResult(requestCode, resultCode, data, activity, null, callback);
onActivityResult(requestCode, resultCode, data, activity, null, callback, t -> {});
}

/**
* @see <a href="https://developer.android.com/training/basics/intents/result"><code>ActivityResultContract</code></a>
* @deprecated Use {@link ImportSsoAccount}
*/
@Deprecated
public static void onActivityResult(int requestCode, int resultCode, Intent data, Fragment fragment,
IAccountAccessGranted callback) throws AccountImportCancelledException {
onActivityResult(requestCode, resultCode, data, null, fragment, callback);
onActivityResult(requestCode, resultCode, data, null, fragment, callback, t -> {});
}

private static void onActivityResult(int requestCode, int resultCode, Intent data, Activity activity,
Fragment fragment, IAccountAccessGranted callback)
@RestrictTo(RestrictTo.Scope.LIBRARY)
public static void onActivityResult(int requestCode, int resultCode, Intent data, Activity activity,
Fragment fragment, IAccountAccessGranted successCallback, Consumer<Throwable> failureCallback)
throws AccountImportCancelledException {
Context context = (activity != null) ? activity : fragment.getContext();

Expand All @@ -236,12 +284,12 @@ private static void onActivityResult(int requestCode, int resultCode, Intent dat
}
} catch (NextcloudFilesAppNotSupportedException |
NextcloudFilesAppAccountPermissionNotGrantedException e) {
UiExceptionManager.showDialogForException(context, e);
UiExceptionManager.showDialogForException(context, e, failureCallback::accept);
}
break;
case REQUEST_AUTH_TOKEN_SSO:
SingleSignOnAccount singleSignOnAccount = extractSingleSignOnAccountFromResponse(data, context);
callback.accountAccessGranted(singleSignOnAccount);
successCallback.accountAccessGranted(singleSignOnAccount);
break;
case REQUEST_GET_ACCOUNTS_PERMISSION:
try {
Expand All @@ -252,7 +300,7 @@ private static void onActivityResult(int requestCode, int resultCode, Intent dat
}
} catch (NextcloudFilesAppNotInstalledException |
AndroidGetAccountsPermissionNotGranted e) {
UiExceptionManager.showDialogForException(context, e);
UiExceptionManager.showDialogForException(context, e, failureCallback::accept);
}
break;
default:
Expand All @@ -267,31 +315,43 @@ private static void onActivityResult(int requestCode, int resultCode, Intent dat
try {
handleFailedAuthRequest(context, data);
} catch (SSOException e) {
UiExceptionManager.showDialogForException(context, e);
UiExceptionManager.showDialogForException(context, e, failureCallback::accept);
} catch (Exception e) {
Toast.makeText(context, e.getMessage(), Toast.LENGTH_LONG).show();
//e.printStackTrace();
Log.e(TAG, e.getMessage());
failureCallback.accept(e);
}
break;
case REQUEST_GET_ACCOUNTS_PERMISSION:
UiExceptionManager.showDialogForException(context, new AndroidGetAccountsPermissionNotGranted(context));
UiExceptionManager.showDialogForException(context, new AndroidGetAccountsPermissionNotGranted(context), failureCallback::accept);
break;
default:
break;
}
}
}

/**
* @see <a href="https://developer.android.com/training/basics/intents/result"><code>ActivityResultContract</code></a>
* @deprecated Use {@link ImportSsoAccount}
*/
@Deprecated
public static void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults, Activity activity) {
onRequestPermissionsResult(requestCode, permissions, grantResults, activity, null);
onRequestPermissionsResult(requestCode, permissions, grantResults, activity, null, t -> {});
}

/**
* @see <a href="https://developer.android.com/training/basics/intents/result"><code>ActivityResultContract</code></a>
* @deprecated Use {@link ImportSsoAccount}
*/
@Deprecated
public static void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults, Fragment fragment) {
onRequestPermissionsResult(requestCode, permissions, grantResults, null, fragment);
onRequestPermissionsResult(requestCode, permissions, grantResults, null, fragment, t -> {});
}

private static void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults, Activity activity, Fragment fragment) {
@RestrictTo(RestrictTo.Scope.LIBRARY)
public static void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults, Activity activity, Fragment fragment, Consumer<Throwable> failureCallback) {
Context context = (activity != null) ? activity : fragment.getContext();

switch (requestCode) {
Expand All @@ -306,11 +366,11 @@ private static void onRequestPermissionsResult(int requestCode, @NonNull String[
}
} catch (NextcloudFilesAppNotInstalledException |
AndroidGetAccountsPermissionNotGranted e) {
UiExceptionManager.showDialogForException(context, e);
UiExceptionManager.showDialogForException(context, e, failureCallback::accept);
}
} else {
// user declined the permission request..
UiExceptionManager.showDialogForException(context, new AndroidGetAccountsPermissionNotGranted(context));
UiExceptionManager.showDialogForException(context, new AndroidGetAccountsPermissionNotGranted(context), failureCallback::accept);
}
break;
default:
Expand All @@ -319,6 +379,10 @@ private static void onRequestPermissionsResult(int requestCode, @NonNull String[

}

/**
* @deprecated Will be private in the future
*/
@Deprecated
public static void handleFailedAuthRequest(@NonNull Context context, @Nullable Intent data) throws SSOException {
if (data != null) {
String exception = data.getStringExtra(NEXTCLOUD_SSO_EXCEPTION);
Expand Down
37 changes: 37 additions & 0 deletions lib/src/main/java/com/nextcloud/android/sso/ImportSsoAccount.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package com.nextcloud.android.sso;

import static com.nextcloud.android.sso.Constants.NEXTCLOUD_SSO;

import android.app.Activity;
import android.content.Context;
import android.content.Intent;

import androidx.activity.result.contract.ActivityResultContract;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;

import com.nextcloud.android.sso.model.SingleSignOnAccount;

/**
* {@link ActivityResultContract} to import a Nextcloud account.
* The result account may be <code>null</code> if something went wrong or the user canceled the import.
*
* @see <a href="https://developer.android.com/training/basics/intents/result"><code>ActivityResultContract</code></a>
*/
public class ImportSsoAccount extends ActivityResultContract<Void, SingleSignOnAccount> {

@NonNull
@Override
public Intent createIntent(@NonNull Context context, Void input) {
return new Intent(context, ImportSsoAccountActivity.class);
}

@Override
public SingleSignOnAccount parseResult(int resultCode, @Nullable Intent intent) {
if (resultCode == Activity.RESULT_OK && intent != null) {
return (SingleSignOnAccount) intent.getSerializableExtra(NEXTCLOUD_SSO);
}

return null;
}
}
Loading
Loading