001/* ******************************************************************************
002 * Copyright (c) 2002-2011  econda GmbH Karlsruhe
003 * All rights reserved.
004 * 
005 * econda GmbH
006 * Eisenlohrstr. 43
007 * 76135 Karlsruhe
008 * Tel. +49 (721) 663035-35
009 * support@econda.de
010 * 
011 * Redistribution and use in source and binary forms, with or without modification,
012 * are permitted provided that the following conditions are met:
013 * 
014 *    * Redistributions of source code must retain the above copyright notice,
015 *       this list of conditions and the following disclaimer.
016 *    * Redistributions in binary form must reproduce the above copyright notice,
017 *       this list of conditions and the following disclaimer in the documentation
018 *       and/or other materials provided with the distribution.
019 *    * Neither the name of the ECONDA GmbH nor the names of its contributors may
020 *       be used to endorse or promote products derived from this software without
021 *       specific prior written permission.
022 * 
023 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
024 * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
025 * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
026 * IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
027 * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
028 * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
029 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF
030 * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
031 * OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
032 * OF THE POSSIBILITY OF SUCH DAMAGE.
033 *******************************************************************************/
034package de.econda.droid;
035
036import android.content.Context;
037import android.content.SharedPreferences;
038import android.content.SharedPreferences.Editor;
039import android.os.Process;
040import android.util.Log;
041
042import java.util.ArrayList;
043import java.util.List;
044import java.util.Random;
045
046import de.econda.droid.impl.Constants;
047import de.econda.droid.impl.RandomIdGenerator;
048import de.econda.droid.impl.RequestExecutor;
049import de.econda.droid.impl.SubmitItem;
050
051
052/**
053 * <p>Session is the representation of one usage of your application.</p>
054 * 
055 * <p>You can collect and submit several PageView-Objects with a session.</p>
056 */
057public class Session {
058
059
060        private static final String PREFS_KEY_VISITOR_ID = "visitor-id";
061        private static final String PREFS_KEY_PRIVACY_MODE = "privacy.mode";
062        private static final String PREFS_KEY_PRIVACY_VISITORANALYTICS = "privacy.visitorAnalytics";
063        private static final String PREFS_KEY_PRIVACY_GEOIP = "privacy.geoip";
064        private static final String PREFS_KEY_PRIVACY_ANONYMIZE = "privacy.anonymize";
065        private static final String PREFS_KEY_SAMPLING_RATE = "sampling-rate";
066        private static final String PREFS_KEY_SAMPLING_TRACKING_ALLOWED = "sampling-tracking-allowed";
067        private static final String SHARED_PREFERENCES_NAME = "econda-tracking";
068        private static final String EMRID = "emrid";
069        private static final String EMSID = "emsid";
070        private static final String PAGELOAD_REQUEST_ID = "plReqId";
071
072        private List<SubmitItem> items;
073        private final String clientKey;
074        private final String logUrl;
075        private PrivacySettings privacySettings;
076        private final String hostName;
077        private String visitorId;
078        private Thread delayedExecutedSubmitTask;
079        private final boolean sampledIn;
080    private String sessionId;
081    private String requestIdPageLoad;
082        private int batchAutoTransmitTimeout;
083        private final Context applicationContext;
084
085
086        /**
087         * <p>Returns a fresh new Session.</p>
088         *
089         * This method return always a complete new Instance.
090         *
091         * When you retrieve a second Session Object via this method,
092         * the PageViews submitted by this second Session object will
093         * belong to another Session in Econda Analytics too.
094         *
095         * <p>Session is used to collect and submit PageView-Data.</p>
096         *
097         * <p>PageView-Data is added with method addPageView.</p>
098     *
099         * @param applicationContext  the Application or Activity context
100         * @param settings a settings object constructed by SettingsBuilder
101         */
102//    @RequiresPermission(
103//            allOf = {"android.permission.INTERNET", "android.permission.ACCESS_NETWORK_STATE"}
104//    )
105        public static Session createNewInstance(Context applicationContext, Settings settings) {
106                return new Session(applicationContext, settings);
107        }
108
109        private Session(Context context2, Settings settings) {
110                this.clientKey = settings.getClientKey();
111                this.logUrl = settings.isSecureTransmit() ? Constants.LOG_URL_SECURE : Constants.LOG_URL_UNSECURE;
112                this.applicationContext = context2.getApplicationContext();
113                this.hostName = settings.getCustomHostName() == null ? createHostNameFromApplicationName(applicationContext) : settings.getCustomHostName();
114                this.items = new ArrayList<>();
115                this.batchAutoTransmitTimeout = settings.getBatchAutoTransmitTimeout();
116                this.delayedExecutedSubmitTask = null;
117                this.sessionId = RandomIdGenerator.generateID();
118                this.privacySettings = readStoredPrivacySettings(settings.getDefaultPrivacySettingsNewUser());
119                this.visitorId = initVisitorId();
120                this.sampledIn = initSamplingRate(settings.getSamplingRate(), applicationContext);
121
122        }
123
124
125        public String getVisitorId() {
126                return visitorId;
127        }
128
129        public String getSessionId() {
130                return sessionId;
131        }
132
133        public void setSessionId(String sessionId) {
134                this.sessionId = sessionId;
135        }
136
137        public void setVisitorId(String visitorId) {
138        if (privacySettings.isVisitorAnalytics()) {
139                        SharedPreferences prefs = getSharedPreferences(applicationContext);
140                        Editor editor = prefs.edit();
141                        editor.putString(PREFS_KEY_VISITOR_ID, visitorId);
142                        editor.apply();
143
144                        this.visitorId = visitorId;
145                }
146        }
147
148        public synchronized void changePrivacySettings(PrivacySettings newPrivacySettings) {
149                boolean wasWithoutVisitorTracking = !this.privacySettings.isVisitorAnalytics();
150
151                this.privacySettings = newPrivacySettings;
152                savePrivacySettings();
153
154                if (wasWithoutVisitorTracking){
155                        this.visitorId = initVisitorId();
156                }
157
158                if (this.privacySettings.getSubmitMode() == SubmitMode.DO_NOT_TRACK){
159
160                        this.items.clear();
161
162                } else if (this.privacySettings.getSubmitMode() == SubmitMode.CACHE){
163
164                        if (this.delayedExecutedSubmitTask !=null){
165                                this.delayedExecutedSubmitTask.interrupt();
166                                this.delayedExecutedSubmitTask = null;
167                        }
168
169                } else if (items.size()>0){
170
171                        sendItemsPerhapsDelayed();
172
173                }
174        }
175
176
177        private static SharedPreferences getSharedPreferences(Context context) {
178                return context.getSharedPreferences(SHARED_PREFERENCES_NAME, Context.MODE_PRIVATE);
179        }
180
181        private String initVisitorId() {
182                SharedPreferences prefs = getSharedPreferences(applicationContext);
183                if (privacySettings.isVisitorAnalytics()) {
184
185                        if (prefs.contains(PREFS_KEY_VISITOR_ID)) {
186                                return prefs.getString(PREFS_KEY_VISITOR_ID, sessionId);
187                        } else {
188                                Editor editor = prefs.edit();
189                                editor.putString(PREFS_KEY_VISITOR_ID, sessionId);
190                                editor.apply();
191                                return sessionId;
192                        }
193                } else {
194                        Editor editor = prefs.edit();
195                        editor.remove(PREFS_KEY_VISITOR_ID);
196                        editor.apply();
197                        return null;
198                }
199        }
200
201        private PrivacySettings readStoredPrivacySettings(PrivacySettings defaultPrivacySettingsNewUser) {
202                try {
203                        SharedPreferences prefs = getSharedPreferences(applicationContext);
204
205                        SubmitMode submitMode;
206                        if (prefs.contains(PREFS_KEY_PRIVACY_MODE)) {
207                                submitMode = SubmitMode.valueOf(prefs.getString(PREFS_KEY_PRIVACY_MODE, defaultPrivacySettingsNewUser.getSubmitMode().name()));
208                        } else {
209                                submitMode = defaultPrivacySettingsNewUser.getSubmitMode();
210                        }
211                        boolean geoIP;
212                        if (prefs.contains(PREFS_KEY_PRIVACY_GEOIP)) {
213                                geoIP = prefs.getBoolean(PREFS_KEY_PRIVACY_GEOIP, defaultPrivacySettingsNewUser.isGeoIP());
214                        } else {
215                                geoIP = defaultPrivacySettingsNewUser.isGeoIP();
216                        }
217                        boolean visitorAnalytics;
218                        if (prefs.contains(PREFS_KEY_PRIVACY_VISITORANALYTICS)) {
219                                visitorAnalytics = prefs.getBoolean(PREFS_KEY_PRIVACY_VISITORANALYTICS, defaultPrivacySettingsNewUser.isVisitorAnalytics());
220                        } else {
221                                visitorAnalytics = defaultPrivacySettingsNewUser.isVisitorAnalytics();
222                        }
223                        boolean anonymizeBeforeTransmit;
224                        if (prefs.contains(PREFS_KEY_PRIVACY_ANONYMIZE)) {
225                                anonymizeBeforeTransmit = prefs.getBoolean(PREFS_KEY_PRIVACY_ANONYMIZE, defaultPrivacySettingsNewUser.isAnonymizeBeforeTransmit());
226                        } else {
227                                anonymizeBeforeTransmit = defaultPrivacySettingsNewUser.isAnonymizeBeforeTransmit();
228                        }
229
230
231            PrivacySettings newPrivacySettings =  new PrivacySettings(submitMode, visitorAnalytics, geoIP, anonymizeBeforeTransmit);
232            Log.d(Constants.LOG_TAG, "Read PrivacySettings from saved SharedPreferences:  " + newPrivacySettings + "  defaultPrivacySettingsNewUser=" + defaultPrivacySettingsNewUser);
233            return newPrivacySettings;
234                } catch (Exception e){
235                        Log.d(Constants.LOG_TAG, "Could not read PrivacySettings. Using default=" + defaultPrivacySettingsNewUser);
236                        return defaultPrivacySettingsNewUser;
237                }
238        }
239
240
241    private void savePrivacySettings() {
242
243        SharedPreferences prefs = getSharedPreferences(applicationContext);
244        Editor editor = prefs.edit();
245        editor.putString(PREFS_KEY_PRIVACY_MODE, privacySettings.getSubmitMode().name());
246        editor.putBoolean(PREFS_KEY_PRIVACY_VISITORANALYTICS, privacySettings.isVisitorAnalytics());
247                editor.putBoolean(PREFS_KEY_PRIVACY_GEOIP, privacySettings.isGeoIP());
248                editor.putBoolean(PREFS_KEY_PRIVACY_ANONYMIZE, privacySettings.isAnonymizeBeforeTransmit());
249        editor.apply();
250
251        Log.d(Constants.LOG_TAG, "saved PrivacySettings:  " + privacySettings);
252    }
253
254    public void clearStoredPrivacySettings() {
255
256        SharedPreferences prefs = getSharedPreferences(applicationContext);
257        Editor editor = prefs.edit();
258        editor.remove(PREFS_KEY_PRIVACY_MODE);
259        editor.remove(PREFS_KEY_PRIVACY_VISITORANALYTICS);
260                editor.remove(PREFS_KEY_PRIVACY_GEOIP);
261                editor.remove(PREFS_KEY_PRIVACY_ANONYMIZE);
262        editor.apply();
263
264        Log.d(Constants.LOG_TAG, "cleared stored PrivacySettings");
265    }
266
267    /**
268         *
269         * @param samplingRate The sampling rate. If samplingRate=5 each 5th Visitor will be analyzed.
270         * @param applicationContext the applicationContext of the Android app.
271         */
272        private static boolean initSamplingRate(int samplingRate, Context applicationContext){
273                if (samplingRate <1){
274                        Log.i(Constants.LOG_TAG, "samplingRate < 1 not allowed. Ignoring samplingRate");
275                        return true;
276                } else if (samplingRate == 1){
277                        SharedPreferences prefs = getSharedPreferences(applicationContext);
278                        Editor editor = prefs.edit();
279                        editor.remove(PREFS_KEY_SAMPLING_RATE);
280                        editor.remove(PREFS_KEY_SAMPLING_TRACKING_ALLOWED);
281                        editor.apply();
282                        return true;
283                } else {
284                        SharedPreferences prefs = getSharedPreferences(applicationContext);
285                        if (prefs.contains(PREFS_KEY_SAMPLING_RATE) && prefs.contains(PREFS_KEY_SAMPLING_TRACKING_ALLOWED)){
286                                int oldSamplingRate = prefs.getInt(PREFS_KEY_SAMPLING_RATE, 1);
287                                if (oldSamplingRate != samplingRate)
288                                        return setNewSamplingRate(samplingRate, prefs);
289                                else {
290                                        boolean sampledIn = prefs.getBoolean(PREFS_KEY_SAMPLING_TRACKING_ALLOWED, true);
291                                        Log.d(Constants.LOG_TAG, "samplingrate unchanged. read sampledIn from prefs.  samplingrate=" + samplingRate + " sampledIn=" + sampledIn);
292                                        return sampledIn;
293                                }
294                        } else {
295                                return setNewSamplingRate(samplingRate, prefs);
296                        }
297                }
298        }
299
300
301        private static boolean setNewSamplingRate(int samplingRate, SharedPreferences prefs) {
302                boolean sampledIn = new Random().nextDouble()*samplingRate < 1;
303                Log.d(Constants.LOG_TAG, "Setting new samplingrate=" + samplingRate + " sampledIn=" + sampledIn);
304                Editor editor = prefs.edit();
305                editor.putInt(PREFS_KEY_SAMPLING_RATE, samplingRate);
306                editor.putBoolean(PREFS_KEY_SAMPLING_TRACKING_ALLOWED, sampledIn);
307                editor.apply();
308                return sampledIn;
309        }
310        
311        /**
312         * 
313         * @param batchAutoTransmitTimeout timeout in seconds. after this time all collected data will be submitted.
314         */
315        public synchronized void setBatchAutoTransmitTimeout(int batchAutoTransmitTimeout){
316                this.batchAutoTransmitTimeout = batchAutoTransmitTimeout;
317        }
318        /**
319         * You can indicate by calling of this method, that the application user has restarted usage of your application. 
320         */
321        public void startNextSession(){
322                this.sessionId = RandomIdGenerator.generateID();                
323        }
324
325
326
327        private static String createHostNameFromApplicationName(Context context) {
328                CharSequence hostName2 = context.getPackageManager().getApplicationLabel(context.getApplicationInfo());
329                if (hostName2 == null)
330                        hostName2 = context.getPackageName();
331
332                return "Android/" + hostName2;
333        }
334
335
336
337    /**
338     * Append data to already submitted pageView.
339     * (After method addPageView with the original pageView is called)
340     *
341     * Not all Properties could be appended to the original pageView.
342     *
343     * Please see econda support documentation:
344     * In German : PI ergänzen
345     * In English : Extend PI
346     *
347     * https://support.econda.de/pages/viewpage.action?pageId=7249036
348     */
349    public synchronized void appendDataToPreviousPageView(PageView pageView) {
350        if (!sampledIn) {
351                        return;
352                }
353
354                if (privacySettings.getSubmitMode() == SubmitMode.DO_NOT_TRACK){
355                        return;
356                }
357
358        if (requestIdPageLoad != null) {
359            pageView.addProperty(PAGELOAD_REQUEST_ID, requestIdPageLoad);
360            pageView.addProperty(EMRID, RandomIdGenerator.generateID());
361            pageView.addProperty(EMSID, sessionId);
362
363            submitItem(pageView);
364        }
365    }
366
367    /**
368     * Adds a pageView to Session
369     *
370     * pageView is submitted immediately or stored for later submission (depends on settings).
371     *
372     */
373        public synchronized void addPageView(PageView pageView){
374                if (!sampledIn) {
375                        return;
376                }
377
378                if (privacySettings.getSubmitMode() == SubmitMode.DO_NOT_TRACK){
379                        return;
380                }
381
382                requestIdPageLoad = RandomIdGenerator.generateID();
383                pageView.addProperty(EMRID, requestIdPageLoad);
384                pageView.addProperty(EMSID, sessionId);
385
386                if (!pageView.getProperties().has("siteid")){
387                        pageView.addSiteId(hostName);
388                }
389                if (!pageView.getProperties().has("content")){
390                        pageView.addContent(hostName);
391                }
392                if (!pageView.getProperties().has("source")){
393                        pageView.addMarketingChannel(Constants.DEFAULT_MARKETINGCHANNEL_MOBILE);
394                }
395
396        submitItem(pageView);
397        }
398
399    private void submitItem(PageView pageView) {
400        SubmitItem submitItem = new SubmitItem(pageView.getProperties(), System.currentTimeMillis());
401
402        if (privacySettings.getSubmitMode() == SubmitMode.TRACK ||
403                                (privacySettings.getSubmitMode() == SubmitMode.CACHE && items.size() < Constants.PRIVACY_MODE_CACHE_CACHE_SIZE)) {
404                        items.add(submitItem);
405                }
406
407                sendItemsPerhapsDelayed();
408        }
409
410        private void sendItemsPerhapsDelayed() {
411                if (privacySettings.getSubmitMode() == SubmitMode.TRACK) {
412                        if (batchAutoTransmitTimeout > 0) {
413                                sendDelayed();
414                        } else {
415                                submitBatch(false);
416                        }
417                }
418        }
419
420        private void sendDelayed() {
421                if (this.delayedExecutedSubmitTask == null) { // Create only Thread if not already one exists
422                        this.delayedExecutedSubmitTask = new Thread(new DelayedExecutedSubmitTask());
423                        this.delayedExecutedSubmitTask.setDaemon(true);
424                        this.delayedExecutedSubmitTask.start();
425                }
426        }
427
428        /**
429         * <p>Transmit all collected data.</p>
430         * 
431         * Data is always transmitted asynchron.
432         */
433        public void submitBatch() {
434                submitBatch(false);
435        }
436        
437        private void submitBatch(boolean calledFromTimerThread) {
438                // Den RequestExecutor zwar noch synchronized erzeugen, jedoch nicht im synchronized ausführen
439                RequestExecutor executor = createRequestExecutor(calledFromTimerThread);
440                if (executor == null)
441                        return;
442
443
444                if (calledFromTimerThread){
445                        executor.run();
446                } else {
447                        new Thread(executor).start();
448                }
449
450        }
451
452        private synchronized RequestExecutor createRequestExecutor(boolean calledFromTimerThread) {
453                if (!sampledIn || privacySettings.getSubmitMode() == SubmitMode.DO_NOT_TRACK) {
454                        items.clear();
455                        return null;
456                } else if (privacySettings.getSubmitMode() == SubmitMode.CACHE) {
457                        return null;
458                }
459
460                if (!calledFromTimerThread && delayedExecutedSubmitTask != null) {
461                        delayedExecutedSubmitTask.interrupt();
462                }
463
464                this.delayedExecutedSubmitTask = null;
465
466                if (items.isEmpty()) {
467                        return null;
468                }
469                List<SubmitItem> submitList = items;
470                items = new ArrayList<>();
471                return new RequestExecutor(submitList, visitorId, privacySettings, hostName, clientKey, logUrl, applicationContext, sessionId);
472        }
473
474
475        private class DelayedExecutedSubmitTask implements Runnable {
476
477                private DelayedExecutedSubmitTask() {
478                }
479
480                @Override
481                public void run() {
482                        Log.d(Constants.LOG_TAG, "TimerTask startet");
483            Process.setThreadPriority(Process.THREAD_PRIORITY_BACKGROUND);
484            try {
485                                Thread.sleep(Session.this.batchAutoTransmitTimeout * 1000);
486                        } catch (InterruptedException e) {
487                                Log.d(Constants.LOG_TAG, "TimerTask interrupted");
488                                return;
489                        }
490                        Log.d(Constants.LOG_TAG, "TimerTask sleep has finished. Execute submit now.");
491                        Session.this.submitBatch(true);
492                        
493                }
494                
495        }
496
497}