Skip to content

Commit 252ef94

Browse files
Merge pull request #1033 from nextcloud/448-activity-result-contract
feat: Implement ActivityResultContract
2 parents dbbf938 + 13d64af commit 252ef94

8 files changed

Lines changed: 346 additions & 140 deletions

File tree

‎README.md‎

Lines changed: 24 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,8 @@ This library allows you to use accounts as well as the network stack provided by
1717

1818
- [How to use this library](#how-to-use-this-library)
1919
- [1) Add this library to your project](#1-add-this-library-to-your-project)
20-
- [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)
21-
- [3) To handle the result of the Account Chooser, include the following](#3-to-handle-the-result-of-the-account-chooser-include-the-following)
20+
- [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)
21+
- [3) Handle the result of the `ActivityResultContract`](#3-handle-the-result-of-the-activityresultcontract)
2222
- [4) How to get account information?](#4-how-to-get-account-information)
2323
- [5) How to make a network request?](#5-how-to-make-a-network-request)
2424
- [5.1) Using Retrofit](#51-using-retrofit)
@@ -54,58 +54,38 @@ dependencies {
5454
}
5555
```
5656

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

5959
```java
60-
private void openAccountChooser() {
61-
try {
62-
AccountImporter.pickNewAccount(activityOrFragment);
63-
} catch (NextcloudFilesAppNotInstalledException | AndroidGetAccountsPermissionNotGranted e) {
64-
UiExceptionManager.showDialogForException(this, e);
65-
}
66-
}
60+
private final ActivityResultLauncher<Void> mImportAccountLauncher = registerForActivityResult(new ImportSsoAccount(), this::handleResult);
6761
```
6862

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

7165
```java
72-
@Override
73-
public void onActivityResult(int requestCode, int resultCode, Intent data) {
74-
super.onActivityResult(requestCode, resultCode, data);
75-
AccountImporter.onActivityResult(requestCode, resultCode, data, this, new AccountImporter.IAccountAccessGranted() {
76-
77-
@Override
78-
public void accountAccessGranted(SingleSignOnAccount account) {
79-
final var context = getApplicationContext();
80-
81-
// As this library supports multiple accounts we created some helper methods if you only want to use one.
82-
// The following line stores the selected account as the "default" account which can be queried by using
83-
// the SingleAccountHelper.getCurrentSingleSignOnAccount(context) method
84-
SingleAccountHelper.commitCurrentAccount(context, account.name);
85-
86-
// Get the "default" account
87-
SingleSignOnAccount ssoAccount = null;
88-
try {
89-
ssoAccount = SingleAccountHelper.getCurrentSingleSignOnAccount(context);
90-
} catch (NextcloudFilesAppAccountNotFoundException | NoCurrentAccountSelectedException e) {
91-
UiExceptionManager.showDialogForException(context, e);
92-
}
93-
94-
final var nextcloudAPI = new NextcloudAPI(context, ssoAccount, new GsonBuilder().create());
66+
private void handleResult(SingleSignOnAccount ssoAccount) {
67+
if (ssoAccount == null) {
68+
// Import flow was canceled by user or an error
69+
return;
70+
}
9571

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

101-
@Override
102-
public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) {
103-
super.onRequestPermissionsResult(requestCode, permissions, grantResults);
104-
AccountImporter.onRequestPermissionsResult(requestCode, permissions, grantResults, this);
105-
}
77+
// Get the "default" account
78+
SingleSignOnAccount ssoAccount = null;
79+
try {
80+
ssoAccount = SingleAccountHelper.getCurrentSingleSignOnAccount(context);
81+
} catch (NextcloudFilesAppAccountNotFoundException | NoCurrentAccountSelectedException e) {
82+
UiExceptionManager.showDialogForException(context, e);
83+
}
10684

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

87+
// TODO … (see code in section 4 and below)
88+
}
10989
```
11090

11191
### 4) How to get account information?

‎lib/src/main/AndroidManifest.xml‎

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,4 +18,11 @@
1818
<package android:name="com.nextcloud.android.beta" />
1919
</queries>
2020

21+
<application>
22+
<activity
23+
android:name=".ImportSsoAccountActivity"
24+
android:exported="false"
25+
android:theme="@style/Theme.Sso.Translucent" />
26+
</application>
27+
2128
</manifest>

‎lib/src/main/java/com/nextcloud/android/sso/AccountImporter.java‎

Lines changed: 83 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,8 @@
3131
import android.util.Log;
3232
import android.widget.Toast;
3333

34+
import androidx.annotation.NonNull;
35+
import androidx.annotation.RestrictTo;
3436
import androidx.core.app.ActivityCompat;
3537
import androidx.core.content.ContextCompat;
3638
import androidx.fragment.app.Fragment;
@@ -49,8 +51,8 @@
4951
import java.io.IOException;
5052
import java.util.ArrayList;
5153
import java.util.List;
54+
import java.util.function.Consumer;
5255

53-
import io.reactivex.annotations.NonNull;
5456
import io.reactivex.annotations.Nullable;
5557

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

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

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

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

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

134-
// Find all currently installed nextcloud accounts on the phone
162+
/**
163+
* Find all currently installed nextcloud accounts on the phone
164+
*/
135165
public static List<Account> findAccounts(final Context context) {
136166
final AccountManager accMgr = AccountManager.get(context);
137167
final Account[] accounts = accMgr.getAccounts();
@@ -147,7 +177,6 @@ public static List<Account> findAccounts(final Context context) {
147177
return accountsAvailable;
148178
}
149179

150-
151180
public static Account getAccountForName(Context context, String name) {
152181
for (Account account : findAccounts(context)) {
153182
if (account.name.equals(name)) {
@@ -181,6 +210,10 @@ public static SingleSignOnAccount getSingleSignOnAccount(Context context, final
181210
throw new NextcloudFilesAppAccountNotFoundException(context, accountName);
182211
}
183212

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

@@ -205,23 +238,38 @@ public static SingleSignOnAccount extractSingleSignOnAccountFromResponse(Intent
205238
return ssoAccount;
206239
}
207240

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

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

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

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

@@ -236,12 +284,12 @@ private static void onActivityResult(int requestCode, int resultCode, Intent dat
236284
}
237285
} catch (NextcloudFilesAppNotSupportedException |
238286
NextcloudFilesAppAccountPermissionNotGrantedException e) {
239-
UiExceptionManager.showDialogForException(context, e);
287+
UiExceptionManager.showDialogForException(context, e, failureCallback::accept);
240288
}
241289
break;
242290
case REQUEST_AUTH_TOKEN_SSO:
243291
SingleSignOnAccount singleSignOnAccount = extractSingleSignOnAccountFromResponse(data, context);
244-
callback.accountAccessGranted(singleSignOnAccount);
292+
successCallback.accountAccessGranted(singleSignOnAccount);
245293
break;
246294
case REQUEST_GET_ACCOUNTS_PERMISSION:
247295
try {
@@ -252,7 +300,7 @@ private static void onActivityResult(int requestCode, int resultCode, Intent dat
252300
}
253301
} catch (NextcloudFilesAppNotInstalledException |
254302
AndroidGetAccountsPermissionNotGranted e) {
255-
UiExceptionManager.showDialogForException(context, e);
303+
UiExceptionManager.showDialogForException(context, e, failureCallback::accept);
256304
}
257305
break;
258306
default:
@@ -267,31 +315,43 @@ private static void onActivityResult(int requestCode, int resultCode, Intent dat
267315
try {
268316
handleFailedAuthRequest(context, data);
269317
} catch (SSOException e) {
270-
UiExceptionManager.showDialogForException(context, e);
318+
UiExceptionManager.showDialogForException(context, e, failureCallback::accept);
271319
} catch (Exception e) {
272320
Toast.makeText(context, e.getMessage(), Toast.LENGTH_LONG).show();
273321
//e.printStackTrace();
274322
Log.e(TAG, e.getMessage());
323+
failureCallback.accept(e);
275324
}
276325
break;
277326
case REQUEST_GET_ACCOUNTS_PERMISSION:
278-
UiExceptionManager.showDialogForException(context, new AndroidGetAccountsPermissionNotGranted(context));
327+
UiExceptionManager.showDialogForException(context, new AndroidGetAccountsPermissionNotGranted(context), failureCallback::accept);
279328
break;
280329
default:
281330
break;
282331
}
283332
}
284333
}
285334

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

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

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

297357
switch (requestCode) {
@@ -306,11 +366,11 @@ private static void onRequestPermissionsResult(int requestCode, @NonNull String[
306366
}
307367
} catch (NextcloudFilesAppNotInstalledException |
308368
AndroidGetAccountsPermissionNotGranted e) {
309-
UiExceptionManager.showDialogForException(context, e);
369+
UiExceptionManager.showDialogForException(context, e, failureCallback::accept);
310370
}
311371
} else {
312372
// user declined the permission request..
313-
UiExceptionManager.showDialogForException(context, new AndroidGetAccountsPermissionNotGranted(context));
373+
UiExceptionManager.showDialogForException(context, new AndroidGetAccountsPermissionNotGranted(context), failureCallback::accept);
314374
}
315375
break;
316376
default:
@@ -319,6 +379,10 @@ private static void onRequestPermissionsResult(int requestCode, @NonNull String[
319379

320380
}
321381

382+
/**
383+
* @deprecated Will be private in the future
384+
*/
385+
@Deprecated
322386
public static void handleFailedAuthRequest(@NonNull Context context, @Nullable Intent data) throws SSOException {
323387
if (data != null) {
324388
String exception = data.getStringExtra(NEXTCLOUD_SSO_EXCEPTION);
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
/*
2+
* Nextcloud Android SingleSignOn Library
3+
*
4+
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
5+
* SPDX-License-Identifier: GPL-3.0-or-later
6+
*/
7+
package com.nextcloud.android.sso;
8+
9+
import static com.nextcloud.android.sso.Constants.NEXTCLOUD_SSO;
10+
11+
import android.app.Activity;
12+
import android.content.Context;
13+
import android.content.Intent;
14+
15+
import androidx.activity.result.contract.ActivityResultContract;
16+
import androidx.annotation.NonNull;
17+
import androidx.annotation.Nullable;
18+
19+
import com.nextcloud.android.sso.model.SingleSignOnAccount;
20+
21+
/**
22+
* {@link ActivityResultContract} to import a Nextcloud account.
23+
* The result account may be <code>null</code> if something went wrong or the user canceled the import.
24+
*
25+
* @see <a href="https://developer.android.com/training/basics/intents/result"><code>ActivityResultContract</code></a>
26+
*/
27+
public class ImportSsoAccount extends ActivityResultContract<Void, SingleSignOnAccount> {
28+
29+
@NonNull
30+
@Override
31+
public Intent createIntent(@NonNull Context context, Void input) {
32+
return new Intent(context, ImportSsoAccountActivity.class);
33+
}
34+
35+
@Override
36+
public SingleSignOnAccount parseResult(int resultCode, @Nullable Intent intent) {
37+
if (resultCode == Activity.RESULT_OK && intent != null) {
38+
return (SingleSignOnAccount) intent.getSerializableExtra(NEXTCLOUD_SSO);
39+
}
40+
41+
return null;
42+
}
43+
}

0 commit comments

Comments
 (0)