From 16fb444a2d50cea73ec2e37c4eceb25233e5380a Mon Sep 17 00:00:00 2001 From: "David G. Young" Date: Mon, 24 Jul 2017 07:40:03 -0700 Subject: [PATCH] Prevent Android O from blocking intent/notification conversion - use BeaconLocalBroadcastProcessor instead of BeaconIntentProcessor when ScanJob is used --- .../beacon/BeaconIntentProcessor.java | 72 +++------------ .../beacon/BeaconLocalBroadcastProcessor.java | 92 +++++++++++++++++++ .../org/altbeacon/beacon/BeaconManager.java | 3 +- .../org/altbeacon/beacon/IntentHandler.java | 85 +++++++++++++++++ .../altbeacon/beacon/service/Callback.java | 50 +++++----- .../org/altbeacon/beacon/service/ScanJob.java | 3 + .../beacon/service/ScanJobScheduler.java | 12 +++ 7 files changed, 233 insertions(+), 84 deletions(-) create mode 100644 src/main/java/org/altbeacon/beacon/BeaconLocalBroadcastProcessor.java create mode 100644 src/main/java/org/altbeacon/beacon/IntentHandler.java diff --git a/src/main/java/org/altbeacon/beacon/BeaconIntentProcessor.java b/src/main/java/org/altbeacon/beacon/BeaconIntentProcessor.java index b2d112690..1f63462ec 100644 --- a/src/main/java/org/altbeacon/beacon/BeaconIntentProcessor.java +++ b/src/main/java/org/altbeacon/beacon/BeaconIntentProcessor.java @@ -35,7 +35,19 @@ /** * Converts internal intents to notifier callbacks - * This IntentService may be running in a different process from the BeaconService. + * + * This is used with the BeaconService and supports scanning in a separate process. + * It is not used with the ScanJob, as an IntentService will not be able to be started in some cases + * where the app is in the background on Android O. + * + * @see BeaconLocalBroadcastProcessor for the equivalent use with ScanJob. + * + * This IntentService may be running in a different process from the BeaconService, which justifies + * its continued existence for multi-process service cases. + * + * Internal library class. Do not use directly from outside the library + * + * @hide */ public class BeaconIntentProcessor extends IntentService { private static final String TAG = "BeaconIntentProcessor"; @@ -46,62 +58,6 @@ public BeaconIntentProcessor() { @Override protected void onHandleIntent(Intent intent) { - LogManager.d(TAG, "got an intent to process"); - - MonitoringData monitoringData = null; - RangingData rangingData = null; - - if (intent != null && intent.getExtras() != null) { - if (intent.getExtras().getBundle("monitoringData") != null) { - monitoringData = MonitoringData.fromBundle(intent.getExtras().getBundle("monitoringData")); - } - if (intent.getExtras().getBundle("rangingData") != null) { - rangingData = RangingData.fromBundle(intent.getExtras().getBundle("rangingData")); - } - } - - if (rangingData != null) { - LogManager.d(TAG, "got ranging data"); - if (rangingData.getBeacons() == null) { - LogManager.w(TAG, "Ranging data has a null beacons collection"); - } - Set notifiers = BeaconManager.getInstanceForApplication(this).getRangingNotifiers(); - java.util.Collection beacons = rangingData.getBeacons(); - if (notifiers != null) { - for(RangeNotifier notifier : notifiers){ - notifier.didRangeBeaconsInRegion(beacons, rangingData.getRegion()); - } - } - else { - LogManager.d(TAG, "but ranging notifier is null, so we're dropping it."); - } - RangeNotifier dataNotifier = BeaconManager.getInstanceForApplication(this).getDataRequestNotifier(); - if (dataNotifier != null) { - dataNotifier.didRangeBeaconsInRegion(beacons, rangingData.getRegion()); - } - } - - if (monitoringData != null) { - LogManager.d(TAG, "got monitoring data"); - Set notifiers = BeaconManager.getInstanceForApplication(this).getMonitoringNotifiers(); - if (notifiers != null) { - for(MonitorNotifier notifier : notifiers) { - LogManager.d(TAG, "Calling monitoring notifier: %s", notifier); - Region region = monitoringData.getRegion(); - Integer state = monitoringData.isInside() ? MonitorNotifier.INSIDE : - MonitorNotifier.OUTSIDE; - notifier.didDetermineStateForRegion(state, region); - // In case the beacon scanner is running in a separate process, the monitoring - // status in this process will not have been updated yet as a result of this - // region state change. We make a call here to keep it in sync. - MonitoringStatus.getInstanceForApplication(this).updateLocalState(region, state); - if (monitoringData.isInside()) { - notifier.didEnterRegion(monitoringData.getRegion()); - } else { - notifier.didExitRegion(monitoringData.getRegion()); - } - } - } - } + new IntentHandler().convertIntentsToCallbacks(this.getApplicationContext(), intent); } } diff --git a/src/main/java/org/altbeacon/beacon/BeaconLocalBroadcastProcessor.java b/src/main/java/org/altbeacon/beacon/BeaconLocalBroadcastProcessor.java new file mode 100644 index 000000000..8794252cf --- /dev/null +++ b/src/main/java/org/altbeacon/beacon/BeaconLocalBroadcastProcessor.java @@ -0,0 +1,92 @@ +/** + * Radius Networks, Inc. + * http://www.radiusnetworks.com + * + * @author David G. Young + * + * Licensed to the Apache Software Foundation (ASF) under one + * or more contributor license agreements. See the NOTICE file + * distributed with this work for additional information + * regarding copyright ownership. The ASF licenses this file + * to you 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 org.altbeacon.beacon; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.support.annotation.NonNull; +import android.support.v4.content.LocalBroadcastManager; + +import org.altbeacon.beacon.logging.LogManager; + +import java.util.Set; + +/** + * Converts internal intents to notifier callbacks + * + * This is used with ScanJob and supports delivering intents even under Android O background + * restrictions preventing starting a new IntentService. + * + * It is not used with the BeaconService, as local broadcast intents cannot be deliverd across + * different processes which the BeaconService supports. + * + * @see BeaconIntentProcessor for the equivalent use with BeaconService. + ** + * Internal library class. Do not use directly from outside the library + * + * @hide + */ +public class BeaconLocalBroadcastProcessor { + private static final String TAG = "BeaconLocalBroadcastProcessor"; + + public static final String RANGE_NOTIFICATION = "org.altbeacon.beacon.range_notification"; + public static final String MONITOR_NOTIFICATION = "org.altbeacon.beacon.monitor_notification"; + + @NonNull + private Context mContext; + private BeaconLocalBroadcastProcessor() { + + } + public BeaconLocalBroadcastProcessor(Context context) { + mContext = context; + + } + + static int registerCallCount = 0; + int registerCallCountForInstnace = 0; + public void register() { + registerCallCount += 1; + registerCallCountForInstnace += 1; + LogManager.d(TAG, "Register calls: global="+registerCallCount+" instance="+registerCallCountForInstnace); + unregister(); + LocalBroadcastManager.getInstance(mContext).registerReceiver(mLocalBroadcastReceiver, + new IntentFilter(RANGE_NOTIFICATION)); + LocalBroadcastManager.getInstance(mContext).registerReceiver(mLocalBroadcastReceiver, + new IntentFilter(MONITOR_NOTIFICATION)); + } + + public void unregister() { + LocalBroadcastManager.getInstance(mContext).unregisterReceiver(mLocalBroadcastReceiver); + } + + + private BroadcastReceiver mLocalBroadcastReceiver = new BroadcastReceiver() { + @Override + public void onReceive(Context context, Intent intent) { + new IntentHandler().convertIntentsToCallbacks(context, intent); + } + }; +} \ No newline at end of file diff --git a/src/main/java/org/altbeacon/beacon/BeaconManager.java b/src/main/java/org/altbeacon/beacon/BeaconManager.java index 1964cbf02..18f74e08f 100644 --- a/src/main/java/org/altbeacon/beacon/BeaconManager.java +++ b/src/main/java/org/altbeacon/beacon/BeaconManager.java @@ -321,8 +321,7 @@ protected BeaconManager(@NonNull Context context) { verifyServiceDeclaration(); } this.beaconParsers.add(new AltBeaconParser()); - // TODO: Change this to >= Build.VERSION_CODES.O when the SDK is released - mScheduledScanJobsEnabled = android.os.Build.VERSION.SDK_INT > Build.VERSION_CODES.N; + mScheduledScanJobsEnabled = android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.O; } /*** diff --git a/src/main/java/org/altbeacon/beacon/IntentHandler.java b/src/main/java/org/altbeacon/beacon/IntentHandler.java new file mode 100644 index 000000000..6e6c84a74 --- /dev/null +++ b/src/main/java/org/altbeacon/beacon/IntentHandler.java @@ -0,0 +1,85 @@ +package org.altbeacon.beacon; + +import android.content.Context; +import android.content.Intent; + +import org.altbeacon.beacon.logging.LogManager; +import org.altbeacon.beacon.service.MonitoringData; +import org.altbeacon.beacon.service.MonitoringStatus; +import org.altbeacon.beacon.service.RangingData; + +import java.util.Set; + +/** + * Converts internal Intents for ranging/monitoring to notifier callbacks. + * These may be local broadcast intents from BeaconLocalBroadcastProcessor or + * global broadcast intents fro BeaconIntentProcessor + * + * Internal library class. Do not use directly from outside the library + * + * @hide + * Created by dyoung on 7/20/17. + */ + +/* package private*/ +class IntentHandler { + private static final String TAG = IntentHandler.class.getSimpleName(); + public void convertIntentsToCallbacks(Context context, Intent intent) { + MonitoringData monitoringData = null; + RangingData rangingData = null; + + if (intent != null && intent.getExtras() != null) { + if (intent.getExtras().getBundle("monitoringData") != null) { + monitoringData = MonitoringData.fromBundle(intent.getExtras().getBundle("monitoringData")); + } + if (intent.getExtras().getBundle("rangingData") != null) { + rangingData = RangingData.fromBundle(intent.getExtras().getBundle("rangingData")); + } + } + + if (rangingData != null) { + LogManager.d(TAG, "got ranging data"); + if (rangingData.getBeacons() == null) { + LogManager.w(TAG, "Ranging data has a null beacons collection"); + } + Set notifiers = BeaconManager.getInstanceForApplication(context).getRangingNotifiers(); + java.util.Collection beacons = rangingData.getBeacons(); + if (notifiers != null) { + for(RangeNotifier notifier : notifiers){ + notifier.didRangeBeaconsInRegion(beacons, rangingData.getRegion()); + } + } + else { + LogManager.d(TAG, "but ranging notifier is null, so we're dropping it."); + } + RangeNotifier dataNotifier = BeaconManager.getInstanceForApplication(context).getDataRequestNotifier(); + if (dataNotifier != null) { + dataNotifier.didRangeBeaconsInRegion(beacons, rangingData.getRegion()); + } + } + + if (monitoringData != null) { + LogManager.d(TAG, "got monitoring data"); + Set notifiers = BeaconManager.getInstanceForApplication(context).getMonitoringNotifiers(); + if (notifiers != null) { + for(MonitorNotifier notifier : notifiers) { + LogManager.d(TAG, "Calling monitoring notifier: %s", notifier); + Region region = monitoringData.getRegion(); + Integer state = monitoringData.isInside() ? MonitorNotifier.INSIDE : + MonitorNotifier.OUTSIDE; + notifier.didDetermineStateForRegion(state, region); + // In case the beacon scanner is running in a separate process, the monitoring + // status in this process will not have been updated yet as a result of this + // region state change. We make a call here to keep it in sync. + MonitoringStatus.getInstanceForApplication(context).updateLocalState(region, state); + if (monitoringData.isInside()) { + notifier.didEnterRegion(monitoringData.getRegion()); + } else { + notifier.didExitRegion(monitoringData.getRegion()); + } + } + } + } + + } +} diff --git a/src/main/java/org/altbeacon/beacon/service/Callback.java b/src/main/java/org/altbeacon/beacon/service/Callback.java index 9d239c95a..a8c9ad926 100644 --- a/src/main/java/org/altbeacon/beacon/service/Callback.java +++ b/src/main/java/org/altbeacon/beacon/service/Callback.java @@ -27,7 +27,10 @@ import android.content.Context; import android.content.Intent; import android.os.Bundle; +import android.support.v4.content.LocalBroadcastManager; +import org.altbeacon.beacon.BeaconLocalBroadcastProcessor; +import org.altbeacon.beacon.BeaconManager; import org.altbeacon.beacon.logging.LogManager; import java.io.IOException; @@ -35,23 +38,9 @@ public class Callback implements Serializable { private static final String TAG = "Callback"; - private transient Intent mIntent; - private String mIntentPackageName; + //TODO: Remove this constructor in favor of an empty one, as the packae name is no longer needed public Callback(String intentPackageName) { - mIntentPackageName = intentPackageName; - initializeIntent(); - } - - private void initializeIntent() { - if (mIntentPackageName != null) { - mIntent = new Intent(); - mIntent.setComponent(new ComponentName(mIntentPackageName, "org.altbeacon.beacon.BeaconIntentProcessor")); - } - } - - public Intent getIntent() { - return mIntent; } /** @@ -63,20 +52,34 @@ public Intent getIntent() { * @return false if it callback cannot be made */ public boolean call(Context context, String dataName, Bundle data) { - if(mIntent == null){ - initializeIntent(); - } + boolean useLocalBroadcast = BeaconManager.getInstanceForApplication(context).getScheduledScanJobsEnabled(); boolean success = false; - if (mIntent != null) { - LogManager.d(TAG, "attempting callback via intent: %s", mIntent.getComponent()); - mIntent.putExtra(dataName, data); + + if(useLocalBroadcast) { + String action = null; + if (dataName == "rangingData") { + action = BeaconLocalBroadcastProcessor.RANGE_NOTIFICATION; + } + else { + action = BeaconLocalBroadcastProcessor.MONITOR_NOTIFICATION; + } + Intent intent = new Intent(action); + intent.putExtra(dataName, data); + LogManager.d(TAG, "attempting callback via local broadcast intent: %s",action); + success = LocalBroadcastManager.getInstance(context).sendBroadcast(intent); + } + else { + Intent intent = new Intent(); + intent.setComponent(new ComponentName(context.getPackageName(), "org.altbeacon.beacon.BeaconIntentProcessor")); + intent.putExtra(dataName, data); + LogManager.d(TAG, "attempting callback via global broadcast intent: %s",intent.getComponent()); try { - context.startService(mIntent); + context.startService(intent); success = true; } catch (Exception e) { LogManager.e( TAG, - "Failed attempting to start service: " + mIntent.getComponent().flattenToString(), + "Failed attempting to start service: " + intent.getComponent().flattenToString(), e ); } @@ -88,6 +91,5 @@ public boolean call(Context context, String dataName, Bundle data) { private void readObject(java.io.ObjectInputStream in) throws IOException, ClassNotFoundException { in.defaultReadObject(); - initializeIntent(); } } diff --git a/src/main/java/org/altbeacon/beacon/service/ScanJob.java b/src/main/java/org/altbeacon/beacon/service/ScanJob.java index 7ba27c465..d0c4ec6c8 100644 --- a/src/main/java/org/altbeacon/beacon/service/ScanJob.java +++ b/src/main/java/org/altbeacon/beacon/service/ScanJob.java @@ -7,8 +7,11 @@ import android.bluetooth.le.ScanResult; import android.os.Build; import android.os.Handler; +import android.support.annotation.NonNull; + import org.altbeacon.beacon.Beacon; import org.altbeacon.beacon.BeaconManager; +import org.altbeacon.beacon.BeaconLocalBroadcastProcessor; import org.altbeacon.beacon.BuildConfig; import org.altbeacon.beacon.Region; import org.altbeacon.beacon.distance.ModelSpecificDistanceCalculator; diff --git a/src/main/java/org/altbeacon/beacon/service/ScanJobScheduler.java b/src/main/java/org/altbeacon/beacon/service/ScanJobScheduler.java index bfe8d3545..43ed2699b 100644 --- a/src/main/java/org/altbeacon/beacon/service/ScanJobScheduler.java +++ b/src/main/java/org/altbeacon/beacon/service/ScanJobScheduler.java @@ -12,6 +12,7 @@ import android.support.annotation.Nullable; import android.support.annotation.RequiresApi; +import org.altbeacon.beacon.BeaconLocalBroadcastProcessor; import org.altbeacon.beacon.BeaconManager; import org.altbeacon.beacon.logging.LogManager; @@ -42,6 +43,8 @@ public class ScanJobScheduler { private Long mScanJobScheduleTime = 0L; @NonNull private List mBackgroundScanResultQueue = new ArrayList<>(); + @Nullable + private BeaconLocalBroadcastProcessor mBeaconNotificationProcessor; @NonNull public static ScanJobScheduler getInstance() { @@ -60,6 +63,13 @@ public static ScanJobScheduler getInstance() { private ScanJobScheduler() { } + private void ensureNotificationProcessorSetup(Context context) { + if (mBeaconNotificationProcessor == null) { + mBeaconNotificationProcessor = new BeaconLocalBroadcastProcessor(context); + mBeaconNotificationProcessor.register(); + } + } + /** * @return previoulsy queued scan results delivered in the background */ @@ -104,6 +114,8 @@ public void scheduleAfterBackgroundWakeup(Context context, List scan } private void schedule(Context context, ScanState scanState, boolean backgroundWakeup) { + ensureNotificationProcessorSetup(context); + long betweenScanPeriod = scanState.getScanJobIntervalMillis() - scanState.getScanJobRuntimeMillis(); long millisToNextJobStart;