1919import java .net .URI ;
2020import java .util .HashMap ;
2121import java .util .concurrent .Future ;
22+ import java .util .concurrent .ScheduledFuture ;
23+ import java .util .concurrent .ScheduledThreadPoolExecutor ;
24+ import java .util .concurrent .TimeUnit ;
2225
2326import io .realm .ErrorCode ;
2427import io .realm .ObjectServerError ;
@@ -99,14 +102,18 @@ public final class ObjectServerSession {
99102 private long nativeSessionPointer ;
100103 private final ObjectServerUser user ;
101104 RealmAsyncTask networkRequest ;
105+ private RealmAsyncTask refreshTokenTask ;
106+ private RealmAsyncTask refreshTokenNetworkRequest ;
102107 NetworkStateReceiver .ConnectionListener networkListener ;
103108 private SyncPolicy syncPolicy ;
104109
105110 // Keeping track of current FSM state
106111 private SessionState currentStateDescription ;
107112 private FsmState currentState ;
108113 private SyncSession userSession ;
109- private SyncSession publicSession ;
114+
115+ private final static ScheduledThreadPoolExecutor REFRESH_TOKENS_EXECUTOR = new ScheduledThreadPoolExecutor (1 );
116+ private final static long REFRESH_MARGIN_DELAY = TimeUnit .SECONDS .toMillis (10 );
110117
111118 /**
112119 * Creates a new Object Server Session.
@@ -166,6 +173,8 @@ public synchronized void start() {
166173 * Stops the session. The session can no longer be used.
167174 */
168175 public synchronized void stop () {
176+ // tries to stop any scheduled access_token refresh
177+ clearScheduledAccessTokenRefresh ();
169178 currentState .onStop ();
170179 }
171180
@@ -238,6 +247,25 @@ void stopNativeSession() {
238247 nativeUnbind (nativeSessionPointer );
239248 nativeSessionPointer = 0 ;
240249 }
250+ clearScheduledAccessTokenRefresh ();
251+ }
252+
253+ // It is an error to call this function before calling Client::bind() state
254+ private boolean updateSessionAccessToken (String userToken ) {
255+ if (nativeSessionPointer != 0 && isBound ()) {
256+ nativeRefresh (nativeSessionPointer , userToken );
257+ return true ;
258+ }
259+ return false ;
260+ }
261+
262+ private void clearScheduledAccessTokenRefresh () {
263+ if (refreshTokenTask != null ) {
264+ refreshTokenTask .cancel ();
265+ }
266+ if (refreshTokenNetworkRequest != null ) {
267+ refreshTokenNetworkRequest .cancel ();
268+ }
241269 }
242270
243271 void removeAccessToken () {
@@ -260,6 +288,10 @@ void authenticateRealm(final Runnable onSuccess, final SyncSession.ErrorHandler
260288 if (networkRequest != null ) {
261289 networkRequest .cancel ();
262290 }
291+ // clear any previously scheduled refresh access_token
292+ // since we're going to obtain a new refresh_token
293+ clearScheduledAccessTokenRefresh ();
294+
263295 // Authenticate in a background thread. This allows incremental backoff and retries in a safe manner.
264296 Future <?> task = SyncManager .NETWORK_POOL_EXECUTOR .submit (new ExponentialBackoffTask <AuthenticateResponse >() {
265297 @ Override
@@ -279,6 +311,8 @@ protected void onSuccess(AuthenticateResponse response) {
279311 configuration .shouldDeleteRealmOnLogout ()
280312 );
281313 user .addRealm (configuration .getServerUrl (), desc );
314+ // schedule a token refresh before it expires
315+ scheduleRefreshAccessToken (response .getAccessToken ().expiresMs ());
282316 onSuccess .run ();
283317 }
284318
@@ -290,6 +324,78 @@ protected void onError(AuthenticateResponse response) {
290324 networkRequest = new RealmAsyncTaskImpl (task , SyncManager .NETWORK_POOL_EXECUTOR );
291325 }
292326
327+ private void scheduleRefreshAccessToken (long expireDateInMs ) {
328+ // calculate the delay time before which we should refresh the access_token,
329+ // we adjust to 10 second to proactively refresh the access_token before the session
330+ // hit the expire date on the token
331+ long refreshAfter = expireDateInMs - System .currentTimeMillis () - REFRESH_MARGIN_DELAY ;
332+ if (refreshAfter < 0 ) {
333+ // Token already expired
334+ RealmLog .debug ("Expires time already reached for the access token, refreshing now" );
335+ refreshAccessToken ();
336+
337+ } else {
338+ RealmLog .debug ("Scheduling an access_token refresh in " + (refreshAfter ) + " milliseconds" );
339+ if (refreshTokenTask != null ) {
340+ refreshTokenTask .cancel ();
341+ }
342+
343+ ScheduledFuture <?> task = REFRESH_TOKENS_EXECUTOR .schedule (new Runnable () {
344+ @ Override
345+ public void run () {
346+ refreshAccessToken ();
347+ }
348+ }, refreshAfter , TimeUnit .MILLISECONDS );
349+ refreshTokenTask = new RealmAsyncTaskImpl (task , REFRESH_TOKENS_EXECUTOR );
350+ }
351+ }
352+
353+ // Authenticate by getting access tokens for the specific Realm
354+ private void refreshAccessToken () {
355+ // Authenticate in a background thread. This allows incremental backoff and retries in a safe manner.
356+ if (refreshTokenNetworkRequest != null ) {
357+ refreshTokenNetworkRequest .cancel ();
358+ }
359+ Future <?> task = SyncManager .NETWORK_POOL_EXECUTOR .submit (new ExponentialBackoffTask <AuthenticateResponse >() {
360+ @ Override
361+ protected AuthenticateResponse execute () {
362+ return authServer .refreshUser (user .getUserToken (), configuration .getServerUrl (), user .getAuthenticationUrl ());
363+ }
364+
365+ @ Override
366+ protected void onSuccess (AuthenticateResponse response ) {
367+ synchronized (ObjectServerSession .this ) {
368+ RealmLog .debug ("Access Token refreshed successfully" );
369+ if (updateSessionAccessToken (response .getAccessToken ().value ())) {
370+ RealmLog .debug ("Token applied" );
371+ // only schedule an update if the token was updated.
372+ // The callback might return will the session state is not BOUND
373+ // in this case we'll wait for the new session state to transition to
374+ // BOUND, which will schedule a refresh in the process
375+
376+ // this will also avoid updating a stopped session
377+
378+ // replaced the user old access_token
379+ ObjectServerUser .AccessDescription desc = new ObjectServerUser .AccessDescription (
380+ response .getAccessToken (),
381+ configuration .getPath (),
382+ configuration .shouldDeleteRealmOnLogout ()
383+ );
384+ user .addRealm (configuration .getServerUrl (), desc );
385+ // schedule the next refresh
386+ scheduleRefreshAccessToken (response .getAccessToken ().expiresMs ());
387+ }
388+ }
389+ }
390+
391+ @ Override
392+ protected void onError (AuthenticateResponse response ) {
393+ RealmLog .error ("Unrecoverable error, while refreshing the access Token (" + response .getError ().toString () + ") reschedule will not happen" );
394+ }
395+ });
396+ refreshTokenNetworkRequest = new RealmAsyncTaskImpl (task , SyncManager .NETWORK_POOL_EXECUTOR );
397+ }
398+
293399 /**
294400 * Checks if a user has valid credentials for accessing this Realm.
295401 *
0 commit comments