diff --git a/.gitignore b/.gitignore
index 9419f7d..0fc6896 100644
--- a/.gitignore
+++ b/.gitignore
@@ -17,4 +17,4 @@ pacemaker/pacemaker.iml
gradlew
gradlew.bat
local.properties
-Pacemaker.iml
\ No newline at end of file
+*.iml
diff --git a/demo/build.gradle b/demo/build.gradle
index 30097ae..bbcddc0 100644
--- a/demo/build.gradle
+++ b/demo/build.gradle
@@ -11,6 +11,21 @@ android {
versionCode 1
versionName "1.0"
}
+
+ productFlavors {
+ demo1 {
+ applicationId "in.raveesh.pacemaker1"
+ }
+
+ demo2 {
+ applicationId "in.raveesh.pacemaker2"
+ }
+
+ demo3 {
+ applicationId "in.raveesh.pacemaker3"
+ }
+ }
+
buildTypes {
release {
minifyEnabled false
@@ -24,5 +39,6 @@ repositories {
}
dependencies {
- compile 'in.raveesh:pacemaker:0.1.0-SNAPSHOT'
+// compile 'in.raveesh:pacemaker:0.2.0-SNAPSHOT'
+ compile project(':pacemaker')
}
diff --git a/demo/src/demo1/res/values/strings.xml b/demo/src/demo1/res/values/strings.xml
new file mode 100644
index 0000000..c5d9119
--- /dev/null
+++ b/demo/src/demo1/res/values/strings.xml
@@ -0,0 +1,3 @@
+
+ Pacemaker1
+
diff --git a/demo/src/demo2/res/values/strings.xml b/demo/src/demo2/res/values/strings.xml
new file mode 100644
index 0000000..7d2857f
--- /dev/null
+++ b/demo/src/demo2/res/values/strings.xml
@@ -0,0 +1,3 @@
+
+ Pacemaker2
+
diff --git a/demo/src/demo3/res/values/strings.xml b/demo/src/demo3/res/values/strings.xml
new file mode 100644
index 0000000..309628c
--- /dev/null
+++ b/demo/src/demo3/res/values/strings.xml
@@ -0,0 +1,3 @@
+
+ Pacemaker3
+
diff --git a/demo/src/main/java/in/raveesh/pacemaker/demo/MainActivity.java b/demo/src/main/java/in/raveesh/pacemaker/demo/MainActivity.java
index 7e981e4..cc0f2ed 100644
--- a/demo/src/main/java/in/raveesh/pacemaker/demo/MainActivity.java
+++ b/demo/src/main/java/in/raveesh/pacemaker/demo/MainActivity.java
@@ -2,34 +2,31 @@
import android.app.Activity;
import android.os.Bundle;
+import android.support.annotation.NonNull;
import android.view.View;
-import android.widget.Button;
import in.raveesh.pacemaker.Pacemaker;
import in.raveesh.pacemaker.R;
public class MainActivity extends Activity {
- boolean beginOrStop = false;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
- final Button set = (Button)findViewById(R.id.begin);
- set.setOnClickListener(new View.OnClickListener() {
+ findViewById(R.id.begin).setOnClickListener(new View.OnClickListener() {
@Override
- public void onClick(View v) {
- if (!beginOrStop) {
- Pacemaker.scheduleLinear(MainActivity.this, 5);
- set.setText(R.string.stop);
- }
- else{
- Pacemaker.cancelLinear(MainActivity.this, 5);
- set.setText(R.string.begin);
- }
- beginOrStop = !beginOrStop;
+ public void onClick(@NonNull View v) {
+ Pacemaker.scheduleLinear(MainActivity.this, 5);
}
});
+ findViewById(R.id.stop).setOnClickListener(new View.OnClickListener() {
+ @Override
+ public void onClick(@NonNull View v) {
+ Pacemaker.cancelLinear(MainActivity.this);
+ }
+ });
+
}
}
diff --git a/demo/src/main/res/layout/activity_main.xml b/demo/src/main/res/layout/activity_main.xml
index ce42566..e173ade 100644
--- a/demo/src/main/res/layout/activity_main.xml
+++ b/demo/src/main/res/layout/activity_main.xml
@@ -1,14 +1,20 @@
-
+ tools:context="in.raveesh.in.raveesh.pacemaker.demo.MainActivity">
+
-
+
diff --git a/pacemaker/src/androidTest/java/in/raveesh/pacemaker/SchedulerTest.java b/pacemaker/src/androidTest/java/in/raveesh/pacemaker/SchedulerTest.java
new file mode 100644
index 0000000..55e3867
--- /dev/null
+++ b/pacemaker/src/androidTest/java/in/raveesh/pacemaker/SchedulerTest.java
@@ -0,0 +1,69 @@
+package in.raveesh.pacemaker;
+
+import junit.framework.TestCase;
+
+/**
+ * @author Badoo
+ */
+public class SchedulerTest extends TestCase {
+ private MyTimeProvider mTime;
+ private Scheduler mScheduler;
+
+ @Override
+ protected void setUp() throws Exception {
+ super.setUp();
+ mTime = new MyTimeProvider();
+ mScheduler = new Scheduler(mTime);
+ }
+
+ public void testEmpty() {
+ assertEquals(-1, mScheduler.getNextTriggerTime());
+ }
+
+ public void testOneRuleWithZeroTimeStart() {
+ mTime.time = 0;
+ mScheduler.addLinearPace("id", 100);
+ assertEquals(100, mScheduler.getNextTriggerTime());
+ mTime.time = 100;
+ assertEquals(200, mScheduler.getNextTriggerTime());
+ mTime.time = 150;
+ assertEquals(200, mScheduler.getNextTriggerTime());
+ }
+
+ public void testOneRuleWithNonZeroTimeStart() {
+ mTime.time = 100;
+ mScheduler.addLinearPace("id", 100);
+ assertEquals(200, mScheduler.getNextTriggerTime());
+ mTime.time = 200;
+ assertEquals(300, mScheduler.getNextTriggerTime());
+ mTime.time = 250;
+ assertEquals(300, mScheduler.getNextTriggerTime());
+ }
+
+ public void testOneRuleWithTimeCloseToEvent() {
+ mTime.time = 100;
+ mScheduler.addLinearPace("id", 100);
+ mTime.time = 199;
+ assertEquals(200, mScheduler.getNextTriggerTime());
+ mTime.time = 201;
+ assertEquals(300, mScheduler.getNextTriggerTime());
+ }
+
+ public void testInTwoRulesSmallerIsChoosen() {
+ mTime.time = 0;
+ mScheduler.addLinearPace("id1", 100);
+ mTime.time = 50;
+ mScheduler.addLinearPace("id2", 10);
+ assertEquals(60, mScheduler.getNextTriggerTime());
+ }
+
+
+ private class MyTimeProvider implements TimeProvider {
+ public long time;
+
+ @Override
+ public long getTime() {
+ return time;
+ }
+ }
+}
\ No newline at end of file
diff --git a/pacemaker/src/main/AndroidManifest.xml b/pacemaker/src/main/AndroidManifest.xml
index bf90773..3a7a762 100644
--- a/pacemaker/src/main/AndroidManifest.xml
+++ b/pacemaker/src/main/AndroidManifest.xml
@@ -1,8 +1,31 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/pacemaker/src/main/java/in/raveesh/pacemaker/HeartbeatReceiver.java b/pacemaker/src/main/java/in/raveesh/pacemaker/HeartbeatReceiver.java
deleted file mode 100644
index 4ac23a2..0000000
--- a/pacemaker/src/main/java/in/raveesh/pacemaker/HeartbeatReceiver.java
+++ /dev/null
@@ -1,43 +0,0 @@
-package in.raveesh.pacemaker;
-
-import android.content.BroadcastReceiver;
-import android.content.Context;
-import android.content.Intent;
-import android.util.Log;
-
-/**
- * Created by Raveesh on 23/07/15.
- */
-public class HeartbeatReceiver extends BroadcastReceiver {
- private static final Intent GTALK_HEART_BEAT_INTENT = new Intent("com.google.android.intent.action.GTALK_HEARTBEAT");
- private static final Intent MCS_MCS_HEARTBEAT_INTENT = new Intent("com.google.android.intent.action.MCS_HEARTBEAT");
-
- @Override
- public void onReceive(Context context, Intent intent) {
- context.sendBroadcast(GTALK_HEART_BEAT_INTENT);
- context.sendBroadcast(MCS_MCS_HEARTBEAT_INTENT);
- Log.v("Heartbeater", "HeartbeatReceiver sent heartbeat request");
- scheduleNext(context, intent);
- }
-
- /**
- * Schedules the next heartbeat when required
- * @param context Context from the broadcast receiver onReceive
- * @param intent Intent from the broadcast receiver onReceive
- */
- private void scheduleNext(Context context, Intent intent) {
- int type = intent.getIntExtra(Pacemaker.KEY_TYPE, Pacemaker.TYPE_LINEAR);
- if (type == Pacemaker.TYPE_EXPONENTIAL) {
- int delay = intent.getIntExtra(Pacemaker.KEY_DELAY, 5);
- delay = delay*2;
- int max = intent.getIntExtra(Pacemaker.KEY_MAX, 60);
- if (delay > max){
- Log.d("Heartbeater", "Killing Heartbeater as delay now exceeds max");
- return;
- }
- Pacemaker.scheduleExponential(context, delay, max);
- } else {
- Log.d("Heartbeater", "Ignored linear schedule request since it should already be there");
- }
- }
-}
diff --git a/pacemaker/src/main/java/in/raveesh/pacemaker/Pacemaker.java b/pacemaker/src/main/java/in/raveesh/pacemaker/Pacemaker.java
index fa68f9f..c51a3ab 100644
--- a/pacemaker/src/main/java/in/raveesh/pacemaker/Pacemaker.java
+++ b/pacemaker/src/main/java/in/raveesh/pacemaker/Pacemaker.java
@@ -1,76 +1,46 @@
package in.raveesh.pacemaker;
-import android.app.AlarmManager;
-import android.app.PendingIntent;
import android.content.Context;
-import android.content.Intent;
import android.util.Log;
/**
+ * Utility to add/remove paces which suppose to wake up GCM
+ *
+ * Several apps with this library will share responsibility to generate paces in battery efficient way:
+ * - Only one app will generate paces
+ * - When it is uninstalled, another will take responsibility
+ * - When device is restarted, paces will be recovered
+ *
+ * Only one pace delay is accepted from each app. If several apps requires different pace delays then shorter delay will take place
+ *
* Created by Raveesh on 23/07/15.
+ *
+ * Modifications made by Badoo 21/09/15
*/
public class Pacemaker {
-
- public static final int TYPE_LINEAR = 1;
- public static final int TYPE_EXPONENTIAL = 2;
-
- public static final String KEY_DELAY = "KEY_DELAY";
- public static final String KEY_TYPE = "KEY_TYPE";
- public static final String KEY_MAX = "KEY_MAX";
+ static final String TAG = "Pacemaker";
+ static final boolean DEBUG = false;
/**
* Starts a linear repeated alarm that sends a broadcast to Play Services, which in turn sends a heartbeat
+ *
+ * Only one delay can be registered per one app. Each time you call to scheduleLinear, it will overload previous settings
+ *
* @param context Context from your application
* @param delay Gap between heartbeats in minutes
*/
public static void scheduleLinear(Context context, int delay) {
- Intent intent = new Intent(context, HeartbeatReceiver.class);
- intent.putExtra(KEY_DELAY, delay);
- intent.putExtra(KEY_TYPE, TYPE_LINEAR);
-
- long timeGap = delay * 60 * 1000;
-
- PendingIntent alarmIntent = PendingIntent.getBroadcast(context, delay, intent, PendingIntent.FLAG_CANCEL_CURRENT);
- AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
- alarmManager.setRepeating(AlarmManager.RTC_WAKEUP,
- System.currentTimeMillis() + timeGap, timeGap,
- alarmIntent);
+ long delayInSec = delay * 60 * 1000;
Log.d("Heartbeater", "Scheduled repeating");
+ PacemakerService.Launcher.scheduleLinear(context, delayInSec);
}
/**
* Function to cancel your linear alarms if required
* @param context Context from your application
- * @param delay Gap between heartbeats that you had set
*/
- public static void cancelLinear(Context context, int delay){
- Intent intent = new Intent(context, HeartbeatReceiver.class);
- intent.putExtra(KEY_DELAY, delay);
- intent.putExtra(KEY_TYPE, TYPE_LINEAR);
- PendingIntent alarmIntent = PendingIntent.getBroadcast(context, delay, intent, PendingIntent.FLAG_CANCEL_CURRENT);
- AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
- alarmManager.cancel(alarmIntent);
- }
-
- /**
- * Starts an exponential alarm that sends a broadcast to Play Services, which in turn sends a heartbeat
- * @param context Context from your application
- * @param delay Time in which to send first broadcast. Subsequent broadcasts would be at exponential intervals
- * @param max The max time till which the broadcasts should be sent. Once past this limit, no more heartbeats are sent
- */
- public static void scheduleExponential(Context context, int delay, int max) {
- Intent intent = new Intent(context, HeartbeatReceiver.class);
- intent.putExtra(KEY_DELAY, delay);
- intent.putExtra(KEY_TYPE, TYPE_EXPONENTIAL);
- intent.putExtra(KEY_MAX, max);
-
- long timeGap = delay * 60 * 1000;
- PendingIntent alarmIntent = PendingIntent.getBroadcast(context, delay, intent, PendingIntent.FLAG_CANCEL_CURRENT);
- AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE);
- alarmManager.set(AlarmManager.RTC_WAKEUP,
- System.currentTimeMillis() + timeGap,
- alarmIntent);
- Log.d("Heartbeater", "Scheduled exponential");
-
+ public static void cancelLinear(Context context) {
+ Log.d("Heartbeater", "Cancelling repeating");
+ PacemakerService.Launcher.cancelLinear(context);
}
}
diff --git a/pacemaker/src/main/java/in/raveesh/pacemaker/PacemakerReceiver.java b/pacemaker/src/main/java/in/raveesh/pacemaker/PacemakerReceiver.java
new file mode 100644
index 0000000..9747b64
--- /dev/null
+++ b/pacemaker/src/main/java/in/raveesh/pacemaker/PacemakerReceiver.java
@@ -0,0 +1,44 @@
+package in.raveesh.pacemaker;
+
+import android.content.BroadcastReceiver;
+import android.content.Context;
+import android.content.Intent;
+import android.support.annotation.NonNull;
+import android.util.Log;
+
+/**
+ * Delegates all work to {@link PacemakerService}.
+ * Listens to
+ * - ACTION_BOOT_COMPLETED to reschedule alarm after device rebooted
+ * - ACTION_PACKAGE_REMOVED to sync alarm settings, remove all alarms scheduled by removed package and to decide what app responsible for generating paces
+ * - ACTION_MANAGE_PACEMAKER to add/remove new setting. Listens to all processes and stores settings. If process is master it will take responsibility to generate paces
+ *
+ * Process will take responsibility when it is oldest among others. If it is oldest, this mean it knows more settings (it was received much more events with settings so it has much more accurate knowledge)
+ *
+ * @author Badoo
+ */
+public class PacemakerReceiver extends BroadcastReceiver {
+ private static final String TAG = Pacemaker.TAG;
+ private static final boolean DEBUG = Pacemaker.DEBUG;
+
+ public PacemakerReceiver() {
+ }
+
+ @Override
+ public void onReceive(@NonNull Context context, @NonNull Intent intent) {
+ if (DEBUG) Log.d(TAG, "received intent: " + intent);
+ switch (intent.getAction()) {
+ case Intent.ACTION_BOOT_COMPLETED:
+ PacemakerService.Launcher.startOnBootCompleted(context);
+ break;
+ case Intent.ACTION_PACKAGE_REMOVED:
+ PacemakerService.Launcher.startOnPackageRemoved(context);
+ break;
+ case PacemakerService.ACTION_MANAGE_PACEMAKER:
+ PacemakerService.Launcher.startOnManage(context, intent);
+ break;
+ default:
+ throw new IllegalStateException("Unknown action: " + intent.getAction());
+ }
+ }
+}
diff --git a/pacemaker/src/main/java/in/raveesh/pacemaker/PacemakerService.java b/pacemaker/src/main/java/in/raveesh/pacemaker/PacemakerService.java
new file mode 100644
index 0000000..e41a9a7
--- /dev/null
+++ b/pacemaker/src/main/java/in/raveesh/pacemaker/PacemakerService.java
@@ -0,0 +1,295 @@
+package in.raveesh.pacemaker;
+
+import android.app.AlarmManager;
+import android.app.IntentService;
+import android.app.PendingIntent;
+import android.content.Context;
+import android.content.Intent;
+import android.content.pm.PackageInfo;
+import android.content.pm.PackageManager;
+import android.content.pm.ResolveInfo;
+import android.os.Build;
+import android.os.IBinder;
+import android.os.SystemClock;
+import android.support.annotation.NonNull;
+import android.util.Log;
+
+import java.io.File;
+import java.util.List;
+
+/**
+ * Android Service which generates pace to GCM.
+ *
+ * Service will store all settings. Also if process is master, it will take responsibility to generate paces.
+ * Master process is the oldest process in system that can receive broadcast events with action "in.raveesh.pacemaker.ACTION_MANAGE_PACEMAKER".
+ * All other processes just stores settings. Once master process uninstalled from system, next oldest process takes responsibility to be master.
+ * Older process is - more settings it collected from others, that is why oldest one will be master.
+ *
+ * Service should be started
+ * - when some app requested to add new pace
+ * - when some app requested cancel of pace
+ * - when package uninstalled (called from PacemakerReceiver)
+ * - when device restarted (called from PacemakerReceiver)
+ * - when pace should be generated (called by AlarmManager)
+ *
+ * @author Badoo
+ */
+public class PacemakerService extends IntentService {
+ public static final String ACTION_MANAGE_PACEMAKER = "in.raveesh.pacemaker.ACTION_MANAGE_PACEMAKER";
+
+ private static final boolean DEBUG = Pacemaker.DEBUG;
+ private static final String TAG = Pacemaker.TAG;
+
+ private static final Intent GTALK_HEART_BEAT_INTENT = new Intent("com.google.android.intent.action.GTALK_HEARTBEAT");
+ private static final Intent MCS_MCS_HEARTBEAT_INTENT = new Intent("com.google.android.intent.action.MCS_HEARTBEAT");
+
+ private static final int TYPE_LINEAR = 1;
+
+ private static final int COMMAND_ADD = 1; // new pace settings
+ private static final int COMMAND_PACE = 2; // time to generate pace
+ private static final int COMMAND_SYNC = 3; // when any package been unistalled
+ private static final int COMMAND_BOOT = 4; // when device restarted
+ private static final int COMMAND_CANCEL = 5; // remove of one pace setting
+
+ private static final String EXTRA_COMMAND = "command";
+ private static final String EXTRA_TYPE = "type";
+ private static final String EXTRA_DELAY = "delay";
+ private static final String EXTRA_PACKAGE_NAME = "package";
+
+ private static final int UNKNOWN_VALUE = 0;
+ private Scheduler mScheduler;
+
+ public PacemakerService() {
+ super("PacemakerScheduler");
+ }
+
+ @Override
+ public IBinder onBind(@NonNull Intent intent) {
+ throw new UnsupportedOperationException("Not supported");
+ }
+
+ @Override
+ protected void onHandleIntent(Intent intent) {
+ if (intent == null) { // Surprise :)
+ return;
+ }
+ if (!ACTION_MANAGE_PACEMAKER.equals(intent.getAction())) {
+ return;
+ }
+ int command = intent.getIntExtra(EXTRA_COMMAND, UNKNOWN_VALUE);
+ int type = intent.getIntExtra(EXTRA_TYPE, UNKNOWN_VALUE);
+ switch (command) {
+ case COMMAND_ADD:
+ onAdd(type, intent);
+ break;
+ case COMMAND_PACE:
+ onPace();
+ break;
+ case COMMAND_SYNC:
+ onSync();
+ break;
+ case COMMAND_BOOT:
+ onBoot();
+ break;
+ case COMMAND_CANCEL:
+ onCancel(type, intent);
+ break;
+ }
+ }
+
+ private void onBoot() {
+ if (DEBUG) Log.d(TAG, "onBoot");
+ getScheduler().resetStartTime();
+ scheduleAlarmIfProcessIsMaster();
+ }
+
+ private void onSync() {
+ if (DEBUG) Log.d(TAG, "onSync");
+ boolean updated = getScheduler().syncKeys(new Scheduler.KeysValidator() {
+ @Override
+ public boolean isValid(String key) {
+ try {
+ getPackageManager().getPackageInfo(key, 0);
+ return true;
+ }
+ catch (PackageManager.NameNotFoundException e) {
+ return false;
+ }
+ }
+ });
+ if (updated) {
+ scheduleAlarmIfProcessIsMaster();
+ }
+ }
+
+ private void onAdd(int type, Intent intent) {
+ final String packageName = intent.getStringExtra(EXTRA_PACKAGE_NAME);
+ switch (type) {
+ case TYPE_LINEAR:
+ onAddLinear(packageName, intent.getLongExtra(EXTRA_DELAY, UNKNOWN_VALUE));
+ break;
+ }
+ }
+
+ private void onCancel(int type, Intent intent) {
+ final String packageName = intent.getStringExtra(EXTRA_PACKAGE_NAME);
+ switch (type) {
+ case TYPE_LINEAR:
+ onCancelLinear(packageName);
+ break;
+ }
+ }
+
+ private void onCancelLinear(String packageName) {
+ if (DEBUG) Log.d(TAG, "onCancelLinear from " + packageName);
+ if (getScheduler().removeLinearPace(packageName)) {
+ scheduleAlarmIfProcessIsMaster();
+ }
+ }
+
+ private void onAddLinear(String packageName, long delay) {
+ if (DEBUG) Log.d(TAG, "onAddLinear from " + packageName);
+ if (getScheduler().addLinearPace(packageName, delay)) {
+ scheduleAlarmIfProcessIsMaster();
+ }
+ }
+
+ private void onPace() {
+ try {
+ if (DEBUG) {
+ Log.d(TAG, "onPace");
+ return;
+ }
+
+ sendBroadcast(GTALK_HEART_BEAT_INTENT);
+ sendBroadcast(MCS_MCS_HEARTBEAT_INTENT);
+ }
+ finally {
+ scheduleAlarmIfProcessIsMaster();
+ }
+ }
+
+ private void scheduleAlarmIfProcessIsMaster() {
+ final Scheduler scheduler = getScheduler();
+ PendingIntent pendingIntent = PendingIntent.getService(this, 0, Launcher.createPaceIntent(this), PendingIntent.FLAG_CANCEL_CURRENT);
+ AlarmManager alarmManager = (AlarmManager) getSystemService(Context.ALARM_SERVICE);
+ long nextTriggerTime = scheduler.getNextTriggerTime();
+ if (isMaster() && nextTriggerTime > 0) {
+ if (DEBUG) {
+ Log.d(TAG, "registering next alarm at " + nextTriggerTime + " which will occur in " + ((nextTriggerTime - SystemClock.elapsedRealtime()) / 1000) + " seconds");
+ }
+ alarmManager.set(AlarmManager.ELAPSED_REALTIME_WAKEUP, nextTriggerTime, pendingIntent);
+ }
+ else {
+ if (DEBUG) Log.d(TAG, "cancelling alarms, if they exist");
+ alarmManager.cancel(pendingIntent); // We could be master in the past, but now we are not anymore
+ }
+ }
+
+ private boolean isMaster() {
+ Intent intent = new Intent(ACTION_MANAGE_PACEMAKER);
+ if (Build.VERSION.SDK_INT >= 12) {
+ intent.addFlags(Intent.FLAG_INCLUDE_STOPPED_PACKAGES);
+ }
+ List infos = getPackageManager().queryBroadcastReceivers(intent, 0);
+ if (infos == null) {
+ return false;
+ }
+ if (infos.isEmpty()) {
+ return false;
+ }
+ PackageInfo masterPackage = null;
+ for (ResolveInfo i : infos) {
+ try {
+ String packageName = i.activityInfo.packageName;
+ PackageInfo currPackage = getPackageManager().getPackageInfo(packageName, 0);
+ if (masterPackage == null) {
+ masterPackage = currPackage;
+ }
+ else if (currPackage.firstInstallTime < masterPackage.firstInstallTime){
+ masterPackage = currPackage;
+ } else if (currPackage.firstInstallTime == masterPackage.firstInstallTime && currPackage.packageName.compareTo(masterPackage.packageName) < 0) {
+ masterPackage = currPackage;
+ }
+
+ }
+ catch (Exception e) {
+ Log.w(TAG, "Failed to get package info", e);
+ }
+ }
+ return masterPackage != null && getPackageName().equals(masterPackage.packageName);
+ }
+
+ private Scheduler getScheduler() {
+ if (mScheduler == null) {
+ mScheduler = new Scheduler();
+ mScheduler.setData(new File(getFilesDir(), "pacemaker.data"));
+ }
+ return mScheduler;
+ }
+
+
+ /**
+ * Work with service using this utility methods
+ */
+ public static class Launcher {
+
+ static void startOnBootCompleted(Context ctx) {
+ Intent intent = new Intent(ctx, PacemakerService.class);
+ intent.setAction(ACTION_MANAGE_PACEMAKER);
+ intent.putExtra(EXTRA_COMMAND, COMMAND_BOOT);
+ ctx.startService(intent);
+ }
+
+ static void startOnPackageRemoved(Context ctx) {
+ Intent intent = new Intent(ctx, PacemakerService.class);
+ intent.setAction(ACTION_MANAGE_PACEMAKER);
+ intent.putExtra(EXTRA_COMMAND, COMMAND_BOOT);
+ ctx.startService(intent);
+ }
+
+ static void startOnManage(Context ctx, Intent caller) {
+ Intent intent = new Intent(ctx, PacemakerService.class);
+ intent.setAction(ACTION_MANAGE_PACEMAKER);
+ copyExtras(caller, intent);
+ ctx.startService(intent);
+ }
+
+ static void scheduleLinear(Context ctx, long delay) {
+ Intent intent = new Intent(ACTION_MANAGE_PACEMAKER);
+ if (Build.VERSION.SDK_INT >= 12) {
+ intent.addFlags(Intent.FLAG_INCLUDE_STOPPED_PACKAGES);
+ }
+ intent.putExtra(EXTRA_COMMAND, COMMAND_ADD);
+ intent.putExtra(EXTRA_TYPE, TYPE_LINEAR);
+ intent.putExtra(EXTRA_DELAY, delay);
+ intent.putExtra(EXTRA_PACKAGE_NAME, ctx.getPackageName());
+ ctx.sendBroadcast(intent);
+ }
+
+ public static void cancelLinear(Context ctx) {
+ Intent intent = new Intent(ACTION_MANAGE_PACEMAKER);
+ if (Build.VERSION.SDK_INT >= 12) {
+ intent.addFlags(Intent.FLAG_INCLUDE_STOPPED_PACKAGES);
+ }
+ intent.putExtra(EXTRA_COMMAND, COMMAND_CANCEL);
+ intent.putExtra(EXTRA_TYPE, TYPE_LINEAR);
+ intent.putExtra(EXTRA_PACKAGE_NAME, ctx.getPackageName());
+ ctx.sendBroadcast(intent);
+ }
+
+ private static Intent createPaceIntent(Context ctx) {
+ Intent result = new Intent(ctx, PacemakerService.class);
+ result.setAction(ACTION_MANAGE_PACEMAKER);
+ result.putExtra(EXTRA_COMMAND, COMMAND_PACE);
+ return result;
+ }
+
+ private static void copyExtras(Intent from, Intent to) {
+ to.putExtra(EXTRA_COMMAND, from.getIntExtra(EXTRA_COMMAND, UNKNOWN_VALUE));
+ to.putExtra(EXTRA_DELAY, from.getLongExtra(EXTRA_DELAY, UNKNOWN_VALUE));
+ to.putExtra(EXTRA_TYPE, from.getIntExtra(EXTRA_TYPE, UNKNOWN_VALUE));
+ to.putExtra(EXTRA_PACKAGE_NAME, from.getStringExtra(EXTRA_PACKAGE_NAME));
+ }
+ }
+}
diff --git a/pacemaker/src/main/java/in/raveesh/pacemaker/Scheduler.java b/pacemaker/src/main/java/in/raveesh/pacemaker/Scheduler.java
new file mode 100644
index 0000000..15b49d5
--- /dev/null
+++ b/pacemaker/src/main/java/in/raveesh/pacemaker/Scheduler.java
@@ -0,0 +1,180 @@
+package in.raveesh.pacemaker;
+
+import android.os.SystemClock;
+import android.support.annotation.VisibleForTesting;
+
+import java.io.BufferedInputStream;
+import java.io.BufferedOutputStream;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.IOException;
+import java.io.ObjectInputStream;
+import java.io.ObjectOutputStream;
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Iterator;
+
+/**
+ * Utility to store setting for repeated paces
+ *
+ * @author Badoo
+ */
+public class Scheduler {
+ interface KeysValidator {
+ boolean isValid(String key);
+ }
+
+ private final TimeProvider mTimeProvider;
+ private File mFile;
+ private HashMap mRules = new HashMap<>();
+ private long mStartTime;
+
+ public Scheduler() {
+ this(new TimeProvider() {
+ @Override
+ public long getTime() {
+ return SystemClock.elapsedRealtime();
+ }
+ });
+ }
+
+
+ @VisibleForTesting
+ Scheduler(TimeProvider timeProvider) {
+ mTimeProvider = timeProvider;
+ }
+
+ /**
+ * Assigns file which is used to store/restore pace settings
+ * @param file
+ */
+ public void setData(File file) {
+ mFile = file;
+ if (!file.exists()) {
+ return;
+ }
+ ObjectInputStream stream = null;
+ try {
+ stream = new ObjectInputStream(new BufferedInputStream(new FileInputStream(file)));
+ //noinspection unchecked
+ mRules = (HashMap) stream.readObject();
+ mStartTime = stream.readLong();
+ }
+ catch (Exception e) {
+ // rules already been initialized with empty map
+ }
+ finally {
+ if (stream != null) {
+ try {
+ stream.close();
+ }
+ catch (IOException e) {
+ // Nothing to do
+ }
+ }
+ }
+ }
+
+ /**
+ * Adds new linear pace for specific id. Each id can have only one setting so each call overrides value for id
+ *
+ * @param id
+ * @param delay
+ * @return true if changes lead to change nextTriggerTime
+ */
+ public boolean addLinearPace(String id, long delay) {
+ long nextTrigger = getNextTriggerTime();
+ if (mRules.isEmpty()) {
+ mStartTime = mTimeProvider.getTime();
+ }
+ mRules.put(id, delay);
+ store();
+ return nextTrigger != getNextTriggerTime();
+ }
+
+ /**
+ * Removes linear pace for id
+ *
+ * @param id
+ * @return true if changes lead to change nextTriggerTime
+ */
+ public boolean removeLinearPace(String id) {
+ long nextTrigger = getNextTriggerTime();
+ mRules.remove(id);
+ if (mRules.isEmpty()) {
+ mStartTime = 0;
+ }
+ store();
+ return nextTrigger != getNextTriggerTime();
+ }
+
+ /**
+ * @return time when next pace should take place according to all rules
+ */
+ public long getNextTriggerTime() {
+ Collection values = mRules.values();
+ if (values.isEmpty()) {
+ return -1;
+ }
+ ArrayList list = new ArrayList<>();
+ list.addAll(values);
+ Collections.sort(list);
+ long linearStep = list.get(0);
+ return (int) ((Math.floor((mTimeProvider.getTime() - mStartTime) / (float) linearStep) + 1) * linearStep) + mStartTime;
+ }
+
+ /**
+ * Travers through all keys and checks if they still valid (using validator). If key is not valid - it is then removed.
+ * @param validator
+ * @return
+ */
+ public boolean syncKeys(KeysValidator validator) {
+ long nextTrigger = getNextTriggerTime();
+
+ Iterator keys = mRules.keySet().iterator();
+ while (keys.hasNext()) {
+ String k = keys.next();
+ if (!validator.isValid(k)) {
+ keys.remove();
+ }
+ }
+ store();
+ return nextTrigger != getNextTriggerTime();
+ }
+
+ /**
+ * Clears start time
+ */
+ public void resetStartTime() {
+ mStartTime = mTimeProvider.getTime();
+ store();
+ }
+
+ private void store() {
+ if (mFile == null) {
+ return;
+ }
+ ObjectOutputStream stream = null;
+ try {
+ stream = new ObjectOutputStream(new BufferedOutputStream(new FileOutputStream(mFile)));
+ stream.writeObject(mRules);
+ stream.writeLong(mStartTime);
+ }
+ catch (Exception e) {
+ // Sad story
+ }
+ finally {
+ if (stream != null) {
+ try {
+ stream.close();
+ }
+ catch (IOException e) {
+ // Nothing to do
+ }
+ }
+ }
+ }
+}
diff --git a/pacemaker/src/main/java/in/raveesh/pacemaker/TimeProvider.java b/pacemaker/src/main/java/in/raveesh/pacemaker/TimeProvider.java
new file mode 100644
index 0000000..ec27b9f
--- /dev/null
+++ b/pacemaker/src/main/java/in/raveesh/pacemaker/TimeProvider.java
@@ -0,0 +1,8 @@
+package in.raveesh.pacemaker;
+
+/**
+ * @author Badoo
+ */
+public interface TimeProvider {
+ long getTime();
+}