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/build.gradle b/app/build.gradle
index 2ca25ae..9ba414e 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -15,8 +15,8 @@ android {
applicationId "org.literacyapp.analytics"
minSdkVersion 21
targetSdkVersion 23
- versionCode 1001000
- versionName "1.1.0"
+ versionCode 1002000
+ versionName "1.2.0"
}
buildTypes {
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/dao/DaoMaster.java b/app/src/main/java/org/literacyapp/analytics/dao/DaoMaster.java
index cb4902d..8af0244 100644
--- a/app/src/main/java/org/literacyapp/analytics/dao/DaoMaster.java
+++ b/app/src/main/java/org/literacyapp/analytics/dao/DaoMaster.java
@@ -14,10 +14,10 @@
// THIS CODE IS GENERATED BY greenDAO, DO NOT EDIT.
/**
- * Master of DAO (schema version 1001000): knows all DAOs.
+ * Master of DAO (schema version 1002000): knows all DAOs.
*/
public class DaoMaster extends AbstractDaoMaster {
- public static final int SCHEMA_VERSION = 1001000;
+ public static final int SCHEMA_VERSION = 1002000;
/** Creates underlying database table using DAOs. */
public static void createAllTables(Database db, boolean ifNotExists) {
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
new file mode 100644
index 0000000..973a8cf
--- /dev/null
+++ b/app/src/main/java/org/literacyapp/analytics/service/ServerSynchronizationJobService.java
@@ -0,0 +1,90 @@
+package org.literacyapp.analytics.service;
+
+import android.app.job.JobParameters;
+import android.app.job.JobService;
+import android.os.Environment;
+import android.util.Log;
+
+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 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
new file mode 100644
index 0000000..1c57b66
--- /dev/null
+++ b/app/src/main/java/org/literacyapp/analytics/service/UploadApplicationOpenedEventsAsyncTask.java
@@ -0,0 +1,84 @@
+package org.literacyapp.analytics.service;
+
+import android.os.AsyncTask;
+import android.util.Log;
+
+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.net.HttpURLConnection;
+import java.net.MalformedURLException;
+import java.net.URL;
+
+public class UploadApplicationOpenedEventsAsyncTask extends AsyncTask {
+
+ @Override
+ protected Void doInBackground(File... files) {
+ Log.i(getClass().getName(), "doInBackground");
+
+ File file = files[0];
+ Log.i(getClass().getName(), "file: " + file);
+ Log.i(getClass().getName(), "file.getName(): " + file.getName());
+
+ try {
+ // See https://stackoverflow.com/a/11826317
+
+ URL url = new URL(EnvironmentSettings.getBaseRestUrl() + "/analytics/application-opened-event/create");
+ Log.i(getClass().getName(), "url: " + url);
+
+ HttpURLConnection connection = (HttpURLConnection) url.openConnection();
+ connection.setRequestMethod("POST");
+ connection.setUseCaches(false);
+ connection.setDoOutput(true);
+
+ connection.setRequestProperty("Connection", "Keep-Alive");
+ connection.setRequestProperty("Cache-Control", "no-cache");
+ connection.setRequestProperty("Content-Type", "multipart/form-data;boundary=*****");
+
+ DataOutputStream request = new DataOutputStream(connection.getOutputStream());
+
+ request.writeBytes("--*****\r\n");
+ request.writeBytes("Content-Disposition: form-data; name=\"multipartFile\";filename=\"" + file.getName() + "\"\r\n");
+ request.writeBytes("\r\n");
+
+ byte[] fileBytes = FileUtils.readFileToByteArray(file);
+ Log.i(getClass().getName(), "fileBytes.length: " + fileBytes.length);
+ request.write(fileBytes);
+
+ request.writeBytes("\r\n");
+ request.writeBytes("--*****--\r\n");
+
+ int responseCode = connection.getResponseCode();
+ Log.i(getClass().getName(), "responseCode: " + responseCode);
+ InputStream inputStream = null;
+ if (responseCode == 200) {
+ inputStream = connection.getInputStream();
+ } else {
+ inputStream = connection.getErrorStream();
+ }
+
+ BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
+ String response = bufferedReader.readLine();
+ Log.i(getClass().getName(), "response: " + response);
+ } catch (MalformedURLException e) {
+ Log.e(getClass().getName(), null, e);
+ } catch (IOException e) {
+ Log.e(getClass().getName(), null, e);
+ }
+
+ return null;
+ }
+
+ @Override
+ protected void onPostExecute(Void v) {
+ Log.i(getClass().getName(), "onPostExecute");
+ super.onPostExecute(v);
+ }
+}
diff --git a/app/src/main/java/org/literacyapp/analytics/util/EnvironmentSettings.java b/app/src/main/java/org/literacyapp/analytics/util/EnvironmentSettings.java
new file mode 100644
index 0000000..6f90106
--- /dev/null
+++ b/app/src/main/java/org/literacyapp/analytics/util/EnvironmentSettings.java
@@ -0,0 +1,34 @@
+package org.literacyapp.analytics.util;
+
+import org.literacyapp.model.enums.Environment;
+
+public class EnvironmentSettings {
+
+// private static final Environment ENVIRONMENT = Environment.DEV;
+// public static final Environment ENVIRONMENT = Environment.TEST;
+ public static final Environment ENVIRONMENT = Environment.PROD;
+
+ public static final String PROD_DOMAIN = "literacyapp.org";
+
+ public static String getDomain() {
+ if (ENVIRONMENT == Environment.DEV) {
+ return "192.168.0.103"; // Replace with the IP address of your WIFI router
+ } else if (ENVIRONMENT == Environment.TEST) {
+ return "test." + PROD_DOMAIN;
+ } else {
+ return PROD_DOMAIN;
+ }
+ }
+
+ public static String getBaseRestUrl() {
+ return getBaseUrl() + "/rest/v1";
+ }
+
+ public static String getBaseUrl() {
+ if (ENVIRONMENT == Environment.DEV) {
+ return "http://" + getDomain() + ":8080/literacyapp-webapp";
+ } else {
+ return "http://" + getDomain();
+ }
+ }
+}