diff --git a/.travis.yml b/.travis.yml
index 9e290c0..cbe2286 100644
--- a/.travis.yml
+++ b/.travis.yml
@@ -14,5 +14,5 @@ android:
- android-23
- extra-android-m2repository
-#notifications:
-# slack: literacyapp:HLjtHJdZ7DYJV2DlnDLrG6Gl
+notifications:
+ email: false
diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml
index 56ab5c4..fdad011 100644
--- a/app/src/main/AndroidManifest.xml
+++ b/app/src/main/AndroidManifest.xml
@@ -3,6 +3,7 @@
package="org.literacyapp.analytics">
+
@@ -10,6 +11,7 @@
android:name=".AnalyticsApplication"
android:icon="@mipmap/ic_launcher"
android:label="@string/app_name">
+
@@ -29,72 +31,64 @@
android:enabled="true"
android:exported="true"
android:permission="android.permission.BIND_JOB_SERVICE" />
+
-
-
-
-
-
-
-
-
-
-
-
-
-
diff --git a/app/src/main/java/org/literacyapp/analytics/MainActivity.java b/app/src/main/java/org/literacyapp/analytics/MainActivity.java
index 0b15726..d7e9b84 100644
--- a/app/src/main/java/org/literacyapp/analytics/MainActivity.java
+++ b/app/src/main/java/org/literacyapp/analytics/MainActivity.java
@@ -17,6 +17,7 @@
import android.widget.Toast;
import org.literacyapp.analytics.service.ScreenshotJobService;
+import org.literacyapp.analytics.service.ServerSynchronizationJobService;
import org.literacyapp.analytics.util.RootHelper;
public class MainActivity extends Activity {
@@ -61,20 +62,34 @@ public void onClick(View view) {
if (!isRootPermissionGranted) {
Toast.makeText(getApplicationContext(), "Root permission was not granted. Please see log for details.", Toast.LENGTH_LONG).show();
} else {
- Toast.makeText(getApplicationContext(), "Root permission was granted. Starting background job...", Toast.LENGTH_LONG).show();
+ Toast.makeText(getApplicationContext(), "Root permission was granted. Starting background jobs...", Toast.LENGTH_LONG).show();
- // Initiate background job for recording screenshots
- ComponentName componentName = new ComponentName(getApplicationContext(), ScreenshotJobService.class);
+ // Initiate background job for synchronizing events with server
+ // Note: This code block also exists in the BootReceiver
+ ComponentName componentName = new ComponentName(getApplicationContext(), ServerSynchronizationJobService.class);
JobInfo.Builder builder = new JobInfo.Builder(1, componentName);
+ builder.setPeriodic(60 * 60 * 1000); // Every 60 minutes
+ JobInfo serverSynchronizationJobInfo = builder.build();
+ JobScheduler jobScheduler = (JobScheduler) getSystemService(Context.JOB_SCHEDULER_SERVICE);
+ int resultId = jobScheduler.schedule(serverSynchronizationJobInfo);
+ if (resultId == JobScheduler.RESULT_SUCCESS) {
+ Log.i(getClass().getName(), "Server synchronization Job scheduled with id: " + serverSynchronizationJobInfo.getId());
+ } else {
+ Log.w(getClass().getName(), "Server synchronization Job scheduling failed. JobInfo id: " + serverSynchronizationJobInfo.getId());
+ }
+
+
+ // Initiate background job for recording screenshots
+ // Note: This code block also exists in the BootReceiver
+ componentName = new ComponentName(getApplicationContext(), ScreenshotJobService.class);
+ builder = new JobInfo.Builder(2, componentName);
builder.setPeriodic(5 * 60 * 1000); // Every 5 minutes
JobInfo screenshotJobInfo = builder.build();
-
- JobScheduler jobScheduler = (JobScheduler) getSystemService(Context.JOB_SCHEDULER_SERVICE);
- int resultId = jobScheduler.schedule(screenshotJobInfo);
- if (resultId > 0) {
- Log.i(getClass().getName(), "Job scheduled with id: " + resultId);
+ resultId = jobScheduler.schedule(screenshotJobInfo);
+ if (resultId == JobScheduler.RESULT_SUCCESS) {
+ Log.i(getClass().getName(), "Screenshot Job scheduled with id: " + screenshotJobInfo.getId());
} else {
- Log.w(getClass().getName(), "Job scheduling failed. Error id: " + resultId);
+ Log.w(getClass().getName(), "Screenshot Job scheduling failed. JobInfo id: " + serverSynchronizationJobInfo.getId());
}
}
diff --git a/app/src/main/java/org/literacyapp/analytics/receiver/BootReceiver.java b/app/src/main/java/org/literacyapp/analytics/receiver/BootReceiver.java
index dd3f4b0..b93fc47 100644
--- a/app/src/main/java/org/literacyapp/analytics/receiver/BootReceiver.java
+++ b/app/src/main/java/org/literacyapp/analytics/receiver/BootReceiver.java
@@ -16,6 +16,7 @@
import org.literacyapp.analytics.dao.BootCompletedEventDao;
import org.literacyapp.analytics.model.BootCompletedEvent;
import org.literacyapp.analytics.service.ScreenshotJobService;
+import org.literacyapp.analytics.service.ServerSynchronizationJobService;
import org.literacyapp.analytics.util.DeviceInfoHelper;
import java.io.File;
@@ -28,19 +29,6 @@ public class BootReceiver extends BroadcastReceiver {
public void onReceive(Context context, Intent intent) {
Log.i(getClass().getName(), "onReceive");
- // Initiate background job for recording screenshots
- ComponentName componentName = new ComponentName(context, ScreenshotJobService.class);
- JobInfo.Builder builder = new JobInfo.Builder(1, componentName);
- builder.setPeriodic(5 * 60 * 1000); // Every 5 minutes
- JobInfo screenshotJobInfo = builder.build();
- JobScheduler jobScheduler = (JobScheduler) context.getSystemService(Context.JOB_SCHEDULER_SERVICE);
- int resultId = jobScheduler.schedule(screenshotJobInfo);
- if (resultId > 0) {
- Log.i(getClass().getName(), "Job scheduled with id: " + resultId);
- } else {
- Log.w(getClass().getName(), "Job scheduling failed. Error id: " + resultId);
- }
-
// Store event in database
BootCompletedEvent bootCompletedEvent = new BootCompletedEvent();
@@ -73,5 +61,34 @@ public void onReceive(Context context, Intent intent) {
} catch (IOException e) {
Log.e(getClass().getName(), null, e);
}
+
+
+ // Initiate background job for synchronizing events with server
+ // Note: This code block also exists in the MainActivity
+ ComponentName componentName = new ComponentName(context, ServerSynchronizationJobService.class);
+ JobInfo.Builder builder = new JobInfo.Builder(1, componentName);
+ builder.setPeriodic(60 * 60 * 1000); // Every 60 minutes
+ JobInfo serverSynchronizationJobInfo = builder.build();
+ JobScheduler jobScheduler = (JobScheduler) context.getSystemService(Context.JOB_SCHEDULER_SERVICE);
+ int resultId = jobScheduler.schedule(serverSynchronizationJobInfo);
+ if (resultId == JobScheduler.RESULT_SUCCESS) {
+ Log.i(getClass().getName(), "Server synchronization Job scheduled with id: " + serverSynchronizationJobInfo.getId());
+ } else {
+ Log.w(getClass().getName(), "Server synchronization Job scheduling failed. JobInfo id: " + serverSynchronizationJobInfo.getId());
+ }
+
+
+ // Initiate background job for recording screenshots
+ // Note: This code block also exists in the MainActivity
+ componentName = new ComponentName(context, ScreenshotJobService.class);
+ builder = new JobInfo.Builder(2, componentName);
+ builder.setPeriodic(5 * 60 * 1000); // Every 5 minutes
+ JobInfo screenshotJobInfo = builder.build();
+ resultId = jobScheduler.schedule(screenshotJobInfo);
+ if (resultId == JobScheduler.RESULT_SUCCESS) {
+ Log.i(getClass().getName(), "Screenshot Job scheduled with id: " + screenshotJobInfo.getId());
+ } else {
+ Log.w(getClass().getName(), "Screenshot Job scheduling failed. JobInfo id: " + serverSynchronizationJobInfo.getId());
+ }
}
}
diff --git a/app/src/main/java/org/literacyapp/analytics/service/ScreenshotJobService.java b/app/src/main/java/org/literacyapp/analytics/service/ScreenshotJobService.java
index 386663d..2c3134f 100644
--- a/app/src/main/java/org/literacyapp/analytics/service/ScreenshotJobService.java
+++ b/app/src/main/java/org/literacyapp/analytics/service/ScreenshotJobService.java
@@ -15,6 +15,8 @@
/**
* Service responsible for recording screenshots when the screen in switched on.
+ *
+ * This service is triggered in the @{link {@link org.literacyapp.analytics.receiver.BootReceiver}}
*/
public class ScreenshotJobService extends JobService {
diff --git a/app/src/main/java/org/literacyapp/analytics/service/ServerSynchronizationJobService.java b/app/src/main/java/org/literacyapp/analytics/service/ServerSynchronizationJobService.java
index be1c62c..973a8cf 100644
--- a/app/src/main/java/org/literacyapp/analytics/service/ServerSynchronizationJobService.java
+++ b/app/src/main/java/org/literacyapp/analytics/service/ServerSynchronizationJobService.java
@@ -1,16 +1,90 @@
package org.literacyapp.analytics.service;
-import android.app.Service;
-import android.content.Intent;
-import android.os.IBinder;
+import android.app.job.JobParameters;
+import android.app.job.JobService;
+import android.os.Environment;
+import android.util.Log;
-public class ServerSynchronizationJobService extends Service {
- public ServerSynchronizationJobService() {
+import org.literacyapp.analytics.util.EnvironmentSettings;
+
+import java.io.BufferedReader;
+import java.io.File;
+import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.net.HttpURLConnection;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.text.ParseException;
+import java.text.SimpleDateFormat;
+import java.util.Calendar;
+import java.util.Date;
+
+/**
+ * Service responsible for uploading event files to server.
+ *
+ * This service is triggered in the @{link {@link org.literacyapp.analytics.receiver.BootReceiver}}
+ */
+public class ServerSynchronizationJobService extends JobService {
+
+ @Override
+ public boolean onStartJob(JobParameters jobParameters) {
+ Log.i(getClass().getName(), "onStartJob");
+
+ String logsPath = Environment.getExternalStorageDirectory() + "/.literacyapp-analytics/events";
+ File eventsDir = new File(logsPath);
+ Log.i(getClass().getName(), "eventsDir: " + eventsDir);
+ Log.i(getClass().getName(), "eventsDir.exists(): " + eventsDir.exists());
+ if (eventsDir.exists()) {
+ File[] deviceDirs = eventsDir.listFiles();
+ for (int i = 0; i < deviceDirs.length; i++) {
+ File deviceDir = deviceDirs[i];
+ Log.i(getClass().getName(), "deviceDir: " + deviceDir);
+
+ File[] eventFiles = deviceDir.listFiles();
+ for (int j = 0; j < eventFiles.length; j++) {
+ File eventFile = eventFiles[j];
+ Log.i(getClass().getName(), "eventFile: " + eventFile);
+ // Expected filename: "application_opened_events_yyyy-MM-dd.log"
+
+ if (eventFile.getName().startsWith("application_opened_events_")) {
+ // Skip files generated more than 7 days ago
+ String dateAsString = eventFile.getName().replace("application_opened_events_", "").replace(".log", "");
+ Log.i(getClass().getName(), "dateAsString: " + dateAsString);
+ SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyy-MM-dd");
+ try {
+ Date date = simpleDateFormat.parse(dateAsString);
+ Calendar calendar = Calendar.getInstance();
+ calendar.setTime(date);
+ Log.i(getClass().getName(), "calendar.getTime(): " + calendar.getTime());
+
+ Calendar calendar7DaysAgo = Calendar.getInstance();
+ calendar7DaysAgo.setTime(calendar.getTime());
+ calendar7DaysAgo.add(Calendar.DAY_OF_MONTH, -7);
+ Log.i(getClass().getName(), "calendar7DaysAgo.getTime(): " + calendar7DaysAgo.getTime());
+
+ if (!calendar.before(calendar7DaysAgo)) {
+ Log.i(getClass().getName(), "Uploading to web server: " + eventFile);
+
+ new UploadApplicationOpenedEventsAsyncTask().execute(eventFile);
+ }
+ } catch (ParseException e) {
+ Log.e(getClass().getName(), null, e);
+ }
+ }
+ }
+ }
+ }
+
+ boolean asynchronousProcessing = false;
+ return asynchronousProcessing;
}
@Override
- public IBinder onBind(Intent intent) {
- // TODO: Return the communication channel to the service.
- throw new UnsupportedOperationException("Not yet implemented");
+ public boolean onStopJob(JobParameters jobParameters) {
+ Log.i(getClass().getName(), "onStopJob");
+
+ boolean restartJob = false;
+ return restartJob;
}
}
diff --git a/app/src/main/java/org/literacyapp/analytics/service/UploadApplicationOpenedEventsAsyncTask.java b/app/src/main/java/org/literacyapp/analytics/service/UploadApplicationOpenedEventsAsyncTask.java
index 4e3e302..d9f8a27 100644
--- a/app/src/main/java/org/literacyapp/analytics/service/UploadApplicationOpenedEventsAsyncTask.java
+++ b/app/src/main/java/org/literacyapp/analytics/service/UploadApplicationOpenedEventsAsyncTask.java
@@ -1,331 +1,78 @@
-package org.literacyapp.appstore.task;
+package org.literacyapp.analytics.service;
-import android.content.Context;
-import android.content.SharedPreferences;
-import android.content.pm.PackageInfo;
-import android.content.pm.PackageManager;
import android.os.AsyncTask;
-import android.os.Build;
-import android.os.Environment;
-import android.preference.PreferenceManager;
-import android.text.TextUtils;
import android.util.Log;
-import com.google.gson.Gson;
-import com.google.gson.reflect.TypeToken;
-
-import org.json.JSONArray;
-import org.json.JSONException;
-import org.json.JSONObject;
-import org.literacyapp.appstore.AppstoreApplication;
-import org.literacyapp.appstore.MainActivity;
-import org.literacyapp.appstore.R;
-import org.literacyapp.appstore.dao.ApplicationDao;
-import org.literacyapp.appstore.model.Application;
-import org.literacyapp.appstore.util.ApkLoader;
-import org.literacyapp.appstore.util.ChecksumHelper;
-import org.literacyapp.appstore.util.ConnectivityHelper;
-import org.literacyapp.appstore.util.DeviceInfoHelper;
-import org.literacyapp.appstore.util.EnvironmentSettings;
-import org.literacyapp.appstore.util.JsonLoader;
-import org.literacyapp.appstore.util.UserPrefsHelper;
-import org.literacyapp.model.enums.admin.ApplicationStatus;
-import org.literacyapp.model.gson.admin.ApplicationGson;
-import org.literacyapp.model.gson.admin.ApplicationVersionGson;
+import org.apache.commons.io.FileUtils;
+import org.apache.commons.io.IOUtils;
+import org.literacyapp.analytics.util.EnvironmentSettings;
import java.io.BufferedReader;
+import java.io.DataOutputStream;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
-import java.lang.reflect.Type;
-import java.util.Calendar;
-import java.util.Locale;
-
-public class DownloadApplicationsAsyncTask extends AsyncTask