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}