Loading build.gradle +3 −1 Original line number Diff line number Diff line Loading @@ -51,6 +51,8 @@ android { } dependencies { implementation 'androidx.legacy:legacy-support-v4:1.0.0' implementation "androidx.appcompat:appcompat:1.0.2" implementation 'androidx.annotation:annotation:1.0.2' implementation 'androidx.core:core:1.0.1' implementation 'androidx.fragment:fragment:1.0.0' Loading @@ -70,5 +72,5 @@ dependencies { // Required for local unit tests (JUnit 4 framework) testImplementation 'junit:junit:4.12' // required if you want to use Mockito for unit tests testImplementation 'org.mockito:mockito-core:2.25.1' testImplementation 'org.mockito:mockito-core:2.26.0' } src/main/java/com/nextcloud/android/sso/AccountImporter.java +25 −5 Original line number Diff line number Diff line Loading @@ -61,17 +61,24 @@ public class AccountImporter { public static final int CHOOSE_ACCOUNT_SSO = 4242; public static final int REQUEST_AUTH_TOKEN_SSO = 4243; private static SharedPreferences SHARED_PREFERENCES; public static boolean accountsToImportAvailable(Context context) { return findAccounts(context).size() > 0; } public static void pickNewAccount(Activity activity) throws NextcloudFilesAppNotInstalledException { if (appInstalledOrNot(activity, "com.nextcloud.client")) { Intent intent = AccountManager.newChooseAccountIntent(null, null, new String[]{"nextcloud"}, true, null, null, null, null); activity.startActivityForResult(intent, CHOOSE_ACCOUNT_SSO); } else { throw new NextcloudFilesAppNotInstalledException(); } } public static void pickNewAccount(Fragment fragment) throws NextcloudFilesAppNotInstalledException { if (appInstalledOrNot(fragment.getContext(), "com.nextcloud.client")) { // Clear all tokens first to prevent some caching issues.. //clearAllAuthTokens(fragment.getContext()); Intent intent = AccountManager.newChooseAccountIntent(null, null, new String[]{"nextcloud"}, true, null, null, null, null); fragment.startActivityForResult(intent, CHOOSE_ACCOUNT_SSO); Loading Loading @@ -269,10 +276,23 @@ public class AccountImporter { public static SharedPreferences getSharedPreferences(Context context) { if(SHARED_PREFERENCES != null) { return SHARED_PREFERENCES; } else { return context.getSharedPreferences(SSO_SHARED_PREFERENCE, Context.MODE_PRIVATE); } } protected static String getPrefKeyForAccount(String accountName) { return PREF_ACCOUNT_STRING + accountName; } /** * Allows developers to set the shared preferences that the account information should be stored in. * This is helpful when writing unit tests */ public static void setSharedPreferences(SharedPreferences sharedPreferences) { AccountImporter.SHARED_PREFERENCES = sharedPreferences; } } src/main/java/com/nextcloud/android/sso/api/AidlNetworkRequest.java 0 → 100644 +220 −0 Original line number Diff line number Diff line package com.nextcloud.android.sso.api; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.ServiceConnection; import android.os.IBinder; import android.os.Looper; import android.os.NetworkOnMainThreadException; import android.os.ParcelFileDescriptor; import android.os.RemoteException; import android.util.Log; import com.nextcloud.android.sso.aidl.IInputStreamService; import com.nextcloud.android.sso.aidl.IThreadListener; import com.nextcloud.android.sso.aidl.NextcloudRequest; import com.nextcloud.android.sso.aidl.ParcelFileDescriptorUtil; import com.nextcloud.android.sso.exceptions.NextcloudApiNotRespondingException; import com.nextcloud.android.sso.model.SingleSignOnAccount; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.util.concurrent.atomic.AtomicBoolean; import static com.nextcloud.android.sso.exceptions.SSOException.parseNextcloudCustomException; 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 AidlNetworkRequest(Context context, SingleSignOnAccount account, NextcloudAPI.ApiConnectedListener callback) { super(context, account, callback); } /** * Class for interacting with the main interface of the service. */ private ServiceConnection mConnection = new ServiceConnection() { public void onServiceConnected(ComponentName className, IBinder service) { Log.v(TAG, "Nextcloud Single sign-on: onServiceConnected [" + Thread.currentThread().getName() + "]"); mService = IInputStreamService.Stub.asInterface(service); mBound.set(true); synchronized (mBound) { mBound.notifyAll(); } mCallback.onConnected(); } public void onServiceDisconnected(ComponentName className) { Log.e(TAG, "Nextcloud Single sign-on: ServiceDisconnected"); // This is called when the connection with the service has been // unexpectedly disconnected -- that is, its process crashed. mService = null; mBound.set(false); if (!mDestroyed) { connectApiWithBackoff(); } } }; public void connect() { super.connect(); // Disconnect if connected if (mBound.get()) { stop(); } try { Intent intentService = new Intent(); intentService.setComponent(new ComponentName("com.nextcloud.client", "com.owncloud.android.services.AccountManagerService")); if (!mContext.bindService(intentService, mConnection, Context.BIND_AUTO_CREATE)) { Log.d(TAG, "Binding to AccountManagerService returned false"); throw new IllegalStateException("Binding to AccountManagerService returned false"); } } catch (SecurityException e) { Log.e(TAG, "can't bind to AccountManagerService, check permission in Manifest"); mCallback.onError(e); } } public void stop() { super.stop(); // Unbind from the service if (mBound.get()) { if (mContext != null) { mContext.unbindService(mConnection); } else { Log.e(TAG, "Context was null, cannot unbind nextcloud single sign-on service connection!"); } mBound.set(false); mContext = null; } } private void waitForApi() throws NextcloudApiNotRespondingException { synchronized (mBound) { // If service is not bound yet.. wait if(!mBound.get()) { Log.v(TAG, "[waitForApi] - api not ready yet.. waiting [" + Thread.currentThread().getName() + "]"); try { mBound.wait(10000); // wait up to 10 seconds // If api is still not bound after 10 seconds.. throw an exception if(!mBound.get()) { throw new NextcloudApiNotRespondingException(); } } catch (InterruptedException ex) { Log.e(TAG, "WaitForAPI failed", ex); } } } } /** * The InputStreams needs to be closed after reading from it * * @param request {@link NextcloudRequest} request to be executed on server via Files app * @param requestBodyInputStream inputstream to be sent to the server * @return InputStream answer from server as InputStream * @throws Exception or SSOException */ public InputStream performNetworkRequest(NextcloudRequest request, InputStream requestBodyInputStream) throws Exception { InputStream os = null; Exception exception; try { ParcelFileDescriptor output = performAidlNetworkRequest(request, requestBodyInputStream); os = new ParcelFileDescriptor.AutoCloseInputStream(output); exception = deserializeObject(os); } catch (ClassNotFoundException e) { //e.printStackTrace(); exception = e; } // Handle Remote Exceptions if (exception != null) { if (exception.getMessage() != null) { exception = parseNextcloudCustomException(exception); } throw exception; } return os; } /** * DO NOT CALL THIS METHOD DIRECTLY - use @link(performNetworkRequest) instead * * @param request * @return * @throws IOException */ private ParcelFileDescriptor performAidlNetworkRequest(NextcloudRequest request, InputStream requestBodyInputStream) throws IOException, RemoteException, NextcloudApiNotRespondingException { // Check if we are on the main thread if(Looper.myLooper() == Looper.getMainLooper()) { throw new NetworkOnMainThreadException(); } // Wait for api to be initialized waitForApi(); // Log.d(TAG, request.url); request.setAccountName(getAccountName()); request.setToken(getAccountToken()); ByteArrayOutputStream baos = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(baos); oos.writeObject(request); oos.close(); baos.close(); InputStream is = new ByteArrayInputStream(baos.toByteArray()); ParcelFileDescriptor input = ParcelFileDescriptorUtil.pipeFrom(is, new IThreadListener() { @Override public void onThreadFinished(Thread thread) { Log.d(TAG, "copy data from service finished"); } }); ParcelFileDescriptor requestBodyParcelFileDescriptor = null; if(requestBodyInputStream != null) { requestBodyParcelFileDescriptor = ParcelFileDescriptorUtil.pipeFrom(requestBodyInputStream, new IThreadListener() { @Override public void onThreadFinished(Thread thread) { Log.d(TAG, "copy data from service finished"); } }); } ParcelFileDescriptor output; if(requestBodyParcelFileDescriptor != null) { output = mService.performNextcloudRequestAndBodyStream(input, requestBodyParcelFileDescriptor); } else { output = mService.performNextcloudRequest(input); } return output; } private static <T> T deserializeObject(InputStream is) throws IOException, ClassNotFoundException { ObjectInputStream ois = new ObjectInputStream(is); T result = (T) ois.readObject(); return result; } } src/main/java/com/nextcloud/android/sso/api/NetworkRequest.java 0 → 100644 +63 −0 Original line number Diff line number Diff line package com.nextcloud.android.sso.api; import android.content.Context; import android.os.Looper; import android.util.Log; import com.nextcloud.android.sso.aidl.NextcloudRequest; import com.nextcloud.android.sso.helper.ExponentialBackoff; import com.nextcloud.android.sso.model.SingleSignOnAccount; import java.io.InputStream; public abstract class NetworkRequest { private static final String TAG = NetworkRequest.class.getCanonicalName(); private SingleSignOnAccount mAccount; protected Context mContext; protected NextcloudAPI.ApiConnectedListener mCallback; protected boolean mDestroyed = false; // Flag indicating if API is destroyed protected NetworkRequest(Context context, SingleSignOnAccount account, NextcloudAPI.ApiConnectedListener callback) { this.mContext = context; this.mAccount = account; this.mCallback = callback; } protected void connect() { Log.v(TAG, "Nextcloud Single sign-on connect() called [" + Thread.currentThread().getName() + "]"); if (mDestroyed) { throw new IllegalStateException("API already destroyed! You cannot reuse a stopped API instance"); } } protected abstract InputStream performNetworkRequest(NextcloudRequest request, InputStream requestBodyInputStream) throws Exception; protected void connectApiWithBackoff() { new ExponentialBackoff(1000, 10000, 2, 5, Looper.getMainLooper(), new Runnable() { @Override public void run() { connect(); } }).start(); } protected void stop() { mCallback = null; mAccount = null; mDestroyed = true; } protected String getAccountName() { return mAccount.name; } protected String getAccountToken() { return mAccount.token; } } src/main/java/com/nextcloud/android/sso/api/NextcloudAPI.java +21 −227 Original line number Diff line number Diff line Loading @@ -19,43 +19,24 @@ package com.nextcloud.android.sso.api; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.ServiceConnection; import android.os.IBinder; import android.os.Looper; import android.os.NetworkOnMainThreadException; import android.os.ParcelFileDescriptor; import android.os.RemoteException; import android.util.Log; import com.google.gson.Gson; import com.nextcloud.android.sso.aidl.IInputStreamService; import com.nextcloud.android.sso.aidl.NextcloudRequest; import com.nextcloud.android.sso.aidl.ParcelFileDescriptorUtil; import com.nextcloud.android.sso.exceptions.NextcloudApiNotRespondingException; import com.nextcloud.android.sso.helper.ExponentialBackoff; import com.nextcloud.android.sso.model.SingleSignOnAccount; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.io.Reader; import java.lang.annotation.Documented; import java.lang.annotation.Retention; import java.lang.annotation.Target; import java.lang.reflect.Type; import java.util.concurrent.atomic.AtomicBoolean; import io.reactivex.Observable; import io.reactivex.annotations.NonNull; import static com.nextcloud.android.sso.exceptions.SSOException.parseNextcloudCustomException; import static java.lang.annotation.ElementType.METHOD; import static java.lang.annotation.RetentionPolicy.RUNTIME; Loading @@ -63,150 +44,46 @@ public class NextcloudAPI { private static final String TAG = NextcloudAPI.class.getCanonicalName(); private NetworkRequest networkRequest; private Gson gson; private IInputStreamService mService = null; private final AtomicBoolean mBound = new AtomicBoolean(false); // Flag indicating whether we have called bind on the service private boolean mDestroyed = false; // Flag indicating if API is destroyed private SingleSignOnAccount mAccount; private ApiConnectedListener mCallback; private Context mContext; @Documented @Target(METHOD) @Retention(RUNTIME) public @interface FollowRedirects{ } public @interface FollowRedirects { } public interface ApiConnectedListener { void onConnected(); void onError(Exception ex); } public NextcloudAPI(Context context, SingleSignOnAccount account, Gson gson, ApiConnectedListener callback) { this.mContext = context; this.mAccount = account; this(gson, new AidlNetworkRequest(context, account, callback)); } public NextcloudAPI(Gson gson, NetworkRequest networkRequest) { this.gson = gson; this.mCallback = callback; this.networkRequest = networkRequest; new Thread() { new Thread() { @Override public void run() { Log.d(TAG, "run() called " + Thread.currentThread().getName()); connectApiWithBackoff(); NextcloudAPI.this.networkRequest.connectApiWithBackoff(); } }.start(); //connectApiWithBackoff(); } private String getAccountName() { return mAccount.name; } private String getAccountToken() { return mAccount.token; } private void connectApiWithBackoff() { new ExponentialBackoff(1000, 10000, 2, 5, Looper.getMainLooper(), this::connect).start(); } private void connect() { Log.v(TAG, "Nextcloud Single sign-on connect() called [" + Thread.currentThread().getName() + "]"); if (mDestroyed) { throw new IllegalStateException("API already destroyed! You cannot reuse a stopped API instance"); } // Disconnect if connected if (mBound.get()) { stop(); } try { Intent intentService = new Intent(); intentService.setComponent(new ComponentName("com.nextcloud.client", "com.owncloud.android.services.AccountManagerService")); if (!mContext.bindService(intentService, mConnection, Context.BIND_AUTO_CREATE)) { Log.d(TAG, "Binding to AccountManagerService returned false"); throw new IllegalStateException("Binding to AccountManagerService returned false"); } } catch (SecurityException e) { Log.e(TAG, "can't bind to AccountManagerService, check permission in Manifest"); mCallback.onError(e); } } public void stop() { gson = null; mDestroyed = true; mAccount = null; mCallback = null; // Unbind from the service if (mBound.get()) { if (mContext != null) { mContext.unbindService(mConnection); } else { Log.e(TAG, "Context was null, cannot unbind nextcloud single sign-on service connection!"); } mBound.set(false); mContext = null; } } /** * Class for interacting with the main interface of the service. */ private ServiceConnection mConnection = new ServiceConnection() { public void onServiceConnected(ComponentName className, IBinder service) { Log.v(TAG, "Nextcloud Single sign-on: onServiceConnected [" + Thread.currentThread().getName() + "]"); mService = IInputStreamService.Stub.asInterface(service); mBound.set(true); synchronized (mBound) { mBound.notifyAll(); } mCallback.onConnected(); } public void onServiceDisconnected(ComponentName className) { Log.e(TAG, "Nextcloud Single sign-on: ServiceDisconnected"); // This is called when the connection with the service has been // unexpectedly disconnected -- that is, its process crashed. mService = null; mBound.set(false); if (!mDestroyed) { connectApiWithBackoff(); } } }; private void waitForApi() throws NextcloudApiNotRespondingException { synchronized (mBound) { // If service is not bound yet.. wait if(!mBound.get()) { Log.v(TAG, "[waitForApi] - api not ready yet.. waiting [" + Thread.currentThread().getName() + "]"); try { mBound.wait(10000); // wait up to 10 seconds // If api is still not bound after 10 seconds.. throw an exception if(!mBound.get()) { throw new NextcloudApiNotRespondingException(); } } catch (InterruptedException ex) { Log.e(TAG, "WaitForAPI failed", ex); } } } networkRequest.stop(); } public <T> Observable<T> performRequestObservable(final Type type, final NextcloudRequest request) { return Observable.fromPublisher( s-> { try { s.onNext((T) performRequest(type, request)); s.onNext(performRequest(type, request)); s.onComplete(); } catch (Exception e) { s.onError(e); Loading @@ -227,17 +104,10 @@ public class NextcloudAPI { } } } return result; } public static <T> T deserializeObject(InputStream is) throws IOException, ClassNotFoundException { ObjectInputStream ois = new ObjectInputStream(is); T result = (T) ois.readObject(); return result; } /** * The InputStreams needs to be closed after reading from it * Loading @@ -246,93 +116,17 @@ public class NextcloudAPI { * @throws Exception or SSOException */ public InputStream performNetworkRequest(NextcloudRequest request) throws Exception { return performNetworkRequest(request, null); } /** * The InputStreams needs to be closed after reading from it * * @param request {@link NextcloudRequest} request to be executed on server via Files app * @param requestBodyInputStream inputstream to be sent to the server * @return InputStream answer from server as InputStream * @throws Exception or SSOException */ public InputStream performNetworkRequest(NextcloudRequest request, InputStream requestBodyInputStream) throws Exception { InputStream os = null; Exception exception; try { ParcelFileDescriptor output = performAidlNetworkRequest(request, requestBodyInputStream); os = new ParcelFileDescriptor.AutoCloseInputStream(output); exception = deserializeObject(os); } catch (ClassNotFoundException e) { //e.printStackTrace(); exception = e; } // Handle Remote Exceptions if (exception != null) { if (exception.getMessage() != null) { exception = parseNextcloudCustomException(exception); } throw exception; } return os; } /** * DO NOT CALL THIS METHOD DIRECTLY - use @link(performNetworkRequest) instead * * @param request * @return * @throws IOException */ private ParcelFileDescriptor performAidlNetworkRequest(NextcloudRequest request, InputStream requestBodyInputStream) throws IOException, RemoteException, NextcloudApiNotRespondingException { // Check if we are on the main thread if(Looper.myLooper() == Looper.getMainLooper()) { throw new NetworkOnMainThreadException(); } // Wait for api to be initialized waitForApi(); // Log.d(TAG, request.url); request.setAccountName(getAccountName()); request.setToken(getAccountToken()); try (ByteArrayOutputStream baos = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(baos) ) { oos.writeObject(request); try (InputStream is = new ByteArrayInputStream(baos.toByteArray()); ParcelFileDescriptor input = ParcelFileDescriptorUtil.pipeFrom(is, thread -> Log.d(TAG, "copy data from service finished"))) { ParcelFileDescriptor requestBodyParcelFileDescriptor = null; if (requestBodyInputStream != null) { requestBodyParcelFileDescriptor = ParcelFileDescriptorUtil.pipeFrom( requestBodyInputStream, thread -> Log.d(TAG, "copy data from service finished")); } ParcelFileDescriptor output; if(requestBodyParcelFileDescriptor != null) { output = mService.performNextcloudRequestAndBodyStream(input, requestBodyParcelFileDescriptor); } else { output = mService.performNextcloudRequest(input); } return output; } } return networkRequest.performNetworkRequest(request, null); } /* public static <T> T deserializeObjectAndCloseStream(InputStream is) throws IOException, ClassNotFoundException { try (ObjectInputStream ois = new ObjectInputStream(is)) { return (T) ois.readObject(); } } */ protected Gson getGson() { return gson; Loading Loading
build.gradle +3 −1 Original line number Diff line number Diff line Loading @@ -51,6 +51,8 @@ android { } dependencies { implementation 'androidx.legacy:legacy-support-v4:1.0.0' implementation "androidx.appcompat:appcompat:1.0.2" implementation 'androidx.annotation:annotation:1.0.2' implementation 'androidx.core:core:1.0.1' implementation 'androidx.fragment:fragment:1.0.0' Loading @@ -70,5 +72,5 @@ dependencies { // Required for local unit tests (JUnit 4 framework) testImplementation 'junit:junit:4.12' // required if you want to use Mockito for unit tests testImplementation 'org.mockito:mockito-core:2.25.1' testImplementation 'org.mockito:mockito-core:2.26.0' }
src/main/java/com/nextcloud/android/sso/AccountImporter.java +25 −5 Original line number Diff line number Diff line Loading @@ -61,17 +61,24 @@ public class AccountImporter { public static final int CHOOSE_ACCOUNT_SSO = 4242; public static final int REQUEST_AUTH_TOKEN_SSO = 4243; private static SharedPreferences SHARED_PREFERENCES; public static boolean accountsToImportAvailable(Context context) { return findAccounts(context).size() > 0; } public static void pickNewAccount(Activity activity) throws NextcloudFilesAppNotInstalledException { if (appInstalledOrNot(activity, "com.nextcloud.client")) { Intent intent = AccountManager.newChooseAccountIntent(null, null, new String[]{"nextcloud"}, true, null, null, null, null); activity.startActivityForResult(intent, CHOOSE_ACCOUNT_SSO); } else { throw new NextcloudFilesAppNotInstalledException(); } } public static void pickNewAccount(Fragment fragment) throws NextcloudFilesAppNotInstalledException { if (appInstalledOrNot(fragment.getContext(), "com.nextcloud.client")) { // Clear all tokens first to prevent some caching issues.. //clearAllAuthTokens(fragment.getContext()); Intent intent = AccountManager.newChooseAccountIntent(null, null, new String[]{"nextcloud"}, true, null, null, null, null); fragment.startActivityForResult(intent, CHOOSE_ACCOUNT_SSO); Loading Loading @@ -269,10 +276,23 @@ public class AccountImporter { public static SharedPreferences getSharedPreferences(Context context) { if(SHARED_PREFERENCES != null) { return SHARED_PREFERENCES; } else { return context.getSharedPreferences(SSO_SHARED_PREFERENCE, Context.MODE_PRIVATE); } } protected static String getPrefKeyForAccount(String accountName) { return PREF_ACCOUNT_STRING + accountName; } /** * Allows developers to set the shared preferences that the account information should be stored in. * This is helpful when writing unit tests */ public static void setSharedPreferences(SharedPreferences sharedPreferences) { AccountImporter.SHARED_PREFERENCES = sharedPreferences; } }
src/main/java/com/nextcloud/android/sso/api/AidlNetworkRequest.java 0 → 100644 +220 −0 Original line number Diff line number Diff line package com.nextcloud.android.sso.api; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.ServiceConnection; import android.os.IBinder; import android.os.Looper; import android.os.NetworkOnMainThreadException; import android.os.ParcelFileDescriptor; import android.os.RemoteException; import android.util.Log; import com.nextcloud.android.sso.aidl.IInputStreamService; import com.nextcloud.android.sso.aidl.IThreadListener; import com.nextcloud.android.sso.aidl.NextcloudRequest; import com.nextcloud.android.sso.aidl.ParcelFileDescriptorUtil; import com.nextcloud.android.sso.exceptions.NextcloudApiNotRespondingException; import com.nextcloud.android.sso.model.SingleSignOnAccount; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.util.concurrent.atomic.AtomicBoolean; import static com.nextcloud.android.sso.exceptions.SSOException.parseNextcloudCustomException; 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 AidlNetworkRequest(Context context, SingleSignOnAccount account, NextcloudAPI.ApiConnectedListener callback) { super(context, account, callback); } /** * Class for interacting with the main interface of the service. */ private ServiceConnection mConnection = new ServiceConnection() { public void onServiceConnected(ComponentName className, IBinder service) { Log.v(TAG, "Nextcloud Single sign-on: onServiceConnected [" + Thread.currentThread().getName() + "]"); mService = IInputStreamService.Stub.asInterface(service); mBound.set(true); synchronized (mBound) { mBound.notifyAll(); } mCallback.onConnected(); } public void onServiceDisconnected(ComponentName className) { Log.e(TAG, "Nextcloud Single sign-on: ServiceDisconnected"); // This is called when the connection with the service has been // unexpectedly disconnected -- that is, its process crashed. mService = null; mBound.set(false); if (!mDestroyed) { connectApiWithBackoff(); } } }; public void connect() { super.connect(); // Disconnect if connected if (mBound.get()) { stop(); } try { Intent intentService = new Intent(); intentService.setComponent(new ComponentName("com.nextcloud.client", "com.owncloud.android.services.AccountManagerService")); if (!mContext.bindService(intentService, mConnection, Context.BIND_AUTO_CREATE)) { Log.d(TAG, "Binding to AccountManagerService returned false"); throw new IllegalStateException("Binding to AccountManagerService returned false"); } } catch (SecurityException e) { Log.e(TAG, "can't bind to AccountManagerService, check permission in Manifest"); mCallback.onError(e); } } public void stop() { super.stop(); // Unbind from the service if (mBound.get()) { if (mContext != null) { mContext.unbindService(mConnection); } else { Log.e(TAG, "Context was null, cannot unbind nextcloud single sign-on service connection!"); } mBound.set(false); mContext = null; } } private void waitForApi() throws NextcloudApiNotRespondingException { synchronized (mBound) { // If service is not bound yet.. wait if(!mBound.get()) { Log.v(TAG, "[waitForApi] - api not ready yet.. waiting [" + Thread.currentThread().getName() + "]"); try { mBound.wait(10000); // wait up to 10 seconds // If api is still not bound after 10 seconds.. throw an exception if(!mBound.get()) { throw new NextcloudApiNotRespondingException(); } } catch (InterruptedException ex) { Log.e(TAG, "WaitForAPI failed", ex); } } } } /** * The InputStreams needs to be closed after reading from it * * @param request {@link NextcloudRequest} request to be executed on server via Files app * @param requestBodyInputStream inputstream to be sent to the server * @return InputStream answer from server as InputStream * @throws Exception or SSOException */ public InputStream performNetworkRequest(NextcloudRequest request, InputStream requestBodyInputStream) throws Exception { InputStream os = null; Exception exception; try { ParcelFileDescriptor output = performAidlNetworkRequest(request, requestBodyInputStream); os = new ParcelFileDescriptor.AutoCloseInputStream(output); exception = deserializeObject(os); } catch (ClassNotFoundException e) { //e.printStackTrace(); exception = e; } // Handle Remote Exceptions if (exception != null) { if (exception.getMessage() != null) { exception = parseNextcloudCustomException(exception); } throw exception; } return os; } /** * DO NOT CALL THIS METHOD DIRECTLY - use @link(performNetworkRequest) instead * * @param request * @return * @throws IOException */ private ParcelFileDescriptor performAidlNetworkRequest(NextcloudRequest request, InputStream requestBodyInputStream) throws IOException, RemoteException, NextcloudApiNotRespondingException { // Check if we are on the main thread if(Looper.myLooper() == Looper.getMainLooper()) { throw new NetworkOnMainThreadException(); } // Wait for api to be initialized waitForApi(); // Log.d(TAG, request.url); request.setAccountName(getAccountName()); request.setToken(getAccountToken()); ByteArrayOutputStream baos = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(baos); oos.writeObject(request); oos.close(); baos.close(); InputStream is = new ByteArrayInputStream(baos.toByteArray()); ParcelFileDescriptor input = ParcelFileDescriptorUtil.pipeFrom(is, new IThreadListener() { @Override public void onThreadFinished(Thread thread) { Log.d(TAG, "copy data from service finished"); } }); ParcelFileDescriptor requestBodyParcelFileDescriptor = null; if(requestBodyInputStream != null) { requestBodyParcelFileDescriptor = ParcelFileDescriptorUtil.pipeFrom(requestBodyInputStream, new IThreadListener() { @Override public void onThreadFinished(Thread thread) { Log.d(TAG, "copy data from service finished"); } }); } ParcelFileDescriptor output; if(requestBodyParcelFileDescriptor != null) { output = mService.performNextcloudRequestAndBodyStream(input, requestBodyParcelFileDescriptor); } else { output = mService.performNextcloudRequest(input); } return output; } private static <T> T deserializeObject(InputStream is) throws IOException, ClassNotFoundException { ObjectInputStream ois = new ObjectInputStream(is); T result = (T) ois.readObject(); return result; } }
src/main/java/com/nextcloud/android/sso/api/NetworkRequest.java 0 → 100644 +63 −0 Original line number Diff line number Diff line package com.nextcloud.android.sso.api; import android.content.Context; import android.os.Looper; import android.util.Log; import com.nextcloud.android.sso.aidl.NextcloudRequest; import com.nextcloud.android.sso.helper.ExponentialBackoff; import com.nextcloud.android.sso.model.SingleSignOnAccount; import java.io.InputStream; public abstract class NetworkRequest { private static final String TAG = NetworkRequest.class.getCanonicalName(); private SingleSignOnAccount mAccount; protected Context mContext; protected NextcloudAPI.ApiConnectedListener mCallback; protected boolean mDestroyed = false; // Flag indicating if API is destroyed protected NetworkRequest(Context context, SingleSignOnAccount account, NextcloudAPI.ApiConnectedListener callback) { this.mContext = context; this.mAccount = account; this.mCallback = callback; } protected void connect() { Log.v(TAG, "Nextcloud Single sign-on connect() called [" + Thread.currentThread().getName() + "]"); if (mDestroyed) { throw new IllegalStateException("API already destroyed! You cannot reuse a stopped API instance"); } } protected abstract InputStream performNetworkRequest(NextcloudRequest request, InputStream requestBodyInputStream) throws Exception; protected void connectApiWithBackoff() { new ExponentialBackoff(1000, 10000, 2, 5, Looper.getMainLooper(), new Runnable() { @Override public void run() { connect(); } }).start(); } protected void stop() { mCallback = null; mAccount = null; mDestroyed = true; } protected String getAccountName() { return mAccount.name; } protected String getAccountToken() { return mAccount.token; } }
src/main/java/com/nextcloud/android/sso/api/NextcloudAPI.java +21 −227 Original line number Diff line number Diff line Loading @@ -19,43 +19,24 @@ package com.nextcloud.android.sso.api; import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.content.ServiceConnection; import android.os.IBinder; import android.os.Looper; import android.os.NetworkOnMainThreadException; import android.os.ParcelFileDescriptor; import android.os.RemoteException; import android.util.Log; import com.google.gson.Gson; import com.nextcloud.android.sso.aidl.IInputStreamService; import com.nextcloud.android.sso.aidl.NextcloudRequest; import com.nextcloud.android.sso.aidl.ParcelFileDescriptorUtil; import com.nextcloud.android.sso.exceptions.NextcloudApiNotRespondingException; import com.nextcloud.android.sso.helper.ExponentialBackoff; import com.nextcloud.android.sso.model.SingleSignOnAccount; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.InputStream; import java.io.InputStreamReader; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.io.Reader; import java.lang.annotation.Documented; import java.lang.annotation.Retention; import java.lang.annotation.Target; import java.lang.reflect.Type; import java.util.concurrent.atomic.AtomicBoolean; import io.reactivex.Observable; import io.reactivex.annotations.NonNull; import static com.nextcloud.android.sso.exceptions.SSOException.parseNextcloudCustomException; import static java.lang.annotation.ElementType.METHOD; import static java.lang.annotation.RetentionPolicy.RUNTIME; Loading @@ -63,150 +44,46 @@ public class NextcloudAPI { private static final String TAG = NextcloudAPI.class.getCanonicalName(); private NetworkRequest networkRequest; private Gson gson; private IInputStreamService mService = null; private final AtomicBoolean mBound = new AtomicBoolean(false); // Flag indicating whether we have called bind on the service private boolean mDestroyed = false; // Flag indicating if API is destroyed private SingleSignOnAccount mAccount; private ApiConnectedListener mCallback; private Context mContext; @Documented @Target(METHOD) @Retention(RUNTIME) public @interface FollowRedirects{ } public @interface FollowRedirects { } public interface ApiConnectedListener { void onConnected(); void onError(Exception ex); } public NextcloudAPI(Context context, SingleSignOnAccount account, Gson gson, ApiConnectedListener callback) { this.mContext = context; this.mAccount = account; this(gson, new AidlNetworkRequest(context, account, callback)); } public NextcloudAPI(Gson gson, NetworkRequest networkRequest) { this.gson = gson; this.mCallback = callback; this.networkRequest = networkRequest; new Thread() { new Thread() { @Override public void run() { Log.d(TAG, "run() called " + Thread.currentThread().getName()); connectApiWithBackoff(); NextcloudAPI.this.networkRequest.connectApiWithBackoff(); } }.start(); //connectApiWithBackoff(); } private String getAccountName() { return mAccount.name; } private String getAccountToken() { return mAccount.token; } private void connectApiWithBackoff() { new ExponentialBackoff(1000, 10000, 2, 5, Looper.getMainLooper(), this::connect).start(); } private void connect() { Log.v(TAG, "Nextcloud Single sign-on connect() called [" + Thread.currentThread().getName() + "]"); if (mDestroyed) { throw new IllegalStateException("API already destroyed! You cannot reuse a stopped API instance"); } // Disconnect if connected if (mBound.get()) { stop(); } try { Intent intentService = new Intent(); intentService.setComponent(new ComponentName("com.nextcloud.client", "com.owncloud.android.services.AccountManagerService")); if (!mContext.bindService(intentService, mConnection, Context.BIND_AUTO_CREATE)) { Log.d(TAG, "Binding to AccountManagerService returned false"); throw new IllegalStateException("Binding to AccountManagerService returned false"); } } catch (SecurityException e) { Log.e(TAG, "can't bind to AccountManagerService, check permission in Manifest"); mCallback.onError(e); } } public void stop() { gson = null; mDestroyed = true; mAccount = null; mCallback = null; // Unbind from the service if (mBound.get()) { if (mContext != null) { mContext.unbindService(mConnection); } else { Log.e(TAG, "Context was null, cannot unbind nextcloud single sign-on service connection!"); } mBound.set(false); mContext = null; } } /** * Class for interacting with the main interface of the service. */ private ServiceConnection mConnection = new ServiceConnection() { public void onServiceConnected(ComponentName className, IBinder service) { Log.v(TAG, "Nextcloud Single sign-on: onServiceConnected [" + Thread.currentThread().getName() + "]"); mService = IInputStreamService.Stub.asInterface(service); mBound.set(true); synchronized (mBound) { mBound.notifyAll(); } mCallback.onConnected(); } public void onServiceDisconnected(ComponentName className) { Log.e(TAG, "Nextcloud Single sign-on: ServiceDisconnected"); // This is called when the connection with the service has been // unexpectedly disconnected -- that is, its process crashed. mService = null; mBound.set(false); if (!mDestroyed) { connectApiWithBackoff(); } } }; private void waitForApi() throws NextcloudApiNotRespondingException { synchronized (mBound) { // If service is not bound yet.. wait if(!mBound.get()) { Log.v(TAG, "[waitForApi] - api not ready yet.. waiting [" + Thread.currentThread().getName() + "]"); try { mBound.wait(10000); // wait up to 10 seconds // If api is still not bound after 10 seconds.. throw an exception if(!mBound.get()) { throw new NextcloudApiNotRespondingException(); } } catch (InterruptedException ex) { Log.e(TAG, "WaitForAPI failed", ex); } } } networkRequest.stop(); } public <T> Observable<T> performRequestObservable(final Type type, final NextcloudRequest request) { return Observable.fromPublisher( s-> { try { s.onNext((T) performRequest(type, request)); s.onNext(performRequest(type, request)); s.onComplete(); } catch (Exception e) { s.onError(e); Loading @@ -227,17 +104,10 @@ public class NextcloudAPI { } } } return result; } public static <T> T deserializeObject(InputStream is) throws IOException, ClassNotFoundException { ObjectInputStream ois = new ObjectInputStream(is); T result = (T) ois.readObject(); return result; } /** * The InputStreams needs to be closed after reading from it * Loading @@ -246,93 +116,17 @@ public class NextcloudAPI { * @throws Exception or SSOException */ public InputStream performNetworkRequest(NextcloudRequest request) throws Exception { return performNetworkRequest(request, null); } /** * The InputStreams needs to be closed after reading from it * * @param request {@link NextcloudRequest} request to be executed on server via Files app * @param requestBodyInputStream inputstream to be sent to the server * @return InputStream answer from server as InputStream * @throws Exception or SSOException */ public InputStream performNetworkRequest(NextcloudRequest request, InputStream requestBodyInputStream) throws Exception { InputStream os = null; Exception exception; try { ParcelFileDescriptor output = performAidlNetworkRequest(request, requestBodyInputStream); os = new ParcelFileDescriptor.AutoCloseInputStream(output); exception = deserializeObject(os); } catch (ClassNotFoundException e) { //e.printStackTrace(); exception = e; } // Handle Remote Exceptions if (exception != null) { if (exception.getMessage() != null) { exception = parseNextcloudCustomException(exception); } throw exception; } return os; } /** * DO NOT CALL THIS METHOD DIRECTLY - use @link(performNetworkRequest) instead * * @param request * @return * @throws IOException */ private ParcelFileDescriptor performAidlNetworkRequest(NextcloudRequest request, InputStream requestBodyInputStream) throws IOException, RemoteException, NextcloudApiNotRespondingException { // Check if we are on the main thread if(Looper.myLooper() == Looper.getMainLooper()) { throw new NetworkOnMainThreadException(); } // Wait for api to be initialized waitForApi(); // Log.d(TAG, request.url); request.setAccountName(getAccountName()); request.setToken(getAccountToken()); try (ByteArrayOutputStream baos = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(baos) ) { oos.writeObject(request); try (InputStream is = new ByteArrayInputStream(baos.toByteArray()); ParcelFileDescriptor input = ParcelFileDescriptorUtil.pipeFrom(is, thread -> Log.d(TAG, "copy data from service finished"))) { ParcelFileDescriptor requestBodyParcelFileDescriptor = null; if (requestBodyInputStream != null) { requestBodyParcelFileDescriptor = ParcelFileDescriptorUtil.pipeFrom( requestBodyInputStream, thread -> Log.d(TAG, "copy data from service finished")); } ParcelFileDescriptor output; if(requestBodyParcelFileDescriptor != null) { output = mService.performNextcloudRequestAndBodyStream(input, requestBodyParcelFileDescriptor); } else { output = mService.performNextcloudRequest(input); } return output; } } return networkRequest.performNetworkRequest(request, null); } /* public static <T> T deserializeObjectAndCloseStream(InputStream is) throws IOException, ClassNotFoundException { try (ObjectInputStream ois = new ObjectInputStream(is)) { return (T) ois.readObject(); } } */ protected Gson getGson() { return gson; Loading