From 167cdb5956ad2e9f945d909fe644e9edbd8f9f2a Mon Sep 17 00:00:00 2001 From: Thomas Zurkan Date: Thu, 14 Sep 2017 14:43:22 -0700 Subject: [PATCH 01/12] Android O solution.... to start. Need to replace the scheduler with job service instead of alarm service. --- build.gradle | 6 +- .../datafile_handler/DatafileLoaderTest.java | 9 +- .../datafile_handler/DatafileServiceTest.java | 2 +- .../datafile_handler/DatafileLoader.java | 5 +- .../datafile_handler/DatafileRescheduler.java | 25 ++++- .../datafile_handler/DatafileService.java | 57 +++++++----- .../DatafileServiceConnection.java | 6 +- .../event_handler/EventIntentService.java | 35 +++++-- .../event_handler/EventRescheduler.java | 26 +++++- proguard-rules.txt | 2 +- shared/src/main/AndroidManifest.xml | 5 + .../ab/android/shared/JobWorkService.java | 93 +++++++++++++++++++ .../android/shared/ServiceWorkScheduled.java | 32 +++++++ 13 files changed, 255 insertions(+), 48 deletions(-) create mode 100644 shared/src/main/java/com/optimizely/ab/android/shared/JobWorkService.java create mode 100644 shared/src/main/java/com/optimizely/ab/android/shared/ServiceWorkScheduled.java diff --git a/build.gradle b/build.gradle index 4a43728d1..55a60ba33 100644 --- a/build.gradle +++ b/build.gradle @@ -47,10 +47,10 @@ allprojects { } ext { - compile_sdk_version = 24 - build_tools_version = "25.0.2" + compile_sdk_version = 26 + build_tools_version = "26.0.1" min_sdk_version = 10 - target_sdk_version = 24 + target_sdk_version = 26 java_core_ver = "1.8.0" android_logger_ver = "1.3.6" diff --git a/datafile-handler/src/androidTest/java/com/optimizely/ab/android/datafile_handler/DatafileLoaderTest.java b/datafile-handler/src/androidTest/java/com/optimizely/ab/android/datafile_handler/DatafileLoaderTest.java index 0bc75e898..5b110cea2 100644 --- a/datafile-handler/src/androidTest/java/com/optimizely/ab/android/datafile_handler/DatafileLoaderTest.java +++ b/datafile-handler/src/androidTest/java/com/optimizely/ab/android/datafile_handler/DatafileLoaderTest.java @@ -63,6 +63,7 @@ public class DatafileLoaderTest { private Client client; private Logger logger; private DatafileLoadedListener datafileLoadedListener; + Context context = InstrumentationRegistry.getTargetContext(); @Before public void setup() { @@ -86,7 +87,7 @@ public void tearDown() { public void loadFromCDNWhenNoCachedFile() throws MalformedURLException, JSONException { final ListeningExecutorService executor = MoreExecutors.newDirectExecutorService(); DatafileLoader datafileLoader = - new DatafileLoader(datafileService, datafileClient, datafileCache, executor, logger); + new DatafileLoader(context, datafileService, datafileClient, datafileCache, executor, logger); when(client.execute(any(Client.Request.class), anyInt(), anyInt())).thenReturn("{}"); @@ -107,7 +108,7 @@ public void loadFromCDNWhenNoCachedFile() throws MalformedURLException, JSONExce public void loadWhenCacheFileExistsAndCDNNotModified() { final ListeningExecutorService executor = MoreExecutors.newDirectExecutorService(); DatafileLoader datafileLoader = - new DatafileLoader(datafileService, datafileClient, datafileCache, executor, logger); + new DatafileLoader(context, datafileService, datafileClient, datafileCache, executor, logger); datafileCache.save("{}"); when(client.execute(any(Client.Request.class), anyInt(), anyInt())).thenReturn(""); @@ -129,7 +130,7 @@ public void loadWhenCacheFileExistsAndCDNNotModified() { public void noCacheAndLoadFromCDNFails() { final ListeningExecutorService executor = MoreExecutors.newDirectExecutorService(); DatafileLoader datafileLoader = - new DatafileLoader(datafileService, datafileClient, datafileCache, executor, logger); + new DatafileLoader(context, datafileService, datafileClient, datafileCache, executor, logger); when(client.execute(any(Client.Request.class), anyInt(), anyInt())).thenReturn(null); @@ -151,7 +152,7 @@ public void warningsAreLogged() throws IOException { Cache cache = mock(Cache.class); datafileCache = new DatafileCache("1", cache, logger); DatafileLoader datafileLoader = - new DatafileLoader(datafileService, datafileClient, datafileCache, executor, logger); + new DatafileLoader(context, datafileService, datafileClient, datafileCache, executor, logger); when(client.execute(any(Client.Request.class), anyInt(), anyInt())).thenReturn("{}"); when(cache.exists(datafileCache.getFileName())).thenReturn(true); diff --git a/datafile-handler/src/androidTest/java/com/optimizely/ab/android/datafile_handler/DatafileServiceTest.java b/datafile-handler/src/androidTest/java/com/optimizely/ab/android/datafile_handler/DatafileServiceTest.java index 612a09d22..bbeadc4a1 100644 --- a/datafile-handler/src/androidTest/java/com/optimizely/ab/android/datafile_handler/DatafileServiceTest.java +++ b/datafile-handler/src/androidTest/java/com/optimizely/ab/android/datafile_handler/DatafileServiceTest.java @@ -90,7 +90,7 @@ public void testBinding() throws TimeoutException { DatafileService datafileService = ((DatafileService.LocalBinder) binder).getService(); - DatafileLoader datafileLoader = new DatafileLoader(datafileService, datafileClient, datafileCache, MoreExecutors.newDirectExecutorService(), mock(Logger.class)); + DatafileLoader datafileLoader = new DatafileLoader(context, datafileService, datafileClient, datafileCache, MoreExecutors.newDirectExecutorService(), mock(Logger.class)); datafileService.getDatafile("1", datafileLoader, datafileLoadedListener); assertTrue(datafileService.isBound()); diff --git a/datafile-handler/src/main/java/com/optimizely/ab/android/datafile_handler/DatafileLoader.java b/datafile-handler/src/main/java/com/optimizely/ab/android/datafile_handler/DatafileLoader.java index 1dc628049..03af665ef 100644 --- a/datafile-handler/src/main/java/com/optimizely/ab/android/datafile_handler/DatafileLoader.java +++ b/datafile-handler/src/main/java/com/optimizely/ab/android/datafile_handler/DatafileLoader.java @@ -41,7 +41,8 @@ public class DatafileLoader { private boolean hasNotifiedListener = false; - public DatafileLoader(@NonNull DatafileService datafileService, + public DatafileLoader(@NonNull Context context, + @NonNull DatafileService datafileService, @NonNull DatafileClient datafileClient, @NonNull DatafileCache datafileCache, @NonNull Executor executor, @@ -52,7 +53,7 @@ public DatafileLoader(@NonNull DatafileService datafileService, this.datafileCache = datafileCache; this.executor = executor; - new DatafileServiceConnection("projectId", datafileService.getApplicationContext(), new DatafileLoadedListener() { + new DatafileServiceConnection("projectId", context.getApplicationContext(), new DatafileLoadedListener() { public void onDatafileLoaded(@Nullable String dataFile) {} public void onStop(Context context) {} }); diff --git a/datafile-handler/src/main/java/com/optimizely/ab/android/datafile_handler/DatafileRescheduler.java b/datafile-handler/src/main/java/com/optimizely/ab/android/datafile_handler/DatafileRescheduler.java index a905e999a..789571236 100644 --- a/datafile-handler/src/main/java/com/optimizely/ab/android/datafile_handler/DatafileRescheduler.java +++ b/datafile-handler/src/main/java/com/optimizely/ab/android/datafile_handler/DatafileRescheduler.java @@ -16,12 +16,18 @@ package com.optimizely.ab.android.datafile_handler; +import android.app.job.JobInfo; +import android.app.job.JobScheduler; +import android.app.job.JobWorkItem; import android.content.BroadcastReceiver; +import android.content.ComponentName; import android.content.Context; import android.content.Intent; +import android.os.Build; import android.support.annotation.NonNull; import com.optimizely.ab.android.shared.Cache; +import com.optimizely.ab.android.shared.JobWorkService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -91,11 +97,28 @@ void dispatch(Intent intent) { List projectIds = backgroundWatchersCache.getWatchingProjectIds(); for (String projectId : projectIds) { intent.putExtra(DatafileService.EXTRA_PROJECT_ID, projectId); - context.startService(intent); + startService(context, intent); logger.info("Rescheduled data file watching for project {}", projectId); } } + private void startService(Context context, Intent intent) { + if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + int JOBID = 2113; + JobInfo jobInfo = new JobInfo.Builder(JOBID, + new ComponentName(context, JobWorkService.class)).setOverrideDeadline(0).build(); + JobScheduler jobScheduler = (JobScheduler) context.getSystemService(Context.JOB_SCHEDULER_SERVICE); + + jobScheduler.enqueue(jobInfo, new JobWorkItem(intent)); + + } + else { + context.startService(intent); + } + + } } + + } diff --git a/datafile-handler/src/main/java/com/optimizely/ab/android/datafile_handler/DatafileService.java b/datafile-handler/src/main/java/com/optimizely/ab/android/datafile_handler/DatafileService.java index 33e811fbc..0796744dc 100644 --- a/datafile-handler/src/main/java/com/optimizely/ab/android/datafile_handler/DatafileService.java +++ b/datafile-handler/src/main/java/com/optimizely/ab/android/datafile_handler/DatafileService.java @@ -17,16 +17,19 @@ package com.optimizely.ab.android.datafile_handler; import android.app.Service; +import android.content.Context; import android.content.Intent; import android.os.Binder; import android.os.Build; import android.os.IBinder; import android.support.annotation.NonNull; +import android.support.annotation.Nullable; import android.support.annotation.RequiresApi; import com.optimizely.ab.android.shared.Cache; import com.optimizely.ab.android.shared.Client; import com.optimizely.ab.android.shared.OptlyStorage; +import com.optimizely.ab.android.shared.ServiceWorkScheduled; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -38,7 +41,7 @@ * These services will only be used if you are using our {@link DefaultDatafileHandler}. * You can chose to implement your own handler and use all or part of this package. */ -public class DatafileService extends Service { +public class DatafileService extends Service implements ServiceWorkScheduled { /** * Extra containing the project id this instance of Optimizely was built with */ @@ -57,26 +60,7 @@ public class DatafileService extends Service { @RequiresApi(api = Build.VERSION_CODES.HONEYCOMB) @Override public int onStartCommand(Intent intent, int flags, int startId) { - if (intent != null) { - if (intent.hasExtra(EXTRA_PROJECT_ID)) { - String projectId = intent.getStringExtra(EXTRA_PROJECT_ID); - DatafileClient datafileClient = new DatafileClient( - new Client(new OptlyStorage(getApplicationContext()), LoggerFactory.getLogger(OptlyStorage.class)), - LoggerFactory.getLogger(DatafileClient.class)); - DatafileCache datafileCache = new DatafileCache( - projectId, - new Cache(getApplicationContext(), LoggerFactory.getLogger(Cache.class)), - LoggerFactory.getLogger(DatafileCache.class)); - - String datafileUrl = getDatafileUrl(projectId); - DatafileLoader datafileLoader = new DatafileLoader(this, datafileClient, datafileCache, Executors.newSingleThreadExecutor(), LoggerFactory.getLogger(DatafileLoader.class)); - datafileLoader.getDatafile(datafileUrl, null); - } else { - logger.warn("Data file service received an intent with no project id extra"); - } - } else { - logger.warn("Data file service received a null intent"); - } + onWork(this, intent); return super.onStartCommand(intent, flags, startId); } @@ -119,6 +103,37 @@ public void getDatafile(String projectId, DatafileLoader datafileLoader, Datafil datafileLoader.getDatafile(datafileUrl, loadedListener); } + @Override + public void initialize(@NonNull Context context) { + + } + + @RequiresApi(api = Build.VERSION_CODES.HONEYCOMB) + @Override + public void onWork(@NonNull Context context, @Nullable Intent intent) { + if (intent != null) { + if (intent.hasExtra(EXTRA_PROJECT_ID)) { + String projectId = intent.getStringExtra(EXTRA_PROJECT_ID); + DatafileClient datafileClient = new DatafileClient( + new Client(new OptlyStorage(context.getApplicationContext()), LoggerFactory.getLogger(OptlyStorage.class)), + LoggerFactory.getLogger(DatafileClient.class)); + DatafileCache datafileCache = new DatafileCache( + projectId, + new Cache(context.getApplicationContext(), LoggerFactory.getLogger(Cache.class)), + LoggerFactory.getLogger(DatafileCache.class)); + + String datafileUrl = getDatafileUrl(projectId); + DatafileLoader datafileLoader = new DatafileLoader(context, this, datafileClient, datafileCache, Executors.newSingleThreadExecutor(), LoggerFactory.getLogger(DatafileLoader.class)); + datafileLoader.getDatafile(datafileUrl, null); + } else { + logger.warn("Data file service received an intent with no project id extra"); + } + } else { + logger.warn("Data file service received a null intent"); + } + + } + public class LocalBinder extends Binder { public DatafileService getService() { // Return this instance of LocalService so clients can call public methods diff --git a/datafile-handler/src/main/java/com/optimizely/ab/android/datafile_handler/DatafileServiceConnection.java b/datafile-handler/src/main/java/com/optimizely/ab/android/datafile_handler/DatafileServiceConnection.java index cf440925f..13163a67e 100644 --- a/datafile-handler/src/main/java/com/optimizely/ab/android/datafile_handler/DatafileServiceConnection.java +++ b/datafile-handler/src/main/java/com/optimizely/ab/android/datafile_handler/DatafileServiceConnection.java @@ -74,16 +74,16 @@ public void onServiceConnected(ComponentName className, final DatafileService datafileService = binder.getService(); if (datafileService != null) { DatafileClient datafileClient = new DatafileClient( - new Client(new OptlyStorage(datafileService.getApplicationContext()), + new Client(new OptlyStorage(context.getApplicationContext()), LoggerFactory.getLogger(OptlyStorage.class)), LoggerFactory.getLogger(DatafileClient.class)); DatafileCache datafileCache = new DatafileCache( projectId, - new Cache(datafileService.getApplicationContext(), LoggerFactory.getLogger(Cache.class)), + new Cache(context.getApplicationContext(), LoggerFactory.getLogger(Cache.class)), LoggerFactory.getLogger(DatafileCache.class)); - DatafileLoader datafileLoader = new DatafileLoader(datafileService, + DatafileLoader datafileLoader = new DatafileLoader(context, datafileService, datafileClient, datafileCache, Executors.newSingleThreadExecutor(), diff --git a/event-handler/src/main/java/com/optimizely/ab/android/event_handler/EventIntentService.java b/event-handler/src/main/java/com/optimizely/ab/android/event_handler/EventIntentService.java index 6f588388e..0d955ab3e 100644 --- a/event-handler/src/main/java/com/optimizely/ab/android/event_handler/EventIntentService.java +++ b/event-handler/src/main/java/com/optimizely/ab/android/event_handler/EventIntentService.java @@ -18,14 +18,17 @@ import android.app.AlarmManager; import android.app.IntentService; +import android.content.Context; import android.content.Intent; import android.os.Build; +import android.support.annotation.NonNull; import android.support.annotation.Nullable; import android.support.annotation.RequiresApi; import com.optimizely.ab.android.shared.Client; import com.optimizely.ab.android.shared.OptlyStorage; import com.optimizely.ab.android.shared.ServiceScheduler; +import com.optimizely.ab.android.shared.ServiceWorkScheduled; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -39,7 +42,7 @@ * worker queue. * */ -public class EventIntentService extends IntentService { +public class EventIntentService extends IntentService implements ServiceWorkScheduled { static final String EXTRA_URL = "com.optimizely.ab.android.EXTRA_URL"; static final String EXTRA_REQUEST_BODY = "com.optimizely.ab.android.EXTRA_REQUEST_BODY"; static final String EXTRA_INTERVAL = "com.optimizely.ab.android.EXTRA_INTERVAL"; @@ -59,15 +62,7 @@ public EventIntentService() { public void onCreate() { super.onCreate(); - OptlyStorage optlyStorage = new OptlyStorage(this); - EventClient eventClient = new EventClient(new Client(optlyStorage, - LoggerFactory.getLogger(Client.class)), LoggerFactory.getLogger(EventClient.class)); - EventDAO eventDAO = EventDAO.getInstance(this, "1", LoggerFactory.getLogger(EventDAO.class)); - ServiceScheduler serviceScheduler = new ServiceScheduler( - (AlarmManager) getSystemService(ALARM_SERVICE), - new ServiceScheduler.PendingIntentFactory(this), - LoggerFactory.getLogger(ServiceScheduler.class)); - eventDispatcher = new EventDispatcher(this, optlyStorage, eventDAO, eventClient, serviceScheduler, LoggerFactory.getLogger(EventDispatcher.class)); + initialize(this); } /** @@ -77,6 +72,11 @@ public void onCreate() { @Override protected void onHandleIntent(Intent intent) { + onWork(this, intent); + } + + @Override + public void onWork(@NonNull Context context, @Nullable Intent intent) { if (intent == null) { logger.warn("Handled a null intent"); return; @@ -89,4 +89,19 @@ protected void onHandleIntent(Intent intent) { logger.warn("Unable to create dependencies needed by intent handler"); } } + + @RequiresApi(api = Build.VERSION_CODES.HONEYCOMB) + @Override + public void initialize(@NonNull Context context) { + OptlyStorage optlyStorage = new OptlyStorage(context); + EventClient eventClient = new EventClient(new Client(optlyStorage, + LoggerFactory.getLogger(Client.class)), LoggerFactory.getLogger(EventClient.class)); + EventDAO eventDAO = EventDAO.getInstance(context, "1", LoggerFactory.getLogger(EventDAO.class)); + ServiceScheduler serviceScheduler = new ServiceScheduler( + (AlarmManager) context.getSystemService(ALARM_SERVICE), + new ServiceScheduler.PendingIntentFactory(context), + LoggerFactory.getLogger(ServiceScheduler.class)); + eventDispatcher = new EventDispatcher(context, optlyStorage, eventDAO, eventClient, serviceScheduler, LoggerFactory.getLogger(EventDispatcher.class)); + + } } diff --git a/event-handler/src/main/java/com/optimizely/ab/android/event_handler/EventRescheduler.java b/event-handler/src/main/java/com/optimizely/ab/android/event_handler/EventRescheduler.java index 5e432e53b..8b57e3a05 100644 --- a/event-handler/src/main/java/com/optimizely/ab/android/event_handler/EventRescheduler.java +++ b/event-handler/src/main/java/com/optimizely/ab/android/event_handler/EventRescheduler.java @@ -17,12 +17,18 @@ package com.optimizely.ab.android.event_handler; import android.app.AlarmManager; +import android.app.job.JobInfo; +import android.app.job.JobScheduler; +import android.app.job.JobWorkItem; import android.content.BroadcastReceiver; +import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.net.wifi.WifiManager; +import android.os.Build; import android.support.annotation.NonNull; +import com.optimizely.ab.android.shared.JobWorkService; import com.optimizely.ab.android.shared.ServiceScheduler; import org.slf4j.Logger; @@ -84,7 +90,7 @@ public void onReceive(Context context, Intent intent) { void reschedule(@NonNull Context context, @NonNull Intent broadcastIntent, @NonNull Intent eventServiceIntent, @NonNull ServiceScheduler serviceScheduler) { if (broadcastIntent.getAction().equals(Intent.ACTION_BOOT_COMPLETED) || broadcastIntent.getAction().equals(Intent.ACTION_MY_PACKAGE_REPLACED)) { - context.startService(eventServiceIntent); + startService(context, eventServiceIntent); logger.info("Rescheduling event flushing if necessary"); } else if (broadcastIntent.getAction().equals(WifiManager.SUPPLICANT_CONNECTION_CHANGE_ACTION) && broadcastIntent.getBooleanExtra(WifiManager.EXTRA_SUPPLICANT_CONNECTED, false)) { @@ -95,11 +101,27 @@ void reschedule(@NonNull Context context, @NonNull Intent broadcastIntent, @NonN // with wifi the service will be rescheduled on the interval. // Wifi connection state changes all the time and starting services is expensive // so it's important to only do this if we have stored events. - context.startService(eventServiceIntent); + startService(context, eventServiceIntent); logger.info("Preemptively flushing events since wifi became available"); } } else { logger.warn("Received unsupported broadcast action to event rescheduler"); } } + + private void startService(Context context, Intent intent) { + if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + int JOBID = 2112; + JobInfo jobInfo = new JobInfo.Builder(JOBID, + new ComponentName(context, JobWorkService.class)).setOverrideDeadline(0).build(); + JobScheduler jobScheduler = (JobScheduler) context.getSystemService(Context.JOB_SCHEDULER_SERVICE); + + jobScheduler.enqueue(jobInfo, new JobWorkItem(intent)); + + } + else { + context.startService(intent); + } + + } } diff --git a/proguard-rules.txt b/proguard-rules.txt index 24bff9326..1f8e8c571 100644 --- a/proguard-rules.txt +++ b/proguard-rules.txt @@ -20,7 +20,7 @@ ## https://developers.optimizely.com/x/solutions/sdks/reference/index.html?language=android&platform=mobile#installation # Optimizely --keep class com.optimizely.ab.** { *; } +-keep class com.optimizely.ab.android.shared.JobWorkService # Gson -keepnames class com.google.gson.Gson diff --git a/shared/src/main/AndroidManifest.xml b/shared/src/main/AndroidManifest.xml index 5adb16528..f5e22c597 100644 --- a/shared/src/main/AndroidManifest.xml +++ b/shared/src/main/AndroidManifest.xml @@ -19,4 +19,9 @@ + + + + diff --git a/shared/src/main/java/com/optimizely/ab/android/shared/JobWorkService.java b/shared/src/main/java/com/optimizely/ab/android/shared/JobWorkService.java new file mode 100644 index 000000000..b27fcea82 --- /dev/null +++ b/shared/src/main/java/com/optimizely/ab/android/shared/JobWorkService.java @@ -0,0 +1,93 @@ +package com.optimizely.ab.android.shared; + +import android.app.Notification; +import android.app.NotificationManager; +import android.app.PendingIntent; +import android.app.job.JobParameters; +import android.app.job.JobService; +import android.app.job.JobWorkItem; +import android.content.Intent; +import android.os.AsyncTask; +import android.os.Build; +import android.support.annotation.RequiresApi; +import android.util.Log; + +/** + * This is an example of implementing a {@link JobService} that dispatches work enqueued in + * to it. The class shows how to interact with the service. + */ +//BEGIN_INCLUDE(service) +@RequiresApi(api = Build.VERSION_CODES.O) +public class JobWorkService extends JobService { + private NotificationManager mNM; + private CommandProcessor mCurProcessor; + /** + * This is a task to dequeue and process work in the background. + */ + final class CommandProcessor extends AsyncTask { + private final JobParameters mParams; + CommandProcessor(JobParameters params) { + mParams = params; + } + @Override + protected Void doInBackground(Void... params) { + boolean cancelled; + JobWorkItem work; + /** + * Iterate over available work. Once dequeueWork() returns null, the + * job's work queue is empty and the job has stopped, so we can let this + * async task complete. + */ + while (!(cancelled=isCancelled()) && (work=mParams.dequeueWork()) != null) { + String componentClass = work.getIntent().getComponent().getClassName(); + Class clazz = null; + Log.i("JobWorkService", "Processing work: " + work + ", component: " + componentClass); + try { + clazz = Class.forName(componentClass); + Object intentService = clazz.newInstance(); + if (intentService instanceof ServiceWorkScheduled) { + ServiceWorkScheduled serviceWorkScheduled = (ServiceWorkScheduled) intentService; + serviceWorkScheduled.initialize(getApplicationContext()); + serviceWorkScheduled.onWork(getApplicationContext(), work.getIntent()); + } + } catch (Exception e) { + Log.e("JobSerivice", "Error creating ServiceWorkScheduled", e); + } + // Tell system we have finished processing the work. + Log.i("JobWorkService", "Done with: " + work); + } + if (cancelled) { + Log.i("JobWorkService", "CANCELLED!"); + } + return null; + } + } + @Override + public void onCreate() { + + } + + @Override + public void onDestroy() { + } + + @Override + public boolean onStartJob(JobParameters params) { + // Start task to pull work out of the queue and process it. + mCurProcessor = new CommandProcessor(params); + mCurProcessor.executeOnExecutor(AsyncTask.THREAD_POOL_EXECUTOR); + // Allow the job to continue running while we process work. + return true; + } + + @Override + public boolean onStopJob(JobParameters params) { + // Have the processor cancel its current work. + mCurProcessor.cancel(true); + // Tell the system to reschedule the job -- the only reason we would be here is + // because the job needs to stop for some reason before it has completed all of + // its work, so we would like it to remain to finish that work in the future. + return true; + } +} +//END_INCLUDE(service) \ No newline at end of file diff --git a/shared/src/main/java/com/optimizely/ab/android/shared/ServiceWorkScheduled.java b/shared/src/main/java/com/optimizely/ab/android/shared/ServiceWorkScheduled.java new file mode 100644 index 000000000..5ce58679c --- /dev/null +++ b/shared/src/main/java/com/optimizely/ab/android/shared/ServiceWorkScheduled.java @@ -0,0 +1,32 @@ +/**************************************************************************** + * Copyright 2017, Optimizely, Inc. and contributors * + * * + * Licensed under the Apache License, Version 2.0 (the "License"); * + * you may not use this file except in compliance with the License. * + * You may obtain a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, software * + * distributed under the License is distributed on an "AS IS" BASIS, * + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * + * See the License for the specific language governing permissions and * + * limitations under the License. * + ***************************************************************************/ + +package com.optimizely.ab.android.shared; + +import android.content.Context; +import android.content.Intent; +import android.support.annotation.NonNull; +import android.support.annotation.Nullable; + +/** + * This interface is implemented by any service that wants to run in the background. If it is pre-Android O it will not be called except from + * within the Service onHandleIntent or onStartCommand. If we are Android O or later, the job service will hold a handle to this interface and call + * it if and when the job service is called. + */ +public interface ServiceWorkScheduled { + void initialize(@NonNull Context context); + void onWork(@NonNull Context context, @Nullable Intent intent); +} From 61ec73585ab356fb361c365d6cf1a298d0eb9dc1 Mon Sep 17 00:00:00 2001 From: Thomas Zurkan Date: Fri, 15 Sep 2017 01:24:58 -0700 Subject: [PATCH 02/12] use job scheduler instead of alarm service for android o --- .../datafile_handler/DatafileRescheduler.java | 10 ++- .../datafile_handler/DatafileService.java | 1 + .../DefaultDatafileHandler.java | 8 +-- .../event_handler/EventIntentService.java | 4 +- .../event_handler/EventRescheduler.java | 11 +-- .../ab/android/shared/JobWorkService.java | 1 + .../ab/android/shared/ServiceScheduler.java | 68 +++++++++++++++++-- 7 files changed, 82 insertions(+), 21 deletions(-) diff --git a/datafile-handler/src/main/java/com/optimizely/ab/android/datafile_handler/DatafileRescheduler.java b/datafile-handler/src/main/java/com/optimizely/ab/android/datafile_handler/DatafileRescheduler.java index 789571236..3925fb84a 100644 --- a/datafile-handler/src/main/java/com/optimizely/ab/android/datafile_handler/DatafileRescheduler.java +++ b/datafile-handler/src/main/java/com/optimizely/ab/android/datafile_handler/DatafileRescheduler.java @@ -105,9 +105,13 @@ void dispatch(Intent intent) { } private void startService(Context context, Intent intent) { if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - int JOBID = 2113; - JobInfo jobInfo = new JobInfo.Builder(JOBID, - new ComponentName(context, JobWorkService.class)).setOverrideDeadline(0).build(); + JobInfo jobInfo = new JobInfo.Builder(DatafileService.JOB_ID, + new ComponentName(context, JobWorkService.class)) + // schedule it to run any time between 1 - 5 minutes + .setMinimumLatency(JobWorkService.ONE_MINUTE) + .setOverrideDeadline(5 * JobWorkService.ONE_MINUTE) + .build(); + JobScheduler jobScheduler = (JobScheduler) context.getSystemService(Context.JOB_SCHEDULER_SERVICE); jobScheduler.enqueue(jobInfo, new JobWorkItem(intent)); diff --git a/datafile-handler/src/main/java/com/optimizely/ab/android/datafile_handler/DatafileService.java b/datafile-handler/src/main/java/com/optimizely/ab/android/datafile_handler/DatafileService.java index 0796744dc..665c86014 100644 --- a/datafile-handler/src/main/java/com/optimizely/ab/android/datafile_handler/DatafileService.java +++ b/datafile-handler/src/main/java/com/optimizely/ab/android/datafile_handler/DatafileService.java @@ -48,6 +48,7 @@ public class DatafileService extends Service implements ServiceWorkScheduled { public static final String EXTRA_PROJECT_ID = "com.optimizely.ab.android.EXTRA_PROJECT_ID"; public static final String FORMAT_VERSIONED_CDN_URL = "https://cdn.optimizely.com/public/%s/datafile_v%s.json"; static final String DATAFILE_VERSION = "3"; + public static final Integer JOB_ID = 2113; @NonNull private final IBinder binder = new LocalBinder(); Logger logger = LoggerFactory.getLogger(DatafileService.class); diff --git a/datafile-handler/src/main/java/com/optimizely/ab/android/datafile_handler/DefaultDatafileHandler.java b/datafile-handler/src/main/java/com/optimizely/ab/android/datafile_handler/DefaultDatafileHandler.java index a2cde75be..b2226793b 100644 --- a/datafile-handler/src/main/java/com/optimizely/ab/android/datafile_handler/DefaultDatafileHandler.java +++ b/datafile-handler/src/main/java/com/optimizely/ab/android/datafile_handler/DefaultDatafileHandler.java @@ -108,11 +108,9 @@ public void onStop(Context context) { public void startBackgroundUpdates(Context context, String projectId, Long updateInterval) { enableBackgroundCache(context, projectId); - AlarmManager alarmManager = (AlarmManager) context - .getSystemService(Context.ALARM_SERVICE); ServiceScheduler.PendingIntentFactory pendingIntentFactory = new ServiceScheduler .PendingIntentFactory(context.getApplicationContext()); - ServiceScheduler serviceScheduler = new ServiceScheduler(alarmManager, pendingIntentFactory, + ServiceScheduler serviceScheduler = new ServiceScheduler(context, pendingIntentFactory, LoggerFactory.getLogger(ServiceScheduler.class)); Intent intent = new Intent(context.getApplicationContext(), DatafileService.class); @@ -127,11 +125,9 @@ public void startBackgroundUpdates(Context context, String projectId, Long updat * @param projectId project id of the datafile uploading */ public void stopBackgroundUpdates(Context context, String projectId) { - AlarmManager alarmManager = (AlarmManager) context - .getSystemService(Context.ALARM_SERVICE); ServiceScheduler.PendingIntentFactory pendingIntentFactory = new ServiceScheduler .PendingIntentFactory(context.getApplicationContext()); - ServiceScheduler serviceScheduler = new ServiceScheduler(alarmManager, pendingIntentFactory, + ServiceScheduler serviceScheduler = new ServiceScheduler(context, pendingIntentFactory, LoggerFactory.getLogger(ServiceScheduler.class)); Intent intent = new Intent(context.getApplicationContext(), DatafileService.class); serviceScheduler.unschedule(intent); diff --git a/event-handler/src/main/java/com/optimizely/ab/android/event_handler/EventIntentService.java b/event-handler/src/main/java/com/optimizely/ab/android/event_handler/EventIntentService.java index 0d955ab3e..5359ff006 100644 --- a/event-handler/src/main/java/com/optimizely/ab/android/event_handler/EventIntentService.java +++ b/event-handler/src/main/java/com/optimizely/ab/android/event_handler/EventIntentService.java @@ -46,6 +46,8 @@ public class EventIntentService extends IntentService implements ServiceWorkSche static final String EXTRA_URL = "com.optimizely.ab.android.EXTRA_URL"; static final String EXTRA_REQUEST_BODY = "com.optimizely.ab.android.EXTRA_REQUEST_BODY"; static final String EXTRA_INTERVAL = "com.optimizely.ab.android.EXTRA_INTERVAL"; + public static final Integer JOB_ID = 2112; + Logger logger = LoggerFactory.getLogger(EventIntentService.class); @Nullable EventDispatcher eventDispatcher; @@ -98,7 +100,7 @@ public void initialize(@NonNull Context context) { LoggerFactory.getLogger(Client.class)), LoggerFactory.getLogger(EventClient.class)); EventDAO eventDAO = EventDAO.getInstance(context, "1", LoggerFactory.getLogger(EventDAO.class)); ServiceScheduler serviceScheduler = new ServiceScheduler( - (AlarmManager) context.getSystemService(ALARM_SERVICE), + context, new ServiceScheduler.PendingIntentFactory(context), LoggerFactory.getLogger(ServiceScheduler.class)); eventDispatcher = new EventDispatcher(context, optlyStorage, eventDAO, eventClient, serviceScheduler, LoggerFactory.getLogger(EventDispatcher.class)); diff --git a/event-handler/src/main/java/com/optimizely/ab/android/event_handler/EventRescheduler.java b/event-handler/src/main/java/com/optimizely/ab/android/event_handler/EventRescheduler.java index 8b57e3a05..a20a001de 100644 --- a/event-handler/src/main/java/com/optimizely/ab/android/event_handler/EventRescheduler.java +++ b/event-handler/src/main/java/com/optimizely/ab/android/event_handler/EventRescheduler.java @@ -70,7 +70,7 @@ public class EventRescheduler extends BroadcastReceiver { public void onReceive(Context context, Intent intent) { if (context != null && intent != null) { ServiceScheduler serviceScheduler = new ServiceScheduler( - (AlarmManager) context.getSystemService(ALARM_SERVICE), + context, new ServiceScheduler.PendingIntentFactory(context), LoggerFactory.getLogger(ServiceScheduler.class)); Intent eventServiceIntent = new Intent(context, EventIntentService.class); @@ -111,9 +111,12 @@ void reschedule(@NonNull Context context, @NonNull Intent broadcastIntent, @NonN private void startService(Context context, Intent intent) { if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - int JOBID = 2112; - JobInfo jobInfo = new JobInfo.Builder(JOBID, - new ComponentName(context, JobWorkService.class)).setOverrideDeadline(0).build(); + JobInfo jobInfo = new JobInfo.Builder(EventIntentService.JOB_ID, + new ComponentName(context, JobWorkService.class)) + // schedule it to run any time between 1 - 5 minutes + .setMinimumLatency(JobWorkService.ONE_MINUTE) + .setOverrideDeadline(5 * JobWorkService.ONE_MINUTE) + .build(); JobScheduler jobScheduler = (JobScheduler) context.getSystemService(Context.JOB_SCHEDULER_SERVICE); jobScheduler.enqueue(jobInfo, new JobWorkItem(intent)); diff --git a/shared/src/main/java/com/optimizely/ab/android/shared/JobWorkService.java b/shared/src/main/java/com/optimizely/ab/android/shared/JobWorkService.java index b27fcea82..5a36700e2 100644 --- a/shared/src/main/java/com/optimizely/ab/android/shared/JobWorkService.java +++ b/shared/src/main/java/com/optimizely/ab/android/shared/JobWorkService.java @@ -19,6 +19,7 @@ //BEGIN_INCLUDE(service) @RequiresApi(api = Build.VERSION_CODES.O) public class JobWorkService extends JobService { + public static final int ONE_MINUTE = 60 * 1000; private NotificationManager mNM; private CommandProcessor mCurProcessor; /** diff --git a/shared/src/main/java/com/optimizely/ab/android/shared/ServiceScheduler.java b/shared/src/main/java/com/optimizely/ab/android/shared/ServiceScheduler.java index 4e7d414d6..815cf5ce1 100644 --- a/shared/src/main/java/com/optimizely/ab/android/shared/ServiceScheduler.java +++ b/shared/src/main/java/com/optimizely/ab/android/shared/ServiceScheduler.java @@ -18,9 +18,15 @@ import android.app.AlarmManager; import android.app.PendingIntent; +import android.app.job.JobInfo; +import android.app.job.JobScheduler; +import android.app.job.JobWorkItem; +import android.content.ComponentName; import android.content.Context; import android.content.Intent; +import android.os.Build; import android.support.annotation.NonNull; +import android.util.Log; import org.slf4j.Logger; @@ -31,20 +37,22 @@ // TODO Unit test coverage public class ServiceScheduler { - @NonNull private final AlarmManager alarmManager; + // @NonNull private final AlarmManager alarmManager; @NonNull private final PendingIntentFactory pendingIntentFactory; @NonNull private final Logger logger; + @NonNull private final Context context; /** - * @param alarmManager an instance of {@link AlarmManager} + * @param context an instance of {@link Context} * @param pendingIntentFactory an instance of {@link PendingIntentFactory} * @param logger an instance of {@link Logger} * @hide */ - public ServiceScheduler(@NonNull AlarmManager alarmManager, @NonNull PendingIntentFactory pendingIntentFactory, @NonNull Logger logger) { - this.alarmManager = alarmManager; + public ServiceScheduler(@NonNull Context context, @NonNull PendingIntentFactory pendingIntentFactory, @NonNull Logger logger) { + //this.alarmManager = alarmManager; this.pendingIntentFactory = pendingIntentFactory; this.logger = logger; + this.context = context; } /** @@ -68,11 +76,58 @@ public void schedule(Intent intent, long interval) { } PendingIntent pendingIntent = pendingIntentFactory.getPendingIntent(intent); - alarmManager.setInexactRepeating(AlarmManager.ELAPSED_REALTIME, interval, interval, pendingIntent); + + setRepeating(interval, pendingIntent, intent); logger.info("Scheduled {}", intent.getComponent().toShortString()); } + + private void setRepeating(long interval, PendingIntent pendingIntent, Intent intent) { + if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + JobScheduler jobScheduler = (JobScheduler) + context.getSystemService(Context.JOB_SCHEDULER_SERVICE); + JobInfo.Builder builder = new JobInfo.Builder(1, + new ComponentName(context.getApplicationContext(), + JobWorkService.class.getName())); + builder.setPeriodic(interval); + builder.setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY); + + if (jobScheduler.enqueue(builder.build(), new JobWorkItem(intent)) <= 0) { + Log.e("ServiceScheduler", "Some error while scheduling the job"); + } + + } + else { + AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); + alarmManager.setInexactRepeating(AlarmManager.ELAPSED_REALTIME, interval, interval, pendingIntent); + } + } + + private void cancelRepeating(PendingIntent pendingIntent, Intent intent) { + if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + JobScheduler jobScheduler = (JobScheduler) + context.getSystemService(Context.JOB_SCHEDULER_SERVICE); + String clazz = intent.getComponent().getClassName(); + Integer id = null; + try { + id = (Integer) Class.forName(clazz).getDeclaredField("JOB_ID").get(null); + jobScheduler.cancel(id); + } catch (IllegalAccessException e) { + e.printStackTrace(); + } catch (NoSuchFieldException e) { + e.printStackTrace(); + } catch (ClassNotFoundException e) { + e.printStackTrace(); + } + } + else { + AlarmManager alarmManager = (AlarmManager) context.getSystemService(Context.ALARM_SERVICE); + alarmManager.cancel(pendingIntent); + pendingIntent.cancel(); + } + } + /** * Unschedule a scheduled {@link Intent} * @@ -84,8 +139,7 @@ public void unschedule(Intent intent) { if (intent != null) { try { PendingIntent pendingIntent = pendingIntentFactory.getPendingIntent(intent); - alarmManager.cancel(pendingIntent); - pendingIntent.cancel(); + cancelRepeating(pendingIntent, intent); logger.info("Unscheduled {}", intent.getComponent().toShortString()); } catch (Exception e) { logger.debug("Failed to unschedule service", e); From c602dbd66a9eb373f5f36c8141ab778073707148 Mon Sep 17 00:00:00 2001 From: Thomas Zurkan Date: Fri, 15 Sep 2017 13:58:59 -0700 Subject: [PATCH 03/12] more clarification on android o work --- .../datafile_handler/DatafileRescheduler.java | 33 ++++------------ .../datafile_handler/DatafileService.java | 4 +- .../event_handler/EventIntentService.java | 5 +-- .../event_handler/EventRescheduler.java | 22 +---------- ...uled.java => JobWorkScheduledService.java} | 2 +- .../ab/android/shared/JobWorkService.java | 23 ++++++++--- .../ab/android/shared/ServiceScheduler.java | 39 +++++++++++++++++++ 7 files changed, 72 insertions(+), 56 deletions(-) rename shared/src/main/java/com/optimizely/ab/android/shared/{ServiceWorkScheduled.java => JobWorkScheduledService.java} (97%) diff --git a/datafile-handler/src/main/java/com/optimizely/ab/android/datafile_handler/DatafileRescheduler.java b/datafile-handler/src/main/java/com/optimizely/ab/android/datafile_handler/DatafileRescheduler.java index 3925fb84a..174d7d32d 100644 --- a/datafile-handler/src/main/java/com/optimizely/ab/android/datafile_handler/DatafileRescheduler.java +++ b/datafile-handler/src/main/java/com/optimizely/ab/android/datafile_handler/DatafileRescheduler.java @@ -28,6 +28,7 @@ import com.optimizely.ab.android.shared.Cache; import com.optimizely.ab.android.shared.JobWorkService; +import com.optimizely.ab.android.shared.ServiceScheduler; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -83,9 +84,12 @@ public void onReceive(Context context, Intent intent) { */ static class Dispatcher { - @NonNull private final Context context; - @NonNull private final BackgroundWatchersCache backgroundWatchersCache; - @NonNull private final Logger logger; + @NonNull + private final Context context; + @NonNull + private final BackgroundWatchersCache backgroundWatchersCache; + @NonNull + private final Logger logger; Dispatcher(@NonNull Context context, @NonNull BackgroundWatchersCache backgroundWatchersCache, @NonNull Logger logger) { this.context = context; @@ -97,32 +101,11 @@ void dispatch(Intent intent) { List projectIds = backgroundWatchersCache.getWatchingProjectIds(); for (String projectId : projectIds) { intent.putExtra(DatafileService.EXTRA_PROJECT_ID, projectId); - startService(context, intent); + ServiceScheduler.startService(context, DatafileService.JOB_ID, intent); logger.info("Rescheduled data file watching for project {}", projectId); } } - private void startService(Context context, Intent intent) { - if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - JobInfo jobInfo = new JobInfo.Builder(DatafileService.JOB_ID, - new ComponentName(context, JobWorkService.class)) - // schedule it to run any time between 1 - 5 minutes - .setMinimumLatency(JobWorkService.ONE_MINUTE) - .setOverrideDeadline(5 * JobWorkService.ONE_MINUTE) - .build(); - - JobScheduler jobScheduler = (JobScheduler) context.getSystemService(Context.JOB_SCHEDULER_SERVICE); - - jobScheduler.enqueue(jobInfo, new JobWorkItem(intent)); - - } - else { - context.startService(intent); - } - - } } - - } diff --git a/datafile-handler/src/main/java/com/optimizely/ab/android/datafile_handler/DatafileService.java b/datafile-handler/src/main/java/com/optimizely/ab/android/datafile_handler/DatafileService.java index 665c86014..04ce0b734 100644 --- a/datafile-handler/src/main/java/com/optimizely/ab/android/datafile_handler/DatafileService.java +++ b/datafile-handler/src/main/java/com/optimizely/ab/android/datafile_handler/DatafileService.java @@ -29,7 +29,7 @@ import com.optimizely.ab.android.shared.Cache; import com.optimizely.ab.android.shared.Client; import com.optimizely.ab.android.shared.OptlyStorage; -import com.optimizely.ab.android.shared.ServiceWorkScheduled; +import com.optimizely.ab.android.shared.JobWorkScheduledService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -41,7 +41,7 @@ * These services will only be used if you are using our {@link DefaultDatafileHandler}. * You can chose to implement your own handler and use all or part of this package. */ -public class DatafileService extends Service implements ServiceWorkScheduled { +public class DatafileService extends Service implements JobWorkScheduledService { /** * Extra containing the project id this instance of Optimizely was built with */ diff --git a/event-handler/src/main/java/com/optimizely/ab/android/event_handler/EventIntentService.java b/event-handler/src/main/java/com/optimizely/ab/android/event_handler/EventIntentService.java index 5359ff006..2a1d87ecb 100644 --- a/event-handler/src/main/java/com/optimizely/ab/android/event_handler/EventIntentService.java +++ b/event-handler/src/main/java/com/optimizely/ab/android/event_handler/EventIntentService.java @@ -16,7 +16,6 @@ package com.optimizely.ab.android.event_handler; -import android.app.AlarmManager; import android.app.IntentService; import android.content.Context; import android.content.Intent; @@ -28,7 +27,7 @@ import com.optimizely.ab.android.shared.Client; import com.optimizely.ab.android.shared.OptlyStorage; import com.optimizely.ab.android.shared.ServiceScheduler; -import com.optimizely.ab.android.shared.ServiceWorkScheduled; +import com.optimizely.ab.android.shared.JobWorkScheduledService; import org.slf4j.Logger; import org.slf4j.LoggerFactory; @@ -42,7 +41,7 @@ * worker queue. * */ -public class EventIntentService extends IntentService implements ServiceWorkScheduled { +public class EventIntentService extends IntentService implements JobWorkScheduledService { static final String EXTRA_URL = "com.optimizely.ab.android.EXTRA_URL"; static final String EXTRA_REQUEST_BODY = "com.optimizely.ab.android.EXTRA_REQUEST_BODY"; static final String EXTRA_INTERVAL = "com.optimizely.ab.android.EXTRA_INTERVAL"; diff --git a/event-handler/src/main/java/com/optimizely/ab/android/event_handler/EventRescheduler.java b/event-handler/src/main/java/com/optimizely/ab/android/event_handler/EventRescheduler.java index a20a001de..fc051c207 100644 --- a/event-handler/src/main/java/com/optimizely/ab/android/event_handler/EventRescheduler.java +++ b/event-handler/src/main/java/com/optimizely/ab/android/event_handler/EventRescheduler.java @@ -90,7 +90,7 @@ public void onReceive(Context context, Intent intent) { void reschedule(@NonNull Context context, @NonNull Intent broadcastIntent, @NonNull Intent eventServiceIntent, @NonNull ServiceScheduler serviceScheduler) { if (broadcastIntent.getAction().equals(Intent.ACTION_BOOT_COMPLETED) || broadcastIntent.getAction().equals(Intent.ACTION_MY_PACKAGE_REPLACED)) { - startService(context, eventServiceIntent); + ServiceScheduler.startService(context, EventIntentService.JOB_ID, eventServiceIntent); logger.info("Rescheduling event flushing if necessary"); } else if (broadcastIntent.getAction().equals(WifiManager.SUPPLICANT_CONNECTION_CHANGE_ACTION) && broadcastIntent.getBooleanExtra(WifiManager.EXTRA_SUPPLICANT_CONNECTED, false)) { @@ -101,7 +101,7 @@ void reschedule(@NonNull Context context, @NonNull Intent broadcastIntent, @NonN // with wifi the service will be rescheduled on the interval. // Wifi connection state changes all the time and starting services is expensive // so it's important to only do this if we have stored events. - startService(context, eventServiceIntent); + ServiceScheduler.startService(context, EventIntentService.JOB_ID, eventServiceIntent); logger.info("Preemptively flushing events since wifi became available"); } } else { @@ -109,22 +109,4 @@ void reschedule(@NonNull Context context, @NonNull Intent broadcastIntent, @NonN } } - private void startService(Context context, Intent intent) { - if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - JobInfo jobInfo = new JobInfo.Builder(EventIntentService.JOB_ID, - new ComponentName(context, JobWorkService.class)) - // schedule it to run any time between 1 - 5 minutes - .setMinimumLatency(JobWorkService.ONE_MINUTE) - .setOverrideDeadline(5 * JobWorkService.ONE_MINUTE) - .build(); - JobScheduler jobScheduler = (JobScheduler) context.getSystemService(Context.JOB_SCHEDULER_SERVICE); - - jobScheduler.enqueue(jobInfo, new JobWorkItem(intent)); - - } - else { - context.startService(intent); - } - - } } diff --git a/shared/src/main/java/com/optimizely/ab/android/shared/ServiceWorkScheduled.java b/shared/src/main/java/com/optimizely/ab/android/shared/JobWorkScheduledService.java similarity index 97% rename from shared/src/main/java/com/optimizely/ab/android/shared/ServiceWorkScheduled.java rename to shared/src/main/java/com/optimizely/ab/android/shared/JobWorkScheduledService.java index 5ce58679c..74ad46531 100644 --- a/shared/src/main/java/com/optimizely/ab/android/shared/ServiceWorkScheduled.java +++ b/shared/src/main/java/com/optimizely/ab/android/shared/JobWorkScheduledService.java @@ -26,7 +26,7 @@ * within the Service onHandleIntent or onStartCommand. If we are Android O or later, the job service will hold a handle to this interface and call * it if and when the job service is called. */ -public interface ServiceWorkScheduled { +public interface JobWorkScheduledService { void initialize(@NonNull Context context); void onWork(@NonNull Context context, @Nullable Intent intent); } diff --git a/shared/src/main/java/com/optimizely/ab/android/shared/JobWorkService.java b/shared/src/main/java/com/optimizely/ab/android/shared/JobWorkService.java index 5a36700e2..a473e6c71 100644 --- a/shared/src/main/java/com/optimizely/ab/android/shared/JobWorkService.java +++ b/shared/src/main/java/com/optimizely/ab/android/shared/JobWorkService.java @@ -1,12 +1,25 @@ +/**************************************************************************** + * Copyright 2017, Optimizely, Inc. and contributors * + * * + * Licensed under the Apache License, Version 2.0 (the "License"); * + * you may not use this file except in compliance with the License. * + * You may obtain a copy of the License at * + * * + * http://www.apache.org/licenses/LICENSE-2.0 * + * * + * Unless required by applicable law or agreed to in writing, software * + * distributed under the License is distributed on an "AS IS" BASIS, * + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * + * See the License for the specific language governing permissions and * + * limitations under the License. * + ***************************************************************************/ + package com.optimizely.ab.android.shared; -import android.app.Notification; import android.app.NotificationManager; -import android.app.PendingIntent; import android.app.job.JobParameters; import android.app.job.JobService; import android.app.job.JobWorkItem; -import android.content.Intent; import android.os.AsyncTask; import android.os.Build; import android.support.annotation.RequiresApi; @@ -46,8 +59,8 @@ protected Void doInBackground(Void... params) { try { clazz = Class.forName(componentClass); Object intentService = clazz.newInstance(); - if (intentService instanceof ServiceWorkScheduled) { - ServiceWorkScheduled serviceWorkScheduled = (ServiceWorkScheduled) intentService; + if (intentService instanceof JobWorkScheduledService) { + JobWorkScheduledService serviceWorkScheduled = (JobWorkScheduledService) intentService; serviceWorkScheduled.initialize(getApplicationContext()); serviceWorkScheduled.onWork(getApplicationContext(), work.getIntent()); } diff --git a/shared/src/main/java/com/optimizely/ab/android/shared/ServiceScheduler.java b/shared/src/main/java/com/optimizely/ab/android/shared/ServiceScheduler.java index 815cf5ce1..f2da455f5 100644 --- a/shared/src/main/java/com/optimizely/ab/android/shared/ServiceScheduler.java +++ b/shared/src/main/java/com/optimizely/ab/android/shared/ServiceScheduler.java @@ -26,6 +26,7 @@ import android.content.Intent; import android.os.Build; import android.support.annotation.NonNull; +import android.support.annotation.RequiresApi; import android.util.Log; import org.slf4j.Logger; @@ -113,6 +114,7 @@ private void cancelRepeating(PendingIntent pendingIntent, Intent intent) { try { id = (Integer) Class.forName(clazz).getDeclaredField("JOB_ID").get(null); jobScheduler.cancel(id); + pendingIntent.cancel(); } catch (IllegalAccessException e) { e.printStackTrace(); } catch (NoSuchFieldException e) { @@ -201,4 +203,41 @@ private PendingIntent getPendingIntent(Intent intent, int flag) { return PendingIntent.getService(context, 0, intent, flag); } } + + public static void startService(Context context, Integer jobId, Intent intent) { + if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + if (ServiceScheduler.getScheduled(context, jobId) != null) { + return; + } + JobInfo jobInfo = new JobInfo.Builder(jobId, + new ComponentName(context, JobWorkService.class)) + // schedule it to run any time between 1 - 5 minutes + .setMinimumLatency(JobWorkService.ONE_MINUTE) + .setOverrideDeadline(5 * JobWorkService.ONE_MINUTE) + .build(); + JobScheduler jobScheduler = (JobScheduler) context.getSystemService(Context.JOB_SCHEDULER_SERVICE); + + jobScheduler.enqueue(jobInfo, new JobWorkItem(intent)); + + } + else { + context.startService(intent); + } + + } + + @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) + public static JobInfo getScheduled(Context context, Integer jobId) { + JobScheduler jobScheduler = (JobScheduler) context.getSystemService(Context.JOB_SCHEDULER_SERVICE); + for (JobInfo jobInfo : jobScheduler.getAllPendingJobs()) { + if (jobInfo.getId() == jobId) { + return jobInfo; + } + } + + return null; + } + + + } From 52539bf60f19fe9e4cca1379268650d9e28cd4a0 Mon Sep 17 00:00:00 2001 From: Thomas Zurkan Date: Mon, 18 Sep 2017 16:40:44 -0700 Subject: [PATCH 04/12] update for android o. set the context via ContextWrapper and cleanup tests to run on android o as well as other platforms. --- .../datafile_handler/DatafileLoaderTest.java | 8 +-- .../datafile_handler/DatafileServiceTest.java | 2 +- .../datafile_handler/DatafileLoader.java | 5 +- .../datafile_handler/DatafileService.java | 12 ++-- .../DatafileServiceConnection.java | 2 +- .../event_handler/ServiceSchedulerTest.java | 57 ++++++++++++++++-- .../event_handler/DefaultEventHandler.java | 4 +- .../event_handler/EventIntentService.java | 18 +++--- proguard-rules.txt | 1 + .../android/shared/ServiceSchedulerTest.java | 35 +++++------ .../shared/JobWorkScheduledService.java | 4 +- .../ab/android/shared/JobWorkService.java | 59 +++++++++++++++++-- .../ab/android/shared/ServiceScheduler.java | 27 ++++++++- .../test_app/MainActivityEspressoTest.java | 2 +- 14 files changed, 176 insertions(+), 60 deletions(-) diff --git a/datafile-handler/src/androidTest/java/com/optimizely/ab/android/datafile_handler/DatafileLoaderTest.java b/datafile-handler/src/androidTest/java/com/optimizely/ab/android/datafile_handler/DatafileLoaderTest.java index 5b110cea2..36e3ece59 100644 --- a/datafile-handler/src/androidTest/java/com/optimizely/ab/android/datafile_handler/DatafileLoaderTest.java +++ b/datafile-handler/src/androidTest/java/com/optimizely/ab/android/datafile_handler/DatafileLoaderTest.java @@ -87,7 +87,7 @@ public void tearDown() { public void loadFromCDNWhenNoCachedFile() throws MalformedURLException, JSONException { final ListeningExecutorService executor = MoreExecutors.newDirectExecutorService(); DatafileLoader datafileLoader = - new DatafileLoader(context, datafileService, datafileClient, datafileCache, executor, logger); + new DatafileLoader(datafileService, datafileClient, datafileCache, executor, logger); when(client.execute(any(Client.Request.class), anyInt(), anyInt())).thenReturn("{}"); @@ -108,7 +108,7 @@ public void loadFromCDNWhenNoCachedFile() throws MalformedURLException, JSONExce public void loadWhenCacheFileExistsAndCDNNotModified() { final ListeningExecutorService executor = MoreExecutors.newDirectExecutorService(); DatafileLoader datafileLoader = - new DatafileLoader(context, datafileService, datafileClient, datafileCache, executor, logger); + new DatafileLoader(datafileService, datafileClient, datafileCache, executor, logger); datafileCache.save("{}"); when(client.execute(any(Client.Request.class), anyInt(), anyInt())).thenReturn(""); @@ -130,7 +130,7 @@ public void loadWhenCacheFileExistsAndCDNNotModified() { public void noCacheAndLoadFromCDNFails() { final ListeningExecutorService executor = MoreExecutors.newDirectExecutorService(); DatafileLoader datafileLoader = - new DatafileLoader(context, datafileService, datafileClient, datafileCache, executor, logger); + new DatafileLoader(datafileService, datafileClient, datafileCache, executor, logger); when(client.execute(any(Client.Request.class), anyInt(), anyInt())).thenReturn(null); @@ -152,7 +152,7 @@ public void warningsAreLogged() throws IOException { Cache cache = mock(Cache.class); datafileCache = new DatafileCache("1", cache, logger); DatafileLoader datafileLoader = - new DatafileLoader(context, datafileService, datafileClient, datafileCache, executor, logger); + new DatafileLoader(datafileService, datafileClient, datafileCache, executor, logger); when(client.execute(any(Client.Request.class), anyInt(), anyInt())).thenReturn("{}"); when(cache.exists(datafileCache.getFileName())).thenReturn(true); diff --git a/datafile-handler/src/androidTest/java/com/optimizely/ab/android/datafile_handler/DatafileServiceTest.java b/datafile-handler/src/androidTest/java/com/optimizely/ab/android/datafile_handler/DatafileServiceTest.java index bbeadc4a1..612a09d22 100644 --- a/datafile-handler/src/androidTest/java/com/optimizely/ab/android/datafile_handler/DatafileServiceTest.java +++ b/datafile-handler/src/androidTest/java/com/optimizely/ab/android/datafile_handler/DatafileServiceTest.java @@ -90,7 +90,7 @@ public void testBinding() throws TimeoutException { DatafileService datafileService = ((DatafileService.LocalBinder) binder).getService(); - DatafileLoader datafileLoader = new DatafileLoader(context, datafileService, datafileClient, datafileCache, MoreExecutors.newDirectExecutorService(), mock(Logger.class)); + DatafileLoader datafileLoader = new DatafileLoader(datafileService, datafileClient, datafileCache, MoreExecutors.newDirectExecutorService(), mock(Logger.class)); datafileService.getDatafile("1", datafileLoader, datafileLoadedListener); assertTrue(datafileService.isBound()); diff --git a/datafile-handler/src/main/java/com/optimizely/ab/android/datafile_handler/DatafileLoader.java b/datafile-handler/src/main/java/com/optimizely/ab/android/datafile_handler/DatafileLoader.java index 03af665ef..1dc628049 100644 --- a/datafile-handler/src/main/java/com/optimizely/ab/android/datafile_handler/DatafileLoader.java +++ b/datafile-handler/src/main/java/com/optimizely/ab/android/datafile_handler/DatafileLoader.java @@ -41,8 +41,7 @@ public class DatafileLoader { private boolean hasNotifiedListener = false; - public DatafileLoader(@NonNull Context context, - @NonNull DatafileService datafileService, + public DatafileLoader(@NonNull DatafileService datafileService, @NonNull DatafileClient datafileClient, @NonNull DatafileCache datafileCache, @NonNull Executor executor, @@ -53,7 +52,7 @@ public DatafileLoader(@NonNull Context context, this.datafileCache = datafileCache; this.executor = executor; - new DatafileServiceConnection("projectId", context.getApplicationContext(), new DatafileLoadedListener() { + new DatafileServiceConnection("projectId", datafileService.getApplicationContext(), new DatafileLoadedListener() { public void onDatafileLoaded(@Nullable String dataFile) {} public void onStop(Context context) {} }); diff --git a/datafile-handler/src/main/java/com/optimizely/ab/android/datafile_handler/DatafileService.java b/datafile-handler/src/main/java/com/optimizely/ab/android/datafile_handler/DatafileService.java index 04ce0b734..090468f1a 100644 --- a/datafile-handler/src/main/java/com/optimizely/ab/android/datafile_handler/DatafileService.java +++ b/datafile-handler/src/main/java/com/optimizely/ab/android/datafile_handler/DatafileService.java @@ -61,7 +61,7 @@ public class DatafileService extends Service implements JobWorkScheduledService @RequiresApi(api = Build.VERSION_CODES.HONEYCOMB) @Override public int onStartCommand(Intent intent, int flags, int startId) { - onWork(this, intent); + onWork(intent); return super.onStartCommand(intent, flags, startId); } @@ -105,26 +105,26 @@ public void getDatafile(String projectId, DatafileLoader datafileLoader, Datafil } @Override - public void initialize(@NonNull Context context) { + public void initialize() { } @RequiresApi(api = Build.VERSION_CODES.HONEYCOMB) @Override - public void onWork(@NonNull Context context, @Nullable Intent intent) { + public void onWork(@Nullable Intent intent) { if (intent != null) { if (intent.hasExtra(EXTRA_PROJECT_ID)) { String projectId = intent.getStringExtra(EXTRA_PROJECT_ID); DatafileClient datafileClient = new DatafileClient( - new Client(new OptlyStorage(context.getApplicationContext()), LoggerFactory.getLogger(OptlyStorage.class)), + new Client(new OptlyStorage(this.getApplicationContext()), LoggerFactory.getLogger(OptlyStorage.class)), LoggerFactory.getLogger(DatafileClient.class)); DatafileCache datafileCache = new DatafileCache( projectId, - new Cache(context.getApplicationContext(), LoggerFactory.getLogger(Cache.class)), + new Cache(this.getApplicationContext(), LoggerFactory.getLogger(Cache.class)), LoggerFactory.getLogger(DatafileCache.class)); String datafileUrl = getDatafileUrl(projectId); - DatafileLoader datafileLoader = new DatafileLoader(context, this, datafileClient, datafileCache, Executors.newSingleThreadExecutor(), LoggerFactory.getLogger(DatafileLoader.class)); + DatafileLoader datafileLoader = new DatafileLoader(this, datafileClient, datafileCache, Executors.newSingleThreadExecutor(), LoggerFactory.getLogger(DatafileLoader.class)); datafileLoader.getDatafile(datafileUrl, null); } else { logger.warn("Data file service received an intent with no project id extra"); diff --git a/datafile-handler/src/main/java/com/optimizely/ab/android/datafile_handler/DatafileServiceConnection.java b/datafile-handler/src/main/java/com/optimizely/ab/android/datafile_handler/DatafileServiceConnection.java index 13163a67e..f6075e871 100644 --- a/datafile-handler/src/main/java/com/optimizely/ab/android/datafile_handler/DatafileServiceConnection.java +++ b/datafile-handler/src/main/java/com/optimizely/ab/android/datafile_handler/DatafileServiceConnection.java @@ -83,7 +83,7 @@ public void onServiceConnected(ComponentName className, new Cache(context.getApplicationContext(), LoggerFactory.getLogger(Cache.class)), LoggerFactory.getLogger(DatafileCache.class)); - DatafileLoader datafileLoader = new DatafileLoader(context, datafileService, + DatafileLoader datafileLoader = new DatafileLoader(datafileService, datafileClient, datafileCache, Executors.newSingleThreadExecutor(), diff --git a/event-handler/src/androidTest/java/com/optimizely/ab/android/event_handler/ServiceSchedulerTest.java b/event-handler/src/androidTest/java/com/optimizely/ab/android/event_handler/ServiceSchedulerTest.java index 0f26d01ab..000edfdba 100644 --- a/event-handler/src/androidTest/java/com/optimizely/ab/android/event_handler/ServiceSchedulerTest.java +++ b/event-handler/src/androidTest/java/com/optimizely/ab/android/event_handler/ServiceSchedulerTest.java @@ -18,17 +18,25 @@ import android.app.AlarmManager; import android.app.PendingIntent; +import android.app.job.JobInfo; +import android.app.job.JobScheduler; +import android.app.job.JobWorkItem; +import android.content.ComponentName; import android.content.Context; import android.content.Intent; +import android.os.Build; +import android.support.annotation.RequiresApi; import android.support.test.InstrumentationRegistry; import android.support.test.runner.AndroidJUnit4; +import com.optimizely.ab.android.shared.JobWorkService; import com.optimizely.ab.android.shared.OptlyStorage; import com.optimizely.ab.android.shared.ServiceScheduler; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; import org.slf4j.Logger; import static junit.framework.Assert.assertEquals; @@ -36,6 +44,7 @@ import static junit.framework.Assert.assertNull; import static junit.framework.Assert.assertTrue; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; @@ -48,18 +57,27 @@ public class ServiceSchedulerTest { private OptlyStorage optlyStorage; private ServiceScheduler.PendingIntentFactory pendingIntentFactory; private AlarmManager alarmManager; + private JobScheduler jobScheduler; private Logger logger; private ServiceScheduler serviceScheduler; private Context context; + @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) @Before public void setup() { - context = InstrumentationRegistry.getTargetContext(); + context = mock(Context.class); optlyStorage = mock(OptlyStorage.class); alarmManager = mock(AlarmManager.class); + jobScheduler = mock(JobScheduler.class); + + when(context.getApplicationContext()).thenReturn(context); + + when(context.getSystemService(Context.ALARM_SERVICE)).thenReturn(alarmManager); + when(context.getSystemService(Context.JOB_SCHEDULER_SERVICE)).thenReturn(jobScheduler); + pendingIntentFactory = mock(ServiceScheduler.PendingIntentFactory.class); logger = mock(Logger.class); - serviceScheduler = new ServiceScheduler(alarmManager, pendingIntentFactory, logger); + serviceScheduler = new ServiceScheduler(context, pendingIntentFactory, logger); } @Test @@ -72,7 +90,18 @@ public void testScheduleWithNoDurationExtra() { serviceScheduler.schedule(intent, AlarmManager.INTERVAL_HOUR); - verify(alarmManager).setInexactRepeating(AlarmManager.ELAPSED_REALTIME, AlarmManager.INTERVAL_HOUR, AlarmManager.INTERVAL_HOUR, pendingIntent); + if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + ArgumentCaptor jobInfoArgumentCaptor = ArgumentCaptor.forClass(JobInfo.class); + ArgumentCaptor workItemArgumentCaptor = ArgumentCaptor.forClass(JobWorkItem.class); + + verify(jobScheduler).enqueue(jobInfoArgumentCaptor.capture(), workItemArgumentCaptor.capture()); + + assertEquals(jobInfoArgumentCaptor.getValue().getIntervalMillis(), AlarmManager.INTERVAL_HOUR ); + + } + else { + verify(alarmManager).setInexactRepeating(AlarmManager.ELAPSED_REALTIME, AlarmManager.INTERVAL_HOUR, AlarmManager.INTERVAL_HOUR, pendingIntent); + } verify(logger).info("Scheduled {}", intent.getComponent().toShortString()); pendingIntent.cancel(); } @@ -95,7 +124,18 @@ public void testScheduleWithDurationExtra() { intent.putExtra(EventIntentService.EXTRA_INTERVAL, duration); serviceScheduler.schedule(intent, duration); - verify(alarmManager).setInexactRepeating(AlarmManager.ELAPSED_REALTIME, duration, duration, pendingIntent); + if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + ArgumentCaptor jobInfoArgumentCaptor = ArgumentCaptor.forClass(JobInfo.class); + ArgumentCaptor workItemArgumentCaptor = ArgumentCaptor.forClass(JobWorkItem.class); + + verify(jobScheduler).enqueue(jobInfoArgumentCaptor.capture(), workItemArgumentCaptor.capture()); + + assertEquals(jobInfoArgumentCaptor.getValue().getIntervalMillis(), duration ); + } + else { + verify(alarmManager).setInexactRepeating(AlarmManager.ELAPSED_REALTIME, duration, duration, pendingIntent); + } + verify(logger).info("Scheduled {}", intent.getComponent().toShortString()); pendingIntent.cancel(); } @@ -104,7 +144,7 @@ public void testScheduleWithDurationExtra() { public void testAlreadyScheduledAlarm() { final Intent intent = new Intent(context, EventIntentService.class); when(pendingIntentFactory.hasPendingIntent(intent)).thenReturn(true); - when(pendingIntentFactory.getPendingIntent(intent)).thenReturn(PendingIntent.getService(InstrumentationRegistry.getTargetContext(), 1, intent, 0)); + when(pendingIntentFactory.getPendingIntent(intent)).thenReturn(getPendingIntent()); serviceScheduler.schedule(intent, AlarmManager.INTERVAL_HOUR); @@ -153,7 +193,12 @@ public void testCancel() { final Intent intent = new Intent(context, EventIntentService.class); when(pendingIntentFactory.getPendingIntent(intent)).thenReturn(pendingIntent); serviceScheduler.unschedule(intent); - verify(alarmManager).cancel(pendingIntent); + if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + verify(jobScheduler).cancel(EventIntentService.JOB_ID); + } + else { + verify(alarmManager).cancel(pendingIntent); + } verify(logger).info("Unscheduled {}", intent.getComponent().toShortString()); } diff --git a/event-handler/src/main/java/com/optimizely/ab/android/event_handler/DefaultEventHandler.java b/event-handler/src/main/java/com/optimizely/ab/android/event_handler/DefaultEventHandler.java index e423f731e..e3fe4d938 100644 --- a/event-handler/src/main/java/com/optimizely/ab/android/event_handler/DefaultEventHandler.java +++ b/event-handler/src/main/java/com/optimizely/ab/android/event_handler/DefaultEventHandler.java @@ -20,6 +20,7 @@ import android.content.Intent; import android.support.annotation.NonNull; +import com.optimizely.ab.android.shared.ServiceScheduler; import com.optimizely.ab.event.EventHandler; import com.optimizely.ab.event.LogEvent; @@ -96,7 +97,8 @@ public void dispatchEvent(@NonNull LogEvent logEvent) { intent.putExtra(EventIntentService.EXTRA_REQUEST_BODY, logEvent.getBody()); intent.putExtra(EventIntentService.EXTRA_INTERVAL, dispatchInterval); - context.startService(intent); + ServiceScheduler.startService(context, EventIntentService.JOB_ID, intent); + logger.info("Sent url {} to the event handler service", logEvent.getEndpointUrl()); } } diff --git a/event-handler/src/main/java/com/optimizely/ab/android/event_handler/EventIntentService.java b/event-handler/src/main/java/com/optimizely/ab/android/event_handler/EventIntentService.java index 2a1d87ecb..301aec167 100644 --- a/event-handler/src/main/java/com/optimizely/ab/android/event_handler/EventIntentService.java +++ b/event-handler/src/main/java/com/optimizely/ab/android/event_handler/EventIntentService.java @@ -63,7 +63,7 @@ public EventIntentService() { public void onCreate() { super.onCreate(); - initialize(this); + initialize(); } /** @@ -73,11 +73,11 @@ public void onCreate() { @Override protected void onHandleIntent(Intent intent) { - onWork(this, intent); + onWork(intent); } @Override - public void onWork(@NonNull Context context, @Nullable Intent intent) { + public void onWork(@Nullable Intent intent) { if (intent == null) { logger.warn("Handled a null intent"); return; @@ -93,16 +93,16 @@ public void onWork(@NonNull Context context, @Nullable Intent intent) { @RequiresApi(api = Build.VERSION_CODES.HONEYCOMB) @Override - public void initialize(@NonNull Context context) { - OptlyStorage optlyStorage = new OptlyStorage(context); + public void initialize() { + OptlyStorage optlyStorage = new OptlyStorage(this); EventClient eventClient = new EventClient(new Client(optlyStorage, LoggerFactory.getLogger(Client.class)), LoggerFactory.getLogger(EventClient.class)); - EventDAO eventDAO = EventDAO.getInstance(context, "1", LoggerFactory.getLogger(EventDAO.class)); + EventDAO eventDAO = EventDAO.getInstance(this, "1", LoggerFactory.getLogger(EventDAO.class)); ServiceScheduler serviceScheduler = new ServiceScheduler( - context, - new ServiceScheduler.PendingIntentFactory(context), + this, + new ServiceScheduler.PendingIntentFactory(this), LoggerFactory.getLogger(ServiceScheduler.class)); - eventDispatcher = new EventDispatcher(context, optlyStorage, eventDAO, eventClient, serviceScheduler, LoggerFactory.getLogger(EventDispatcher.class)); + eventDispatcher = new EventDispatcher(this, optlyStorage, eventDAO, eventClient, serviceScheduler, LoggerFactory.getLogger(EventDispatcher.class)); } } diff --git a/proguard-rules.txt b/proguard-rules.txt index 1f8e8c571..0edb1ecfa 100644 --- a/proguard-rules.txt +++ b/proguard-rules.txt @@ -21,6 +21,7 @@ # Optimizely -keep class com.optimizely.ab.android.shared.JobWorkService +-keep class com.optimizely.ab.android.shared.ServiceScheduler # Gson -keepnames class com.google.gson.Gson diff --git a/shared/src/androidTest/java/com/optimizely/ab/android/shared/ServiceSchedulerTest.java b/shared/src/androidTest/java/com/optimizely/ab/android/shared/ServiceSchedulerTest.java index 5d9fecbc8..96ca94949 100644 --- a/shared/src/androidTest/java/com/optimizely/ab/android/shared/ServiceSchedulerTest.java +++ b/shared/src/androidTest/java/com/optimizely/ab/android/shared/ServiceSchedulerTest.java @@ -52,33 +52,34 @@ public class ServiceSchedulerTest { private Context context; - @Before - public void setup() { - context = getTargetContext(); - } + public static class MyIntent extends IntentService { - @Test - public void testScheduler() { + // if you want to schedule an intent for Android O or greater, the intent has to have the public + // job id or it will not be scheduled. + public static final Integer JOB_ID = 2112; - class MyIntent extends IntentService { + public MyIntent() { + super("MyItentServiceTest"); - public MyIntent() { - super("MyItentServiceTest"); - - } + } - @Override - protected void onHandleIntent(@Nullable Intent intent) { + @Override + protected void onHandleIntent(@Nullable Intent intent) { - } } + } + + @Before + public void setup() { + context = getTargetContext(); + } + @Test + public void testScheduler() { - AlarmManager alarmManager = (AlarmManager) context - .getSystemService(Context.ALARM_SERVICE); ServiceScheduler.PendingIntentFactory pendingIntentFactory = new ServiceScheduler .PendingIntentFactory(context.getApplicationContext()); - ServiceScheduler serviceScheduler = new ServiceScheduler(alarmManager, pendingIntentFactory, + ServiceScheduler serviceScheduler = new ServiceScheduler(context, pendingIntentFactory, LoggerFactory.getLogger(ServiceScheduler.class)); diff --git a/shared/src/main/java/com/optimizely/ab/android/shared/JobWorkScheduledService.java b/shared/src/main/java/com/optimizely/ab/android/shared/JobWorkScheduledService.java index 74ad46531..1a24f3789 100644 --- a/shared/src/main/java/com/optimizely/ab/android/shared/JobWorkScheduledService.java +++ b/shared/src/main/java/com/optimizely/ab/android/shared/JobWorkScheduledService.java @@ -27,6 +27,6 @@ * it if and when the job service is called. */ public interface JobWorkScheduledService { - void initialize(@NonNull Context context); - void onWork(@NonNull Context context, @Nullable Intent intent); + void initialize(); + void onWork(@Nullable Intent intent); } diff --git a/shared/src/main/java/com/optimizely/ab/android/shared/JobWorkService.java b/shared/src/main/java/com/optimizely/ab/android/shared/JobWorkService.java index a473e6c71..f51ad0689 100644 --- a/shared/src/main/java/com/optimizely/ab/android/shared/JobWorkService.java +++ b/shared/src/main/java/com/optimizely/ab/android/shared/JobWorkService.java @@ -16,15 +16,23 @@ package com.optimizely.ab.android.shared; +import android.app.IntentService; import android.app.NotificationManager; +import android.app.Service; import android.app.job.JobParameters; import android.app.job.JobService; import android.app.job.JobWorkItem; +import android.content.Context; +import android.content.ContextWrapper; +import android.content.Intent; import android.os.AsyncTask; import android.os.Build; import android.support.annotation.RequiresApi; import android.util.Log; +import java.lang.reflect.InvocationTargetException; +import java.lang.reflect.Method; + /** * This is an example of implementing a {@link JobService} that dispatches work enqueued in * to it. The class shows how to interact with the service. @@ -33,8 +41,8 @@ @RequiresApi(api = Build.VERSION_CODES.O) public class JobWorkService extends JobService { public static final int ONE_MINUTE = 60 * 1000; - private NotificationManager mNM; private CommandProcessor mCurProcessor; + private int startId = 1; /** * This is a task to dequeue and process work in the background. */ @@ -58,11 +66,22 @@ protected Void doInBackground(Void... params) { Log.i("JobWorkService", "Processing work: " + work + ", component: " + componentClass); try { clazz = Class.forName(componentClass); - Object intentService = clazz.newInstance(); - if (intentService instanceof JobWorkScheduledService) { - JobWorkScheduledService serviceWorkScheduled = (JobWorkScheduledService) intentService; - serviceWorkScheduled.initialize(getApplicationContext()); - serviceWorkScheduled.onWork(getApplicationContext(), work.getIntent()); + Object service = clazz.newInstance(); + setContext((Service) service); + + if (service instanceof JobWorkScheduledService) { + JobWorkScheduledService serviceWorkScheduled = (JobWorkScheduledService) service; + serviceWorkScheduled.initialize(); + serviceWorkScheduled.onWork(work.getIntent()); + } + else { + if (service instanceof IntentService) { + IntentService intentService = (IntentService) service; + intentService.onCreate(); + callOnHandleIntent(intentService, work.getIntent()); + } else { + callOnStartCommand((Service) service, work.getIntent()); + } } } catch (Exception e) { Log.e("JobSerivice", "Error creating ServiceWorkScheduled", e); @@ -103,5 +122,33 @@ public boolean onStopJob(JobParameters params) { // its work, so we would like it to remain to finish that work in the future. return true; } + + private void setContext(Service service) { + callMethod(ContextWrapper.class, service, "attachBaseContext", new Class[] { Context.class }, getApplicationContext()); + } + + private void callOnStartCommand(Service service, Intent intent) { + callMethod(Service.class, service, "onStartService", new Class[] { Intent.class, Integer.class, Integer.class}, intent, 0, startId++); + } + + private void callOnHandleIntent(IntentService intentService, Intent intent) { + callMethod(IntentService.class, intentService, "onHandleIntent", new Class[] { Intent.class }, intent); + } + + private void callMethod(Class clazz, Object object, String methodName, Class[] parameterTypes, Object... parameters ) { + try { + Method method = clazz.getDeclaredMethod(methodName, parameterTypes); + method.setAccessible(true); + method.invoke(object, parameters); + + } catch (NoSuchMethodException e) { + e.printStackTrace(); + } catch (InvocationTargetException e) { + e.printStackTrace(); + } catch (IllegalAccessException e) { + e.printStackTrace(); + } + + } } //END_INCLUDE(service) \ No newline at end of file diff --git a/shared/src/main/java/com/optimizely/ab/android/shared/ServiceScheduler.java b/shared/src/main/java/com/optimizely/ab/android/shared/ServiceScheduler.java index f2da455f5..5f6d99015 100644 --- a/shared/src/main/java/com/optimizely/ab/android/shared/ServiceScheduler.java +++ b/shared/src/main/java/com/optimizely/ab/android/shared/ServiceScheduler.java @@ -50,7 +50,6 @@ public class ServiceScheduler { * @hide */ public ServiceScheduler(@NonNull Context context, @NonNull PendingIntentFactory pendingIntentFactory, @NonNull Logger logger) { - //this.alarmManager = alarmManager; this.pendingIntentFactory = pendingIntentFactory; this.logger = logger; this.context = context; @@ -86,12 +85,18 @@ public void schedule(Intent intent, long interval) { private void setRepeating(long interval, PendingIntent pendingIntent, Intent intent) { if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + int jobId = getJobId(intent); + if (jobId == -1) { + logger.error("Problem getting job id"); + return; + } + JobScheduler jobScheduler = (JobScheduler) context.getSystemService(Context.JOB_SCHEDULER_SERVICE); - JobInfo.Builder builder = new JobInfo.Builder(1, + JobInfo.Builder builder = new JobInfo.Builder(jobId, new ComponentName(context.getApplicationContext(), JobWorkService.class.getName())); - builder.setPeriodic(interval); + builder.setPeriodic(interval, interval); builder.setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY); if (jobScheduler.enqueue(builder.build(), new JobWorkItem(intent)) <= 0) { @@ -130,6 +135,22 @@ private void cancelRepeating(PendingIntent pendingIntent, Intent intent) { } } + private int getJobId(Intent intent) { + String clazz = intent.getComponent().getClassName(); + Integer id = null; + try { + id = (Integer) Class.forName(clazz).getDeclaredField("JOB_ID").get(null); + } catch (IllegalAccessException e) { + e.printStackTrace(); + } catch (NoSuchFieldException e) { + e.printStackTrace(); + } catch (ClassNotFoundException e) { + e.printStackTrace(); + } + + return id == null ? -1 : id; + } + /** * Unschedule a scheduled {@link Intent} * diff --git a/test-app/src/androidTest/java/com/optimizely/ab/android/test_app/MainActivityEspressoTest.java b/test-app/src/androidTest/java/com/optimizely/ab/android/test_app/MainActivityEspressoTest.java index 0dafa6d75..0b013e8a1 100644 --- a/test-app/src/androidTest/java/com/optimizely/ab/android/test_app/MainActivityEspressoTest.java +++ b/test-app/src/androidTest/java/com/optimizely/ab/android/test_app/MainActivityEspressoTest.java @@ -114,7 +114,7 @@ protected void before() throws Throwable { Context applicationContext = context.getApplicationContext(); ServiceScheduler.PendingIntentFactory pendingIntentFactory = new ServiceScheduler.PendingIntentFactory(applicationContext); AlarmManager alarmManager = (AlarmManager) applicationContext.getSystemService(Context.ALARM_SERVICE); - serviceScheduler = new ServiceScheduler(alarmManager, pendingIntentFactory, LoggerFactory.getLogger(ServiceScheduler.class)); + serviceScheduler = new ServiceScheduler(applicationContext, pendingIntentFactory, LoggerFactory.getLogger(ServiceScheduler.class)); } @Override From d9fa4154d702df56900f293df9e38f5e19bcd2d9 Mon Sep 17 00:00:00 2001 From: Thomas Zurkan Date: Tue, 19 Sep 2017 12:44:41 -0700 Subject: [PATCH 05/12] make sure to use logger and update proguard rules --- .../datafile_handler/DatafileRescheduler.java | 9 +++------ proguard-rules.txt | 4 ++-- .../ab/android/shared/JobWorkService.java | 18 +++++++++++------- .../ab/android/shared/ServiceScheduler.java | 14 +++++++------- 4 files changed, 23 insertions(+), 22 deletions(-) diff --git a/datafile-handler/src/main/java/com/optimizely/ab/android/datafile_handler/DatafileRescheduler.java b/datafile-handler/src/main/java/com/optimizely/ab/android/datafile_handler/DatafileRescheduler.java index 174d7d32d..03dbfe17a 100644 --- a/datafile-handler/src/main/java/com/optimizely/ab/android/datafile_handler/DatafileRescheduler.java +++ b/datafile-handler/src/main/java/com/optimizely/ab/android/datafile_handler/DatafileRescheduler.java @@ -84,12 +84,9 @@ public void onReceive(Context context, Intent intent) { */ static class Dispatcher { - @NonNull - private final Context context; - @NonNull - private final BackgroundWatchersCache backgroundWatchersCache; - @NonNull - private final Logger logger; + @NonNull private final Context context; + @NonNull private final BackgroundWatchersCache backgroundWatchersCache; + @NonNull private final Logger logger; Dispatcher(@NonNull Context context, @NonNull BackgroundWatchersCache backgroundWatchersCache, @NonNull Logger logger) { this.context = context; diff --git a/proguard-rules.txt b/proguard-rules.txt index 0edb1ecfa..a13f8bf57 100644 --- a/proguard-rules.txt +++ b/proguard-rules.txt @@ -20,8 +20,8 @@ ## https://developers.optimizely.com/x/solutions/sdks/reference/index.html?language=android&platform=mobile#installation # Optimizely --keep class com.optimizely.ab.android.shared.JobWorkService --keep class com.optimizely.ab.android.shared.ServiceScheduler +-keep class com.optimizely.ab.android.datafile_handler.DatafileService { java.lang.Integer JOB_ID; } +-keep class com.optimizely.ab.android.event_handler.EventIntentService { java.lang.Integer JOB_ID; } # Gson -keepnames class com.google.gson.Gson diff --git a/shared/src/main/java/com/optimizely/ab/android/shared/JobWorkService.java b/shared/src/main/java/com/optimizely/ab/android/shared/JobWorkService.java index f51ad0689..81f3a05ef 100644 --- a/shared/src/main/java/com/optimizely/ab/android/shared/JobWorkService.java +++ b/shared/src/main/java/com/optimizely/ab/android/shared/JobWorkService.java @@ -30,6 +30,9 @@ import android.support.annotation.RequiresApi; import android.util.Log; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; @@ -43,6 +46,7 @@ public class JobWorkService extends JobService { public static final int ONE_MINUTE = 60 * 1000; private CommandProcessor mCurProcessor; private int startId = 1; + Logger logger = LoggerFactory.getLogger("JobWorkService"); /** * This is a task to dequeue and process work in the background. */ @@ -63,7 +67,7 @@ protected Void doInBackground(Void... params) { while (!(cancelled=isCancelled()) && (work=mParams.dequeueWork()) != null) { String componentClass = work.getIntent().getComponent().getClassName(); Class clazz = null; - Log.i("JobWorkService", "Processing work: " + work + ", component: " + componentClass); + logger.info("JobWorkService", "Processing work: " + work + ", component: " + componentClass); try { clazz = Class.forName(componentClass); Object service = clazz.newInstance(); @@ -84,13 +88,13 @@ protected Void doInBackground(Void... params) { } } } catch (Exception e) { - Log.e("JobSerivice", "Error creating ServiceWorkScheduled", e); + logger.error("JobSerivice", "Error creating ServiceWorkScheduled", e); } // Tell system we have finished processing the work. - Log.i("JobWorkService", "Done with: " + work); + logger.error("JobWorkService", "Done with: " + work); } if (cancelled) { - Log.i("JobWorkService", "CANCELLED!"); + logger.error("JobWorkService", "CANCELLED!"); } return null; } @@ -142,11 +146,11 @@ private void callMethod(Class clazz, Object object, String methodName, Class[] p method.invoke(object, parameters); } catch (NoSuchMethodException e) { - e.printStackTrace(); + logger.error("Error calling method " + methodName, e); } catch (InvocationTargetException e) { - e.printStackTrace(); + logger.error("Error calling method " + methodName, e); } catch (IllegalAccessException e) { - e.printStackTrace(); + logger.error("Error calling method " + methodName, e); } } diff --git a/shared/src/main/java/com/optimizely/ab/android/shared/ServiceScheduler.java b/shared/src/main/java/com/optimizely/ab/android/shared/ServiceScheduler.java index 5f6d99015..294c70c1d 100644 --- a/shared/src/main/java/com/optimizely/ab/android/shared/ServiceScheduler.java +++ b/shared/src/main/java/com/optimizely/ab/android/shared/ServiceScheduler.java @@ -100,7 +100,7 @@ private void setRepeating(long interval, PendingIntent pendingIntent, Intent int builder.setRequiredNetworkType(JobInfo.NETWORK_TYPE_ANY); if (jobScheduler.enqueue(builder.build(), new JobWorkItem(intent)) <= 0) { - Log.e("ServiceScheduler", "Some error while scheduling the job"); + logger.error("ServiceScheduler", "Some error while scheduling the job"); } } @@ -121,11 +121,11 @@ private void cancelRepeating(PendingIntent pendingIntent, Intent intent) { jobScheduler.cancel(id); pendingIntent.cancel(); } catch (IllegalAccessException e) { - e.printStackTrace(); + logger.error("Error in Cancel ", e); } catch (NoSuchFieldException e) { - e.printStackTrace(); + logger.error("Error in Cancel ", e); } catch (ClassNotFoundException e) { - e.printStackTrace(); + logger.error("Error in Cancel ", e); } } else { @@ -141,11 +141,11 @@ private int getJobId(Intent intent) { try { id = (Integer) Class.forName(clazz).getDeclaredField("JOB_ID").get(null); } catch (IllegalAccessException e) { - e.printStackTrace(); + logger.error("Error getting JOB_ID from " + clazz, e); } catch (NoSuchFieldException e) { - e.printStackTrace(); + logger.error("Error getting JOB_ID from " + clazz, e); } catch (ClassNotFoundException e) { - e.printStackTrace(); + logger.error("Error getting JOB_ID from " + clazz, e); } return id == null ? -1 : id; From 706828d54ee42a7d4dd127b534c9f97f4af11d2c Mon Sep 17 00:00:00 2001 From: Thomas Zurkan Date: Tue, 19 Sep 2017 15:23:06 -0700 Subject: [PATCH 06/12] finally got all the proguard rules right --- proguard-rules.txt | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/proguard-rules.txt b/proguard-rules.txt index a13f8bf57..c99eaad35 100644 --- a/proguard-rules.txt +++ b/proguard-rules.txt @@ -20,8 +20,18 @@ ## https://developers.optimizely.com/x/solutions/sdks/reference/index.html?language=android&platform=mobile#installation # Optimizely --keep class com.optimizely.ab.android.datafile_handler.DatafileService { java.lang.Integer JOB_ID; } --keep class com.optimizely.ab.android.event_handler.EventIntentService { java.lang.Integer JOB_ID; } +-keep class com.optimizely.ab.android.datafile_handler.DatafileService +-keepclassmembers class com.optimizely.ab.android.datafile_handler.DatafileService { + public *; +} +-keep class com.optimizely.ab.android.event_handler.EventIntentService +-keepclassmembers class com.optimizely.ab.android.event_handler.EventIntentService { + public *; +} +-keep class com.optimizely.ab.config.** +-keepclassmembers class com.optimizely.ab.config.** { + *; +} # Gson -keepnames class com.google.gson.Gson From 3c9ac0a6eb51d241a86017d18f57442b97d47a2d Mon Sep 17 00:00:00 2001 From: Thomas Zurkan Date: Tue, 19 Sep 2017 19:35:26 -0700 Subject: [PATCH 07/12] update travis build tools and sdk environment --- .travis.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 695202722..037d2e368 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,8 +3,8 @@ android: components: - tools - platform-tools - - build-tools-25.0.2 - - android-24 + - build-tools-26.0.1 + - android-26 - doc-24 - extra-android-m2repository - sys-img-armeabi-v7a-android-24 From ae62a3d6880528245c5fc0748ade9f4cbadc747a Mon Sep 17 00:00:00 2001 From: Thomas Zurkan Date: Tue, 19 Sep 2017 19:44:13 -0700 Subject: [PATCH 08/12] update travis --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index 037d2e368..acc2b8237 100644 --- a/.travis.yml +++ b/.travis.yml @@ -19,6 +19,7 @@ cache: before_script: - echo $TRAVIS_BRANCH - echo $TRAVIS_TAG + - android list targets - echo no | android create avd --force -n test -t android-24 --abi default/armeabi-v7a - emulator -avd test -no-audio -no-window & - android-wait-for-emulator From 7ef920c6dd48113cee1bb448f2af7c92ebe34e15 Mon Sep 17 00:00:00 2001 From: Thomas Zurkan Date: Tue, 19 Sep 2017 20:01:03 -0700 Subject: [PATCH 09/12] travis and android26 --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index acc2b8237..7fd1335df 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,7 +3,7 @@ android: components: - tools - platform-tools - - build-tools-26.0.1 + - build-tools-26.0.0 - android-26 - doc-24 - extra-android-m2repository From 1ef42954e46a1681c2a375ecc9be18bbf0a1734d Mon Sep 17 00:00:00 2001 From: Thomas Zurkan Date: Tue, 19 Sep 2017 20:31:32 -0700 Subject: [PATCH 10/12] trying to get travis to build --- .travis.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 7fd1335df..6f0cdd59b 100644 --- a/.travis.yml +++ b/.travis.yml @@ -3,11 +3,12 @@ android: components: - tools - platform-tools + - tools - build-tools-26.0.0 - android-26 - doc-24 - extra-android-m2repository - - sys-img-armeabi-v7a-android-24 + - sys-img-armeabi-v7a-android-22 jdk: - oraclejdk8 before_cache: @@ -20,7 +21,7 @@ before_script: - echo $TRAVIS_BRANCH - echo $TRAVIS_TAG - android list targets - - echo no | android create avd --force -n test -t android-24 --abi default/armeabi-v7a + - echo no | android create avd --force -n test -t android-22 --abi armeabi-v7a - emulator -avd test -no-audio -no-window & - android-wait-for-emulator - adb shell input keyevent 82 & From 7361101b4013673a73fb01f9443cc272cc28f304 Mon Sep 17 00:00:00 2001 From: Thomas Zurkan Date: Wed, 20 Sep 2017 10:24:51 -0700 Subject: [PATCH 11/12] trying to get travis to work with android 26 --- .travis.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 6f0cdd59b..159060fb0 100644 --- a/.travis.yml +++ b/.travis.yml @@ -4,8 +4,6 @@ android: - tools - platform-tools - tools - - build-tools-26.0.0 - - android-26 - doc-24 - extra-android-m2repository - sys-img-armeabi-v7a-android-22 From d973c39ec593ccfe055a8e9ee2689bc81f4afa3e Mon Sep 17 00:00:00 2001 From: Thomas Zurkan Date: Mon, 25 Sep 2017 16:04:30 -0700 Subject: [PATCH 12/12] updates for early versions of android. and, a cleanup of the onStartListener. --- .../ab/android/sdk/OptimizelyManagerTest.java | 2 +- .../ab/android/sdk/OptimizelyManager.java | 21 ++++++++++++------- .../event_handler/ServiceSchedulerTest.java | 2 ++ .../ab/android/shared/ServiceScheduler.java | 18 +++++++++------- 4 files changed, 26 insertions(+), 17 deletions(-) diff --git a/android-sdk/src/androidTest/java/com/optimizely/ab/android/sdk/OptimizelyManagerTest.java b/android-sdk/src/androidTest/java/com/optimizely/ab/android/sdk/OptimizelyManagerTest.java index 48fe78027..0121a0140 100644 --- a/android-sdk/src/androidTest/java/com/optimizely/ab/android/sdk/OptimizelyManagerTest.java +++ b/android-sdk/src/androidTest/java/com/optimizely/ab/android/sdk/OptimizelyManagerTest.java @@ -217,8 +217,8 @@ public void injectOptimizely() { verify(startListener).onStart(any(OptimizelyClient.class)); } - @RequiresApi(api = Build.VERSION_CODES.HONEYCOMB) @Test + @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) public void injectOptimizelyNullListener() { Context context = mock(Context.class); PackageManager packageManager = mock(PackageManager.class); diff --git a/android-sdk/src/main/java/com/optimizely/ab/android/sdk/OptimizelyManager.java b/android-sdk/src/main/java/com/optimizely/ab/android/sdk/OptimizelyManager.java index 8e514b2d3..6a6accae8 100644 --- a/android-sdk/src/main/java/com/optimizely/ab/android/sdk/OptimizelyManager.java +++ b/android-sdk/src/main/java/com/optimizely/ab/android/sdk/OptimizelyManager.java @@ -111,6 +111,14 @@ void setOptimizelyStartListener(@Nullable OptimizelyStartListener optimizelyStar this.optimizelyStartListener = optimizelyStartListener; } + private void notifyStartListener() { + if (optimizelyStartListener != null) { + optimizelyStartListener.onStart(getOptimizely()); + optimizelyStartListener = null; + } + + } + /** * Initialize Optimizely Synchronously *

@@ -232,16 +240,13 @@ public void onDatafileLoaded(@Nullable String datafile) { // We should always call the callback even with the dummy // instances. Devs might gate the rest of their app // based on the loading of Optimizely - OptimizelyStartListener optimizelyStartListener = getOptimizelyStartListener(); - if (optimizelyStartListener != null) { - optimizelyStartListener.onStart(getOptimizely()); - } + notifyStartListener(); } } @Override public void onStop(Context context) { - stop(context); + } }; } @@ -345,7 +350,7 @@ void injectOptimizely(@NonNull final Context context, final @NonNull UserProfile public void onStartComplete(UserProfileService userProfileService) { if (optimizelyStartListener != null) { logger.info("Sending Optimizely instance to listener"); - optimizelyStartListener.onStart(optimizelyClient); + notifyStartListener(); } else { logger.info("No listener to send Optimizely to"); } @@ -355,7 +360,7 @@ public void onStartComplete(UserProfileService userProfileService) { else { if (optimizelyStartListener != null) { logger.info("Sending Optimizely instance to listener"); - optimizelyStartListener.onStart(optimizelyClient); + notifyStartListener(); } else { logger.info("No listener to send Optimizely to"); } @@ -364,7 +369,7 @@ public void onStartComplete(UserProfileService userProfileService) { logger.error("Unable to build OptimizelyClient instance", e); if (optimizelyStartListener != null) { logger.info("Sending Optimizely instance to listener may be null on failure"); - optimizelyStartListener.onStart(optimizelyClient); + notifyStartListener(); } } catch (Error e) { logger.error("Unable to build OptimizelyClient instance", e); diff --git a/event-handler/src/androidTest/java/com/optimizely/ab/android/event_handler/ServiceSchedulerTest.java b/event-handler/src/androidTest/java/com/optimizely/ab/android/event_handler/ServiceSchedulerTest.java index 000edfdba..217ca96f0 100644 --- a/event-handler/src/androidTest/java/com/optimizely/ab/android/event_handler/ServiceSchedulerTest.java +++ b/event-handler/src/androidTest/java/com/optimizely/ab/android/event_handler/ServiceSchedulerTest.java @@ -27,6 +27,7 @@ import android.os.Build; import android.support.annotation.RequiresApi; import android.support.test.InstrumentationRegistry; +import android.support.test.filters.SdkSuppress; import android.support.test.runner.AndroidJUnit4; import com.optimizely.ab.android.shared.JobWorkService; @@ -52,6 +53,7 @@ * Tests for {@link ServiceScheduler} */ @RunWith(AndroidJUnit4.class) +@SdkSuppress(minSdkVersion = Build.VERSION_CODES.LOLLIPOP) public class ServiceSchedulerTest { private OptlyStorage optlyStorage; diff --git a/shared/src/main/java/com/optimizely/ab/android/shared/ServiceScheduler.java b/shared/src/main/java/com/optimizely/ab/android/shared/ServiceScheduler.java index 294c70c1d..549d83a1f 100644 --- a/shared/src/main/java/com/optimizely/ab/android/shared/ServiceScheduler.java +++ b/shared/src/main/java/com/optimizely/ab/android/shared/ServiceScheduler.java @@ -227,7 +227,8 @@ private PendingIntent getPendingIntent(Intent intent, int flag) { public static void startService(Context context, Integer jobId, Intent intent) { if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { - if (ServiceScheduler.getScheduled(context, jobId) != null) { + + if (!ServiceScheduler.isScheduled(context, jobId)) { return; } JobInfo jobInfo = new JobInfo.Builder(jobId, @@ -248,15 +249,16 @@ public static void startService(Context context, Integer jobId, Intent intent) { } @RequiresApi(api = Build.VERSION_CODES.LOLLIPOP) - public static JobInfo getScheduled(Context context, Integer jobId) { - JobScheduler jobScheduler = (JobScheduler) context.getSystemService(Context.JOB_SCHEDULER_SERVICE); - for (JobInfo jobInfo : jobScheduler.getAllPendingJobs()) { - if (jobInfo.getId() == jobId) { - return jobInfo; + private static boolean isScheduled(Context context, Integer jobId) { + if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { + JobScheduler jobScheduler = (JobScheduler) context.getSystemService(Context.JOB_SCHEDULER_SERVICE); + for (JobInfo jobInfo : jobScheduler.getAllPendingJobs()) { + if (jobInfo.getId() == jobId) { + return true; + } } } - - return null; + return false; }