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
1 change: 1 addition & 0 deletions lib/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,7 @@ android {
}
testOptions {
targetSdk 36
unitTests.returnDefaultValues = true
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,7 +48,8 @@ public class AidlNetworkRequest extends NetworkRequest {
private static final String TAG = AidlNetworkRequest.class.getCanonicalName();

private IInputStreamService mService = null;
private final AtomicBoolean mBound = new AtomicBoolean(false); // Flag indicating whether we have called bind on the service
private final AtomicBoolean mBound = new AtomicBoolean(false); // Flag indicating whether the service connection has been established
private final AtomicBoolean mBindingRequested = new AtomicBoolean(false); // Flag indicating whether we have called bind on the service

AidlNetworkRequest(@NonNull Context context, @NonNull SingleSignOnAccount account, @NonNull NextcloudAPI.ApiConnectedListener callback) {
super(context, account, callback);
Expand All @@ -61,12 +62,19 @@ public class AidlNetworkRequest extends NetworkRequest {
public void onServiceConnected(ComponentName className, IBinder service) {
Log.d(TAG, "[onServiceConnected] called from Thread: [" + Thread.currentThread().getName() + "] with IBinder [" + className.toString() + "]: " + service);

final NextcloudAPI.ApiConnectedListener callback = mCallback;
if (mDestroyed || callback == null) {
// API has been closed while the connection was still being established
Log.w(TAG, "[onServiceConnected] API already closed - ignoring connection to [" + className + "]");
return;
}

mService = IInputStreamService.Stub.asInterface(service);
mBound.set(true);
synchronized (mBound) {
mBound.notifyAll();
}
mCallback.onConnected();
callback.onConnected();
}

public void onServiceDisconnected(ComponentName className) {
Expand Down Expand Up @@ -105,6 +113,7 @@ public void connect(String type) {
throw new IllegalStateException("Binding to AccountManagerService returned false");
} else {
Log.d(TAG, "[connect] Bound to AccountManagerService successfully");
mBindingRequested.set(true);
}
} catch (SecurityException e) {
Log.e(TAG, "[connect] can't bind to AccountManagerService, check permission in Manifest");
Expand All @@ -127,17 +136,19 @@ public void close() {
}

private void unbindService() {
// Unbind from the service
if (mBound.get()) {
// Unbind whenever a binding has been requested: the ServiceConnection stays registered
// even if the connection has not been established yet and would otherwise still receive
// onServiceConnected() after this API has been closed
if (mBindingRequested.getAndSet(false)) {
if (mContext != null) {
Log.d(TAG, "[unbindService] Unbinding AccountManagerService");
mContext.unbindService(mConnection);
} else {
Log.e(TAG, "[unbindService] Context was null, cannot unbind nextcloud single sign-on service connection!");
}
mBound.set(false);
mService = null;
}
mBound.set(false);
mService = null;
}

private void waitForApi() throws NextcloudApiNotRespondingException {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
/*
* Nextcloud Android SingleSignOn Library
*
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: GPL-3.0-or-later
*/
package com.nextcloud.android.sso.api;

import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;

import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.ServiceConnection;
import android.os.IBinder;

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

import org.junit.Before;
import org.junit.Test;
import org.mockito.ArgumentCaptor;

public class AidlNetworkRequestTest {

private static final String ACCOUNT_TYPE = "nextcloud";

private Context context;
private NextcloudAPI.ApiConnectedListener callback;
private AidlNetworkRequest request;

@Before
public void setUp() {
context = mock(Context.class);
callback = mock(NextcloudAPI.ApiConnectedListener.class);
final var account = new SingleSignOnAccount("name", "userId", "token", "https://example.com", ACCOUNT_TYPE);
request = new AidlNetworkRequest(context, account, callback);
}

private ServiceConnection connectAndCaptureServiceConnection() {
when(context.bindService(any(Intent.class), any(ServiceConnection.class), anyInt())).thenReturn(true);
request.connect(ACCOUNT_TYPE);

final var connectionCaptor = ArgumentCaptor.forClass(ServiceConnection.class);
verify(context).bindService(any(Intent.class), connectionCaptor.capture(), anyInt());
return connectionCaptor.getValue();
}

@Test
public void onServiceConnectedBeforeCloseInvokesCallback() {
final var connection = connectAndCaptureServiceConnection();

connection.onServiceConnected(new ComponentName("com.nextcloud.client", "AccountManagerService"), mock(IBinder.class));

verify(callback).onConnected();
}

@Test
public void onServiceConnectedAfterCloseDoesNotThrowAndDoesNotInvokeCallback() {
final var connection = connectAndCaptureServiceConnection();

request.close();
connection.onServiceConnected(new ComponentName("com.nextcloud.client", "AccountManagerService"), mock(IBinder.class));

verify(callback, never()).onConnected();
}

@Test
public void closeBeforeServiceConnectedUnbindsServiceConnection() {
final var connection = connectAndCaptureServiceConnection();

request.close();

verify(context).unbindService(connection);
}
}