001package com.gigya.android.sdk.session;
002
003import android.app.Activity;
004import android.app.Application;
005import android.content.Intent;
006import android.os.Bundle;
007import android.support.v4.content.LocalBroadcastManager;
008
009import com.gigya.android.sdk.Config;
010import com.gigya.android.sdk.GigyaDefinitions;
011import com.gigya.android.sdk.GigyaInterceptor;
012import com.gigya.android.sdk.GigyaLogger;
013import com.gigya.android.sdk.account.IAccountService;
014import com.gigya.android.sdk.api.ApiService;
015import com.gigya.android.sdk.api.GigyaApiRequest;
016import com.gigya.android.sdk.api.GigyaApiResponse;
017import com.gigya.android.sdk.api.IApiRequestFactory;
018import com.gigya.android.sdk.api.IApiService;
019import com.gigya.android.sdk.network.GigyaError;
020import com.gigya.android.sdk.network.adapter.RestAdapter;
021import com.gigya.android.sdk.ui.Presenter;
022
023import java.util.Date;
024import java.util.HashMap;
025import java.util.Map;
026import java.util.Timer;
027import java.util.TimerTask;
028import java.util.concurrent.TimeUnit;
029
030public class SessionVerificationService implements ISessionVerificationService {
031
032    private static final String LOG_TAG = "SessionVerificationService";
033
034    final private Application _context;
035    final private Config _config;
036    final private ISessionService _sessionService;
037    final private IAccountService _accountService;
038    final private IApiService _apiService;
039    final private IApiRequestFactory _requestFactory;
040
041    public SessionVerificationService(Application context,
042                                      Config config,
043                                      ISessionService sessionService,
044                                      IAccountService accountService,
045                                      IApiService apiService,
046                                      IApiRequestFactory requestFactory) {
047        _context = context;
048        _config = config;
049        _sessionService = sessionService;
050        _accountService = accountService;
051        _apiService = apiService;
052        _requestFactory = requestFactory;
053
054        /*
055        Add a setSession interception in order to make sure that the service starts when a new
056        Session is being set.
057         */
058        _sessionService.addInterceptor(new GigyaInterceptor("VERIFY_LOGIN") {
059            @Override
060            public void intercept() {
061                updateInterval();
062                if (_sessionService.isValid() && _verificationInterval != 0) {
063                    restart();
064                }
065            }
066        });
067    }
068
069    private long _verificationInterval;
070    private long _lastRequestTimestamp = 0;
071    private Timer _timer;
072
073    @Override
074    public void updateInterval() {
075        /*
076        Update the current interval as set in the configuration.
077         */
078        _verificationInterval = TimeUnit.SECONDS.toMillis(_config.getSessionVerificationInterval());
079    }
080
081    @Override
082    public void registerActivityLifecycleCallbacks() {
083        _context.registerActivityLifecycleCallbacks(new Application.ActivityLifecycleCallbacks() {
084
085            private int activityReferences = 0;
086            private boolean isActivityChangingConfigurations = false;
087
088            @Override
089            public void onActivityCreated(Activity activity, Bundle bundle) {
090                // Stub.
091            }
092
093            @Override
094            public void onActivityStarted(Activity activity) {
095                if (++activityReferences == 1 && !isActivityChangingConfigurations) {
096
097                    // App enters foreground
098                    GigyaLogger.info(LOG_TAG, "Application lifecycle - Foreground");
099                    if (_sessionService.isValid()) {
100                        // Will start session countdown timer if the current session contains an expiration time.
101                        _sessionService.startSessionCountdownTimerIfNeeded();
102                        // Make sure interval is updated correctly.
103                        updateInterval();
104                        // Session verification is only relevant when user is logged in.
105                        start();
106                    }
107                }
108            }
109
110            @Override
111            public void onActivityResumed(Activity activity) {
112                // Stub. Can track the current resumed activity.
113            }
114
115            @Override
116            public void onActivityPaused(Activity activity) {
117                // Stub.
118            }
119
120            @Override
121            public void onActivityStopped(Activity activity) {
122                isActivityChangingConfigurations = activity.isChangingConfigurations();
123                if (--activityReferences == 0 && !isActivityChangingConfigurations) {
124                    // App enters background
125                    GigyaLogger.info(LOG_TAG, "Application lifecycle - Background");
126                    // Make sure to cancel the session expiration countdown timer (if live).
127                    _sessionService.cancelSessionCountdownTimer();
128                    stop();
129                }
130            }
131
132            @Override
133            public void onActivitySaveInstanceState(Activity activity, Bundle bundle) {
134                // Stub.
135            }
136
137            @Override
138            public void onActivityDestroyed(Activity activity) {
139                if (--activityReferences == 0 && !isActivityChangingConfigurations) {
140                    // Flush the Presenter statics just in case. When all activities have been destroyed.
141                    Presenter.flush();
142                }
143            }
144        });
145    }
146
147    @Override
148    public void start() {
149        if (_verificationInterval == 0) {
150            GigyaLogger.debug(LOG_TAG, "start: Verification interval is 0. Verification flow irrelevant");
151            return;
152        }
153        GigyaLogger.debug(LOG_TAG, "start: Verification interval is " + TimeUnit.MILLISECONDS.toSeconds(_verificationInterval) + " seconds");
154        if (_timer == null) {
155            _timer = new Timer();
156        }
157        _timer.scheduleAtFixedRate(new TimerTask() {
158            @SuppressWarnings("unchecked") // Generic reference is irrelevant.
159            @Override
160            public void run() {
161                if (!Thread.currentThread().isInterrupted()) {
162                    GigyaLogger.debug(LOG_TAG, "dispatching verifyLogin request " + new Date().toString());
163                    _lastRequestTimestamp = System.currentTimeMillis();
164                    final Map<String, Object> params = new HashMap<>();
165                    params.put("include", "identities-all,loginIDs,profile,email,data");
166                    final GigyaApiRequest request = _requestFactory.create(
167                            GigyaDefinitions.API.API_VERIFY_LOGIN,
168                            params,
169                            RestAdapter.HttpMethod.POST);
170                    _apiService.send(request, false, new ApiService.IApiServiceResponse() {
171                        @Override
172                        public void onApiSuccess(GigyaApiResponse response) {
173                            if (response.getErrorCode() == 0) {
174                                GigyaLogger.debug(LOG_TAG, "verifyLogin success");
175                            } else {
176                                evaluateVerifyLoginError(GigyaError.fromResponse(response));
177                            }
178                        }
179
180                        @Override
181                        public void onApiError(GigyaError gigyaError) {
182                            evaluateVerifyLoginError(gigyaError);
183                        }
184                    });
185                }
186            }
187        }, getInitialDelay(), _verificationInterval);
188    }
189
190    @Override
191    public void stop() {
192        GigyaLogger.debug(LOG_TAG, "stop: ");
193        if (_timer != null) {
194            _timer.cancel();
195            _timer.purge();
196            _timer = null;
197        }
198        System.gc();
199    }
200
201    private void restart() {
202        stop();
203        start();
204    }
205
206    /**
207     * get the initial timer delay.
208     *
209     * @return Initial timer task delay in milliseconds.
210     */
211    @Override
212    public long getInitialDelay() {
213        if (_lastRequestTimestamp == 0) {
214            return _verificationInterval;
215        }
216        final long interval = _verificationInterval;
217        final long delta = System.currentTimeMillis() - _lastRequestTimestamp;
218        final long initialDelay = delta > interval ? 0 : interval - delta;
219        GigyaLogger.debug(LOG_TAG, "getInitialDelay: " + TimeUnit.MILLISECONDS.toSeconds(initialDelay) + " seconds");
220        return initialDelay;
221    }
222
223    /**
224     * Evaluate notifyLogin endpoint error.
225     * Will ignore network error.
226     *
227     * @param error GigyaError received.
228     */
229    private void evaluateVerifyLoginError(GigyaError error) {
230        final int errorCode = error.getErrorCode();
231        if (errorCode == GigyaError.Codes.ERROR_NETWORK) {
232            return;
233        }
234        GigyaLogger.error(LOG_TAG, "evaluateVerifyLoginError: error = " + errorCode + " session invalid -> invalidate & notify");
235        notifyInvalidSession();
236    }
237
238    /**
239     * Session no longer valid.
240     * 1. Clear saved session & invalidate cached account.
241     * 2. Broadcast a local event to notify that the session is invalid.
242     */
243    private void notifyInvalidSession() {
244        GigyaLogger.debug(LOG_TAG, "notifyInvalidSession: Invalidating session and cached account. Trigger local broadcast");
245        // Clear current session & cached account.
246        _sessionService.clear(true);
247        _accountService.invalidateAccount();
248        // Send "session invalid" local broadcast & flush the timer.
249        LocalBroadcastManager.getInstance(_context)
250                .sendBroadcast(new Intent(GigyaDefinitions.Broadcasts.INTENT_ACTION_SESSION_INVALID));
251        stop();
252    }
253}