Loading src/main/java/com/nextcloud/android/sso/api/NextcloudAPI.java +22 −4 Original line number Original line Diff line number Diff line Loading @@ -5,6 +5,7 @@ import android.content.Context; import android.content.Intent; import android.content.Intent; import android.content.ServiceConnection; import android.content.ServiceConnection; import android.os.IBinder; import android.os.IBinder; import android.os.Looper; import android.os.ParcelFileDescriptor; import android.os.ParcelFileDescriptor; import android.os.RemoteException; import android.os.RemoteException; import android.util.Log; import android.util.Log; Loading @@ -20,6 +21,7 @@ import com.nextcloud.android.sso.exceptions.NextcloudHttpRequestFailedException; import com.nextcloud.android.sso.exceptions.NextcloudInvalidRequestUrlException; import com.nextcloud.android.sso.exceptions.NextcloudInvalidRequestUrlException; import com.nextcloud.android.sso.exceptions.NextcloudUnsupportedMethodException; import com.nextcloud.android.sso.exceptions.NextcloudUnsupportedMethodException; import com.nextcloud.android.sso.exceptions.TokenMismatchException; import com.nextcloud.android.sso.exceptions.TokenMismatchException; import com.nextcloud.android.sso.helper.ExponentialBackoff; import com.nextcloud.android.sso.model.SingleSignOnAccount; import com.nextcloud.android.sso.model.SingleSignOnAccount; import org.reactivestreams.Publisher; import org.reactivestreams.Publisher; Loading Loading @@ -64,9 +66,13 @@ public class NextcloudAPI { void onError(Exception ex); void onError(Exception ex); } } public NextcloudAPI(SingleSignOnAccount account, Gson gson) { public NextcloudAPI(Context context, SingleSignOnAccount account, Gson gson, ApiConnectedListener callback) { this.context = context; // memory leaks..?? this.mAccount = account; this.mAccount = account; this.gson = gson; this.gson = gson; this.mCallback = callback; connectApiWithBackoff(); } } private static final String TAG = NextcloudAPI.class.getCanonicalName(); private static final String TAG = NextcloudAPI.class.getCanonicalName(); Loading @@ -76,6 +82,7 @@ public class NextcloudAPI { private boolean mBound = false; // Flag indicating whether we have called bind on the service private boolean mBound = false; // Flag indicating whether we have called bind on the service private SingleSignOnAccount mAccount; private SingleSignOnAccount mAccount; private ApiConnectedListener mCallback; private ApiConnectedListener mCallback; private Context context; private String getAccountName() { private String getAccountName() { return mAccount.name; return mAccount.name; Loading @@ -85,9 +92,16 @@ public class NextcloudAPI { return mAccount.token; return mAccount.token; } } public void start(Context context, ApiConnectedListener callback) { private void connectApiWithBackoff() { this.mCallback = callback; new ExponentialBackoff(1000, 10000, 2, 5, Looper.getMainLooper(), new Runnable() { @Override public void run() { connect(); } }).start(); } private void connect() { // Disconnect if connected // Disconnect if connected if(mBound) { if(mBound) { stop(context); stop(context); Loading @@ -98,10 +112,11 @@ public class NextcloudAPI { intentService.setComponent(new ComponentName("com.nextcloud.client", "com.owncloud.android.services.AccountManagerService")); intentService.setComponent(new ComponentName("com.nextcloud.client", "com.owncloud.android.services.AccountManagerService")); if (!context.bindService(intentService, mConnection, Context.BIND_AUTO_CREATE)) { if (!context.bindService(intentService, mConnection, Context.BIND_AUTO_CREATE)) { Log.d(TAG, "Binding to AccountManagerService returned false"); Log.d(TAG, "Binding to AccountManagerService returned false"); throw new IllegalStateException("Binding to AccountManagerService returned false"); } } } catch (SecurityException e) { } catch (SecurityException e) { Log.e(TAG, "can't bind to AccountManagerService, check permission in Manifest"); Log.e(TAG, "can't bind to AccountManagerService, check permission in Manifest"); callback.onError(e); mCallback.onError(e); } } } } Loading Loading @@ -133,6 +148,9 @@ public class NextcloudAPI { // unexpectedly disconnected -- that is, its process crashed. // unexpectedly disconnected -- that is, its process crashed. mService = null; mService = null; mBound = false; mBound = false; connectApiWithBackoff(); } } }; }; Loading src/main/java/com/nextcloud/android/sso/helper/ExponentialBackoff.java 0 → 100644 +144 −0 Original line number Original line Diff line number Diff line /* * Copyright (C) 2017 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.nextcloud.android.sso.helper; import android.os.Handler; import android.os.Looper; import android.support.annotation.NonNull; import android.support.annotation.VisibleForTesting; import java.security.InvalidParameterException; /** The implementation of exponential backoff with jitter applied. */ public class ExponentialBackoff { private int mRetryCounter; private long mStartDelayMs; private long mMaximumDelayMs; private long mCurrentDelayMs; private int mMaxRetries; private int mMultiplier; private final Runnable mRunnable; private final Handler mHandler; /** * Implementation of Handler methods, Adapter for testing (can't spy on final methods). */ private HandlerAdapter mHandlerAdapter = new HandlerAdapter() { @Override public boolean postDelayed(Runnable runnable, long delayMillis) { return mHandler.postDelayed(runnable, delayMillis); } @Override public void removeCallbacks(Runnable runnable) { mHandler.removeCallbacks(runnable); } }; /** * Need to spy final methods for testing. */ public interface HandlerAdapter { boolean postDelayed(Runnable runnable, long delayMillis); void removeCallbacks(Runnable runnable); } public ExponentialBackoff( long initialDelayMs, long maximumDelayMs, int multiplier, int maxRetries, @NonNull Looper looper, @NonNull Runnable runnable) { this(initialDelayMs, maximumDelayMs, multiplier, maxRetries, new Handler(looper), runnable); } public ExponentialBackoff( long initialDelayMs, long maximumDelayMs, int multiplier, int maxRetries, @NonNull Handler handler, @NonNull Runnable runnable) { mRetryCounter = 0; mStartDelayMs = initialDelayMs; mMaximumDelayMs = maximumDelayMs; mMultiplier = multiplier; mMaxRetries = maxRetries; mHandler = handler; mRunnable = new WrapperRunnable(runnable); if(initialDelayMs <= 0) { throw new InvalidParameterException("initialDelayMs should not be less or equal to 0"); } } /** Starts the backoff, the runnable will be executed after {@link #mStartDelayMs}. */ public void start() { mRetryCounter = 0; mHandlerAdapter.removeCallbacks(mRunnable); mHandlerAdapter.postDelayed(mRunnable, 0); } /** Stops the backoff, all pending messages will be removed from the message queue. */ public void stop() { mRetryCounter = 0; mHandlerAdapter.removeCallbacks(mRunnable); } /** Should call when the retry action has failed and we want to retry after a longer delay. */ public void notifyFailed() { if(mRetryCounter > mMaxRetries) { stop(); } else { mRetryCounter++; long temp = Math.min( mMaximumDelayMs, (long) (mStartDelayMs * Math.pow(mMultiplier, mRetryCounter))); mCurrentDelayMs = (long) (((1 + Math.random()) / 2) * temp); mHandlerAdapter.removeCallbacks(mRunnable); mHandlerAdapter.postDelayed(mRunnable, mCurrentDelayMs); } } class WrapperRunnable implements Runnable { private final Runnable runnable; public WrapperRunnable(Runnable runnable) { this.runnable = runnable; } @Override public void run() { try { runnable.run(); } catch (Exception ex) { notifyFailed(); } } } /** Returns the delay for the most recently posted message. */ public long getCurrentDelay() { return mCurrentDelayMs; } @VisibleForTesting public void setHandlerAdapter(HandlerAdapter a) { mHandlerAdapter = a; } } No newline at end of file Loading
src/main/java/com/nextcloud/android/sso/api/NextcloudAPI.java +22 −4 Original line number Original line Diff line number Diff line Loading @@ -5,6 +5,7 @@ import android.content.Context; import android.content.Intent; import android.content.Intent; import android.content.ServiceConnection; import android.content.ServiceConnection; import android.os.IBinder; import android.os.IBinder; import android.os.Looper; import android.os.ParcelFileDescriptor; import android.os.ParcelFileDescriptor; import android.os.RemoteException; import android.os.RemoteException; import android.util.Log; import android.util.Log; Loading @@ -20,6 +21,7 @@ import com.nextcloud.android.sso.exceptions.NextcloudHttpRequestFailedException; import com.nextcloud.android.sso.exceptions.NextcloudInvalidRequestUrlException; import com.nextcloud.android.sso.exceptions.NextcloudInvalidRequestUrlException; import com.nextcloud.android.sso.exceptions.NextcloudUnsupportedMethodException; import com.nextcloud.android.sso.exceptions.NextcloudUnsupportedMethodException; import com.nextcloud.android.sso.exceptions.TokenMismatchException; import com.nextcloud.android.sso.exceptions.TokenMismatchException; import com.nextcloud.android.sso.helper.ExponentialBackoff; import com.nextcloud.android.sso.model.SingleSignOnAccount; import com.nextcloud.android.sso.model.SingleSignOnAccount; import org.reactivestreams.Publisher; import org.reactivestreams.Publisher; Loading Loading @@ -64,9 +66,13 @@ public class NextcloudAPI { void onError(Exception ex); void onError(Exception ex); } } public NextcloudAPI(SingleSignOnAccount account, Gson gson) { public NextcloudAPI(Context context, SingleSignOnAccount account, Gson gson, ApiConnectedListener callback) { this.context = context; // memory leaks..?? this.mAccount = account; this.mAccount = account; this.gson = gson; this.gson = gson; this.mCallback = callback; connectApiWithBackoff(); } } private static final String TAG = NextcloudAPI.class.getCanonicalName(); private static final String TAG = NextcloudAPI.class.getCanonicalName(); Loading @@ -76,6 +82,7 @@ public class NextcloudAPI { private boolean mBound = false; // Flag indicating whether we have called bind on the service private boolean mBound = false; // Flag indicating whether we have called bind on the service private SingleSignOnAccount mAccount; private SingleSignOnAccount mAccount; private ApiConnectedListener mCallback; private ApiConnectedListener mCallback; private Context context; private String getAccountName() { private String getAccountName() { return mAccount.name; return mAccount.name; Loading @@ -85,9 +92,16 @@ public class NextcloudAPI { return mAccount.token; return mAccount.token; } } public void start(Context context, ApiConnectedListener callback) { private void connectApiWithBackoff() { this.mCallback = callback; new ExponentialBackoff(1000, 10000, 2, 5, Looper.getMainLooper(), new Runnable() { @Override public void run() { connect(); } }).start(); } private void connect() { // Disconnect if connected // Disconnect if connected if(mBound) { if(mBound) { stop(context); stop(context); Loading @@ -98,10 +112,11 @@ public class NextcloudAPI { intentService.setComponent(new ComponentName("com.nextcloud.client", "com.owncloud.android.services.AccountManagerService")); intentService.setComponent(new ComponentName("com.nextcloud.client", "com.owncloud.android.services.AccountManagerService")); if (!context.bindService(intentService, mConnection, Context.BIND_AUTO_CREATE)) { if (!context.bindService(intentService, mConnection, Context.BIND_AUTO_CREATE)) { Log.d(TAG, "Binding to AccountManagerService returned false"); Log.d(TAG, "Binding to AccountManagerService returned false"); throw new IllegalStateException("Binding to AccountManagerService returned false"); } } } catch (SecurityException e) { } catch (SecurityException e) { Log.e(TAG, "can't bind to AccountManagerService, check permission in Manifest"); Log.e(TAG, "can't bind to AccountManagerService, check permission in Manifest"); callback.onError(e); mCallback.onError(e); } } } } Loading Loading @@ -133,6 +148,9 @@ public class NextcloudAPI { // unexpectedly disconnected -- that is, its process crashed. // unexpectedly disconnected -- that is, its process crashed. mService = null; mService = null; mBound = false; mBound = false; connectApiWithBackoff(); } } }; }; Loading
src/main/java/com/nextcloud/android/sso/helper/ExponentialBackoff.java 0 → 100644 +144 −0 Original line number Original line Diff line number Diff line /* * Copyright (C) 2017 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ package com.nextcloud.android.sso.helper; import android.os.Handler; import android.os.Looper; import android.support.annotation.NonNull; import android.support.annotation.VisibleForTesting; import java.security.InvalidParameterException; /** The implementation of exponential backoff with jitter applied. */ public class ExponentialBackoff { private int mRetryCounter; private long mStartDelayMs; private long mMaximumDelayMs; private long mCurrentDelayMs; private int mMaxRetries; private int mMultiplier; private final Runnable mRunnable; private final Handler mHandler; /** * Implementation of Handler methods, Adapter for testing (can't spy on final methods). */ private HandlerAdapter mHandlerAdapter = new HandlerAdapter() { @Override public boolean postDelayed(Runnable runnable, long delayMillis) { return mHandler.postDelayed(runnable, delayMillis); } @Override public void removeCallbacks(Runnable runnable) { mHandler.removeCallbacks(runnable); } }; /** * Need to spy final methods for testing. */ public interface HandlerAdapter { boolean postDelayed(Runnable runnable, long delayMillis); void removeCallbacks(Runnable runnable); } public ExponentialBackoff( long initialDelayMs, long maximumDelayMs, int multiplier, int maxRetries, @NonNull Looper looper, @NonNull Runnable runnable) { this(initialDelayMs, maximumDelayMs, multiplier, maxRetries, new Handler(looper), runnable); } public ExponentialBackoff( long initialDelayMs, long maximumDelayMs, int multiplier, int maxRetries, @NonNull Handler handler, @NonNull Runnable runnable) { mRetryCounter = 0; mStartDelayMs = initialDelayMs; mMaximumDelayMs = maximumDelayMs; mMultiplier = multiplier; mMaxRetries = maxRetries; mHandler = handler; mRunnable = new WrapperRunnable(runnable); if(initialDelayMs <= 0) { throw new InvalidParameterException("initialDelayMs should not be less or equal to 0"); } } /** Starts the backoff, the runnable will be executed after {@link #mStartDelayMs}. */ public void start() { mRetryCounter = 0; mHandlerAdapter.removeCallbacks(mRunnable); mHandlerAdapter.postDelayed(mRunnable, 0); } /** Stops the backoff, all pending messages will be removed from the message queue. */ public void stop() { mRetryCounter = 0; mHandlerAdapter.removeCallbacks(mRunnable); } /** Should call when the retry action has failed and we want to retry after a longer delay. */ public void notifyFailed() { if(mRetryCounter > mMaxRetries) { stop(); } else { mRetryCounter++; long temp = Math.min( mMaximumDelayMs, (long) (mStartDelayMs * Math.pow(mMultiplier, mRetryCounter))); mCurrentDelayMs = (long) (((1 + Math.random()) / 2) * temp); mHandlerAdapter.removeCallbacks(mRunnable); mHandlerAdapter.postDelayed(mRunnable, mCurrentDelayMs); } } class WrapperRunnable implements Runnable { private final Runnable runnable; public WrapperRunnable(Runnable runnable) { this.runnable = runnable; } @Override public void run() { try { runnable.run(); } catch (Exception ex) { notifyFailed(); } } } /** Returns the delay for the most recently posted message. */ public long getCurrentDelay() { return mCurrentDelayMs; } @VisibleForTesting public void setHandlerAdapter(HandlerAdapter a) { mHandlerAdapter = a; } } No newline at end of file