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(); + } + } +}