I implemented the Activity Transitions API with a PendingIntent and a BroadcastReceiver as seen below. The code works perfectly fine on a Pixel 3a. However, on a Samsung A32 and Samsung S22 Pro, the Broadcast receiver is never reached, eventhough the ActivityRecognition.getClient(mainActivity).requestActivityTransitionUpdates() succeeds and enters the onSuccessListener().
After a lot of time spent reading through the internet, I wasn't able to find any further information. Neither concerning the Activity Transitions API, nor concerning such problems on Samsung devices (e.g. not raching BroadcastReceiver). Some people hint to disabling battery saving features from Samsung, but the App runs currently only in foreground in the MainActivty thread, therefore I don't think my problem is related to that. Other point out that, for example Huawai devices, need a diffferent permission than the in the android docs specified one for the Activity Transition API. So currently I'm specifing those three permissions (and check them run-time with ContextCompat.checkSelfPermission()):
<uses-permission android:name="com.google.android.gms.permission.ACTIVITY_RECOGNITION" />
<uses-permission android:name="com.huawei.hms.permission.ACTIVITY_RECOGNITION" />
<uses-permission android:name="android.permission.ACTIVITY_RECOGNITION" />
Code
Main class with
initalization function initController() called in the form of
mActivityTransitionController = ActivityTransitionController().also { it.initController(this#MainActivity) }
and the entrypoint onClickEnableOrDisableActivityRecognition():
class ActivityTransitionController() {
companion object {
internal val TRANSITION_RECEIVER_ACTION: String =
"MyMachineLearningStalkingProtection.TRANSITIONS_RECEIVER_ACTION"
}
private var activityTrackingOn: Boolean = false
private lateinit var activityTransitionList: List<ActivityTransition>
private lateinit var mActivityTransitionPendingIntent: PendingIntent
internal fun initController(mainActivity: MainActivity) {
activityTrackingOn = false
activityTransitionList = buildTransitionList()
val intent = Intent(TRANSITION_RECEIVER_ACTION)
mActivityTransitionPendingIntent =
PendingIntent.getBroadcast(mainActivity, 0, intent, PendingIntent.FLAG_MUTABLE)
Utils.makeSnackBar("Activity Recognition initialized!", mainActivity)
}
internal fun onClickEnableOrDisableActivityRecognition(mainActivity: MainActivity) {
if (activityTrackingOn) {
disableActivityTransitions(mainActivity)
} else {
enableActivityTransitions(mainActivity)
}
}
private fun buildTransitionList(): ArrayList<ActivityTransition> {
val list = ArrayList<ActivityTransition>()
list.add(
ActivityTransition.Builder()
.setActivityType(DetectedActivity.WALKING)
.setActivityTransition(ActivityTransition.ACTIVITY_TRANSITION_ENTER)
.build()
)
list.add(
ActivityTransition.Builder()
.setActivityType(DetectedActivity.WALKING)
.setActivityTransition(ActivityTransition.ACTIVITY_TRANSITION_EXIT)
.build()
)
list.add(
ActivityTransition.Builder()
.setActivityType(DetectedActivity.STILL)
.setActivityTransition(ActivityTransition.ACTIVITY_TRANSITION_ENTER)
.build()
)
list.add(
ActivityTransition.Builder()
.setActivityType(DetectedActivity.STILL)
.setActivityTransition(ActivityTransition.ACTIVITY_TRANSITION_EXIT)
.build()
)
return list
}
#SuppressLint("MissingPermission")
internal fun disableActivityTransitions(mainActivity: MainActivity) {
Log.d(Utils.MY_LOG_TAG, "disableActivityTransitions()")
ActivityRecognition.getClient(mainActivity)
.removeActivityTransitionUpdates(mActivityTransitionPendingIntent)
.addOnSuccessListener {
activityTrackingOn = false
Utils.makeSnackBar("Transitions successfully unregistered.", mainActivity)
}.addOnFailureListener {
Utils.makeSnackBar("Transitions could NOT be unregistered.", mainActivity)
Log.e(Utils.MY_LOG_TAG, "Transitions could not be unregistered $it")
}
}
#SuppressLint("MissingPermission")
internal fun enableActivityTransitions(mainActivity: MainActivity) {
Log.d(Utils.MY_LOG_TAG, "enableActivityTransitions()")
val request = ActivityTransitionRequest(activityTransitionList)
ActivityRecognition.getClient(mainActivity)
.requestActivityTransitionUpdates(request, mActivityTransitionPendingIntent)
.addOnSuccessListener {
Utils.makeSnackBar("Transitions Api was successfully registered", mainActivity)
activityTrackingOn = true
}
.addOnFailureListener {
Utils.makeSnackBar("Transitions Api could NOT be registered", mainActivity)
Log.e(Utils.MY_LOG_TAG, "Transitions Api could NOT be registered. $it")
}
}
}
Boradcast receiver
registerd in MainActivity's onStart()
unregisterd in MainActivity's onStop()
class ActivityTransitionReceiver : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
val mainActivity = context as MainActivity
val findViewById = mainActivity.findViewById<TextView>(R.id.txt_activity)
val now = Calendar.getInstance().time.toString()
val currentText = findViewById.text
if (currentText.isEmpty()) {
findViewById.text = "1##$now"
} else {
val split = currentText.split("##")
val num = split[0].toInt() + 1
findViewById.text = "$num##$now"
}
if (ActivityRecognitionResult.hasResult(intent)) {
Log.d(Utils.MY_LOG_TAG, "RECOGNITION called")
}
if (ActivityTransitionResult.hasResult(intent)) {
Log.d(Utils.MY_LOG_TAG, "TRANSITION called")
val result = ActivityTransitionResult.extractResult(intent!!)
for (event in result!!.transitionEvents) {
val activityType = event.activityType
val transitionType = event.transitionType
val elapsedRealTimeNanos = event.elapsedRealTimeNanos
val findViewById1 = mainActivity.findViewById<TextView>(R.id.txt_confidence)
findViewById1.text ="${findViewById1.text} + $activityType + $transitionType"
}
}
}
}
With this code, the Pixel 3a is able to detect my activities as soon as I call the onClickEnableOrDisableActivityRecognition() entrypoint. On the Samsung devices however, nothing happens, the requestActivityTransitionUpdates() succeeds, though the broadcast receiver ActivityTransitionReceiver is never reached. Do you guys have any idea why I expereience this behaviour? Maybe you experienced similar behaviour with a BroadcastReceiver and were able to fix it?
On a short side note: I also tested if the ActivityRecognition API is available on the Samsung devices using code which is equivalent as described in the docs https://developers.google.com/android/guides/api-client#check-api-availability which succeeded.
If something is unclear, do not hesitate to ask for clarification. Thanks in advance!
Related
I'm trying to make a custom implementation of a call using TelecomManager between two users who had installed my app on their devices
Following this guide I implemented connection service, subclass of Connection, added permissions, registered a PhoneAccount and so on...
The thing I'm struggling to understand for a third week already how to place a call between users of my app without using telephone number but user name or userId.
Below code starting to make a call from my device but this call never reaches end user device
telecomManager.placeCall(Uri.fromParts(/*tried also with PhoneAccount.SCHEME_SIP and PhoneAccount.SCHEME_TEL*/
TripmateConnectionService.SCHEME_AG, "userId", null), extras)
Need to mention, that in my BroadcastReceiver implementation I can detect incoming calls from other apps, so it's seems that I handling call detection correct and the call from above code is never really send to device of the user it was intended to.
Now is the question. I feel like I missing something vital. How exactly do devices with same app can communicate to each other without phone number? Does it really enough just to pass a user name to a telecomManager.placeCall and it should somehow manage to find right device with installed app and make a call to it? How can telecomManager distinguish where to make a call?
Sorry for unclear question, it's my first time doing something related to calls and I feel I luck understanding of the subject and it hard to make a question more concrete because I don't exactly know what am I missing.
I will put below some code I'm using now
Start an outgoing call
private fun placeSystemCall(myUid: String, peerUid: String, channel: String, role: Int) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
val extras = Bundle()
extras.putInt(TelecomManager.EXTRA_START_CALL_WITH_VIDEO_STATE, VideoProfile.STATE_BIDIRECTIONAL)
val extraBundle = Bundle()
extraBundle.putString(Constants.CS_KEY_UID, myUid)
extraBundle.putString(Constants.CS_KEY_SUBSCRIBER, peerUid)
extraBundle.putString(Constants.CS_KEY_CHANNEL, channel)
extraBundle.putInt(Constants.CS_KEY_ROLE, Constants.CALL_ID_OUT)
extras.putBundle(TelecomManager.EXTRA_OUTGOING_CALL_EXTRAS, extraBundle)
try {
val telecomManager = applicationContext.getSystemService(TELECOM_SERVICE) as TelecomManager
val pa: PhoneAccount = telecomManager.getPhoneAccount(
config().phoneAccountOut?.accountHandle)
extras.putBoolean(TelecomManager.EXTRA_START_CALL_WITH_SPEAKERPHONE, true);
extras.putParcelable(TelecomManager.EXTRA_PHONE_ACCOUNT_HANDLE, pa.accountHandle)
telecomManager.placeCall(Uri.fromParts(
TripmateConnectionService.SCHEME_AG, peerUid, null), extras)
} catch (e: SecurityException) {
e.printStackTrace()
}
}
}
In ConnectionService
override fun onCreateOutgoingConnection(phoneAccount: PhoneAccountHandle?, request: ConnectionRequest): Connection {
Log.i(TAG, "onCreateOutgoingConnection: called. $phoneAccount $request")
val extras = request.extras
val uid = extras.getString(Constants.CS_KEY_UID) ?: "0"
val channel = extras.getString(Constants.CS_KEY_CHANNEL) ?: "0"
val subscriber = extras.getString(Constants.CS_KEY_SUBSCRIBER) ?: "0"
val role = extras.getInt(Constants.CS_KEY_ROLE)
val videoState = extras.getInt(TelecomManager.EXTRA_START_CALL_WITH_VIDEO_STATE)
val connection = TripmateConnection(applicationContext, uid, channel, subscriber, role)
connection.setVideoState(videoState)
connection.setAddress(Uri.fromParts(SCHEME_AG, subscriber, null), TelecomManager.PRESENTATION_ALLOWED)
connection.setCallerDisplayName(subscriber, TelecomManager.PRESENTATION_ALLOWED)
connection.setRinging()
TMApplication.getInstance().config().setConnection(connection)
return connection
}
creating PhoneAccounts
private fun registerPhoneAccount(context: Context) {
val telecomManager = context.getSystemService(Context.TELECOM_SERVICE) as? TelecomManager
?: throw RuntimeException("cannot obtain telecom system service")
val accountHandleIn = PhoneAccountHandle(
ComponentName(context, TripmateConnectionService::class.java), Constants.PA_LABEL_CALL_IN)
val accountHandleOut = PhoneAccountHandle(
ComponentName(context, TripmateConnectionService::class.java), Constants.PA_LABEL_CALL_OUT)
try {
var paBuilder: PhoneAccount.Builder = PhoneAccount.builder(accountHandleIn, Constants.PA_LABEL_CALL_IN)
.setCapabilities(PhoneAccount.CAPABILITY_SELF_MANAGED)
val phoneIn = paBuilder.build()
paBuilder = PhoneAccount.builder(accountHandleOut, Constants.PA_LABEL_CALL_OUT)
.setCapabilities(PhoneAccount.CAPABILITY_SELF_MANAGED)
val extra = Bundle()
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
extra.putBoolean(PhoneAccount.EXTRA_LOG_SELF_MANAGED_CALLS, true)
}
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
paBuilder.setExtras(extra)
}
val phoneOut = paBuilder.build()
telecomManager.registerPhoneAccount(phoneIn)
telecomManager.registerPhoneAccount(phoneOut)
if (telecomManager.getPhoneAccount(phoneIn.accountHandle) == null || telecomManager.getPhoneAccount(phoneOut.accountHandle) == null) {
throw RuntimeException("cannot create account");
}
mCallSession = TripmateCallSession()
mCallSession?.phoneAccountIn = phoneIn
mCallSession?.phoneAccountOut = phoneOut
} catch (e: SecurityException) {
throw RuntimeException("cannot create account", e);
}
}
Thank you for your time! Any suggestions and links that could help me to understand more will be highly appreciated!
I am making a library that can be used to incorporate breaking and entering detection into any application
There is an arduino set to the alarm of the house which sends an SMS to a specific phone upon trigger
Within my sdk I register an sms receiver which upon receiving an sms with a specific text, should show a full screen activity (on top of the lockscreen too) that will alert the user
I created an application to test this behaviour
the application's package is : com.example.demo
the library's package is : com.example.sdk
the sms receiver looks like this:
class SMSReceiver : BroadcastReceiver() {
companion object {
const val TAG = "SMSReceiver"
}
private val logger by lazy { Injections.logger }
override fun onReceive(context: Context?, intent: Intent?) {
logger.log(TAG) { "Got sms" }
val ctx = context ?: return
val bundle = intent?.extras ?: return
val format = bundle.getString("format") ?: return
val pdus = (bundle["pdus"] as? Array<*>) ?: return
for (idx in pdus.indices) {
val pdu = pdus[idx] as? ByteArray ?: continue
val msg = SmsMessage.createFromPdu(pdu, format)
if (msg.messageBody.startsWith("theft event", true)) {
logger.log(TAG) { "Got theft event" }
abortBroadcast()
showTheftActivity(ctx, msg.messageBody)
break
}
}
}
private fun showTheftActivity(context: Context, messageBody: String) {
val intent = Intent(context, TheftActivity::class.java)
intent.addFlags(Intent.FLAG_FROM_BACKGROUND)
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
// .addCategory(Intent.CATEGORY_LAUNCHER)
val location = messageBody.split(" ").getOrNull(2)
if (location != null) {
val coords = location.split(",")
if (coords.size == 2) {
val x = coords[0].toBigDecimalOrNull()
val y = coords[1].toBigDecimalOrNull()
if (x != null && y != null) {
intent.putExtra(TheftActivity.X, x.toString())
intent.putExtra(TheftActivity.Y, y.toString())
}
}
}
context.startActivity(intent)
}
}
the activity that should show on top of everything is this :
class TheftActivity : Activity() {
companion object {
const val X = "locationX"
const val Y = "locationY"
}
private val x: String? by lazy { intent.getStringExtra(X) }
private val y: String? by lazy { intent.getStringExtra(Y) }
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_theft)
val location = findViewById<Button>(R.id.locate)
if (x != null && y != null) {
location.setOnClickListener { Toast.makeText(applicationContext, "Going to $x , $y", Toast.LENGTH_SHORT).show() }
location.isEnabled = true
finish()
} else {
location.isEnabled = false
}
findViewById<Button>(R.id.cancel).setOnClickListener {
finish()
}
turnScreenOnAndKeyguardOff()
}
private fun turnScreenOnAndKeyguardOff() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
setShowWhenLocked(true)
setTurnScreenOn(true)
} else {
window.addFlags(
WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
or WindowManager.LayoutParams.FLAG_ALLOW_LOCK_WHILE_SCREEN_ON
)
}
with(getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
requestDismissKeyguard(this#TheftActivity, null)
}
}
}
override fun onDestroy() {
super.onDestroy()
turnScreenOffAndKeyguardOn()
}
private fun turnScreenOffAndKeyguardOn() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
setShowWhenLocked(false)
setTurnScreenOn(false)
} else {
window.clearFlags(
WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON
or WindowManager.LayoutParams.FLAG_ALLOW_LOCK_WHILE_SCREEN_ON
)
}
}
}
and the sdk's android manifest contains this:
<application>
<activity
android:name=".ui.TheftActivity"
android:configChanges="orientation|keyboardHidden|screenSize"
android:excludeFromRecents="true"
android:label="#string/title_activity_theft"
android:showOnLockScreen="true"
android:theme="#style/Theme.Sdk.Fullscreen" />
<receiver
android:name=".receivers.SMSReceiver"
android:enabled="true"
android:exported="true"
android:permission="android.permission.BROADCAST_SMS">
<intent-filter>
<action android:name="android.provider.Telephony.SMS_RECEIVED" />
</intent-filter>
</receiver>
</application>
when testing this on an emulator I send the sms to trigger the theft event
if the activity for testing (the one in the com.example.demo package) is not closed then it is brought to the front , but if it is closed nothing happens (though I do see the log messages from the receiver)
how can I make my sms receiver open the TheftActivity instead of the main activity from the main package?
edit: if it helps, the theft activity seems to start and then get immediately destroyed
It looks like the system can't bring the activity to the foreground due to the restrictions implemented in Android Q
With Android Q, it is impossible to start an activity from the background automatically if your app does not include those exceptions listed in the link below.
https://developer.android.com/guide/components/activities/background-starts
For possible solutions :
https://stackoverflow.com/a/59421118/11982611
This is My Code :
#RequiresApi(Build.VERSION_CODES.M)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.home_activity)
loadProductForTheFirst()
#RequiresApi(Build.VERSION_CODES.M)
private fun hasNetworkAvilable(context: Context): Boolean {
val service = Context.CONNECTIVITY_SERVICE
val manager = context.getSystemService(service) as ConnectivityManager
val network = manager.activeNetwork
return (network != null)
}
#RequiresApi(Build.VERSION_CODES.M)
fun loadProductForTheFirst(){
swipeRefreshMain.isRefreshing = true
viewModel.getalldata().observe(this, Observer {
if (!it.isNullOrEmpty()) {
recycler_main.apply {
layoutManager = GridLayoutManager(this#HomeActivity, 2)
adapter = RecyclerAdapterMain(it, this#HomeActivity)
swipeRefreshMain.isRefreshing = false
}
} else {
if (hasNetworkAvilable(this)) {
viewModel.products.observe(this, Observer {
recycler_main.apply {
layoutManager = GridLayoutManager(this#HomeActivity, 2)
adapter = RecyclerAdapterMain(it, this#HomeActivity)
swipeRefreshMain.isRefreshing = false
}
})
viewModel.setup()
} else {
/// in here if the user not internet for loading the products
/// the alert dialog displays .
AlertDialog.Builder(this)
.setTitle("Internet State")
.setMessage("please turn on your internet connection")
.create()
.show()
/// in here I want a method ( workmanager )
// that as soon as the internet be accessible
/// my product will be updated .
}
}
})
}
well , For the first time that user open my app need the internet to load product from api .
So I just want the method like WorkManager to check if the intenrnet avalibility is accessible .
And after that my method will be load from api .
I did some search but could'nt find any useful example of work with workmanager.
anyone can help me with this . ?
I did this code and work for me .
I put it here if someone looking for this method .
I used work manager to get data from api whenever the network is on .
val constraints = Constriants.builder(this)
.setRequiredNetworkType(NetworkType.Connected)
val workManager : WorkManager = WorkManager.getInstance(this)
val oneRequestWork = OneRequestWorker.build(UploadWorker::class.java)
.setconstrints(constraints)
.build
workmanager.enqueue(oneRequestWork)
the Upload worker class :
class UploadWorker(context : Context , param : WorkerParameters) : Worker(context , param)
private val viewModel: ViewModelRoom by lazy {
ViewModelProvider(
ViewModelStore(),
FactoryRoom(RepositoryCart(DataBaseRoom.invoke(applicationContext)))
)
.get(ViewModelRoom::class.java)
}
override fun dowork() : Result {
return try {
viewModel.setup()
Result.success()
} catch (e: Exception) {
Result.failure()
}
I am coding a simple app that measures all available sensors of the android device (Wifi, BT, etc). One of them is the user activity (via ActivityRecognition API), but I can't make it works properly.
I code a class to do everything related to user activity. I want to get only 4 states and one attribute to store the current one:
var VEHICLE = "vehicle"
var WALKING = "walking"
var STILL = "still"
var UNKNOWN = "unknown"
private var current: String? = null
It also includes a BroadcastReceiver object to handle activity transitions:
private var recognitionHandler = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
if (ActivityRecognitionResult.hasResult(intent)) {
val result = ActivityRecognitionResult.extractResult(intent)
val activity = result.mostProbableActivity
current = when(activity.type) {
DetectedActivity.IN_VEHICLE,
DetectedActivity.ON_BICYCLE -> VEHICLE
DetectedActivity.WALKING,
DetectedActivity.RUNNING -> WALKING
DetectedActivity.STILL -> STILL
else -> UNKNOWN
}
}
}
}
The class also have two methods to define the intent and request:
private fun createIntent() : PendingIntent {
val intent = Intent(context, recognitionHandler.javaClass)
val pendingIntent = PendingIntent.getBroadcast(context, 0, intent, 0)
context.registerReceiver(recognitionHandler, IntentFilter())
return pendingIntent
}
private fun createRequest() : ActivityTransitionRequest {
val types = listOf(
DetectedActivity.IN_VEHICLE,
DetectedActivity.WALKING,
DetectedActivity.RUNNING,
DetectedActivity.ON_BICYCLE,
DetectedActivity.STILL
)
val transitions = mutableListOf<ActivityTransition>()
types.forEach { activity ->
transitions.add(
ActivityTransition.Builder()
.setActivityType(activity)
.setActivityTransition(ActivityTransition.ACTIVITY_TRANSITION_ENTER)
.build()
)
}
return ActivityTransitionRequest(transitions)
}
And also one to start listening:
override fun start(onResult: (res: String?) -> Unit) {
// ...
intent = createIntent()
val request = createRequest()
ActivityRecognition.getClient(context)
.requestActivityTransitionUpdates(request, intent)
.addOnSuccessListener {
Log.d("UserActivity Service info", "listening...")
}
.addOnFailureListener { e ->
Log.d("UserActivity Service error", e.toString())
}
// ...
}
The problem is that the current attribute is always null. I think I have some issues with intent or handler registration, but I have no idea where.
Does someone have any comments? :)
Thanks!
This is your problem. In this code from createIntent():
val intent = Intent(context, recognitionHandler.javaClass)
val pendingIntent = PendingIntent.getBroadcast(context, 0, intent, 0)
context.registerReceiver(recognitionHandler, IntentFilter())
return pendingIntent
You return a PendingIntent that you use in the call to requestActivityTransitionUpdates(). However, that PendingIntent refers to a dynamically created inner class (your BroadcastReceiver) and Android cannot instantiate that class.
You also additionally call registerReceiver(), however you pass an empty IntentFilter in that call so the registered BroadcastReceiver is never called.
To fix the problem, you can either provide a correctIntentFilter that matches your PendingIntent OR you can refactor your BroadcastReceiver into a proper class (not a private inner class) and make sure that you've added the BroadcastReceiver to your manifest and make it publicly available (exported="true").
Here's an example of how to do this using a BroadcastReceiver:
https://steemit.com/utopian-io/#betheleyo/implementing-android-s-new-activity-recognition-transition-api
Initially I setup a BroadcastReceiver to receive intents from the Nearby Messages API.
class BeaconMessageReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
Nearby.getMessagesClient(context).handleIntent(intent, object : MessageListener() {
override fun onFound(message: Message) {
val id = IBeaconId.from(message)
Timber.i("Found iBeacon=$id")
sendNotification(context, "Found iBeacon=$id")
}
override fun onLost(message: Message) {
val id = IBeaconId.from(message)
Timber.i("Lost iBeacon=$id")
sendNotification(context, "Lost iBeacon=$id")
}
})
}
private fun sendNotification(context: Context, text: String) {
Timber.d("Send notification.")
val manager = context.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
val notification = NotificationCompat.Builder(context, Notifications.CHANNEL_GENERAL)
.setContentTitle("Beacons")
.setContentText(text)
.setSmallIcon(R.drawable.ic_notification_white)
.build()
manager.notify(NotificationIdGenerator.nextID(), notification)
}
}
Then registered this receiver in my MainActivity after location permissions have been granted.
class MainActivity : AppCompatActivity() {
// ...
private fun onLocationPermissionsGranted() {
val filter = MessageFilter.Builder()
.includeIBeaconIds(UUID.fromString("B9407F30-F5F8-466E-AFF9-25556B57FEED"), null, null)
.build()
val options = SubscribeOptions.Builder().setStrategy(Strategy.BLE_ONLY).setFilter(filter).build()
Nearby.getMessagesClient(context).subscribe(getPendingIntent(), options)
}
private fun getPendingIntent(): PendingIntent = PendingIntent.getBroadcast(
this, 0, Intent(context, BeaconMessageReceiver::class.java), PendingIntent.FLAG_UPDATE_CURRENT)
}
This worked well while the app was open, but does not work when the app is closed. So I found this example, that demonstrates how to setup an IntentService to receive messages while the app is in the background.
The example does use the Nearby.Messages class, which was deprecated in favor of the MessagesClient. So I replaced the deprecated code with the MessagesClient implementation.
class MainActivity : AppCompatActivity() {
// ...
private fun onLocationPermissionsGranted() {
val filter = MessageFilter.Builder()
.includeIBeaconIds(UUID.fromString("B9407F30-F5F8-466E-AFF9-25556B57FEED"), null, null)
.build()
val options = SubscribeOptions.Builder().setStrategy(Strategy.BLE_ONLY).setFilter(filter).build()
Nearby.getMessagesClient(context).subscribe(getPendingIntent(), options)
.addOnSuccessListener {
Timber.i("Subscribed successfully.")
startService(Intent(this, BeaconMessageIntentService::class.java))
}.addOnFailureListener {
Timber.e(exception, "Subscription failed.")
}
}
private fun getPendingIntent(): PendingIntent = PendingIntent.getBroadcast(
this, 0, Intent(context, BeaconMessageIntentService::class.java), PendingIntent.FLAG_UPDATE_CURRENT)
}
And this is the IntentService (which is almost identical to my BroadcastReceiver).
class BeaconMessageIntentService : IntentService("BeaconMessageIntentService") {
override fun onHandleIntent(intent: Intent?) {
intent?.let {
Nearby.getMessagesClient(this)
.handleIntent(it, object : MessageListener() {
override fun onFound(message: Message) {
val id = IBeaconId.from(message)
Timber.i("Found iBeacon=$id")
sendNotification("Found iBeacon=$id")
}
override fun onLost(message: Message) {
val id = IBeaconId.from(message)
Timber.i("Lost iBeacon=$id")
sendNotification("Lost iBeacon=$id")
}
})
}
}
private fun sendNotification(text: String) {
Timber.d("Send notification.")
val manager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
val notification = NotificationCompat.Builder(this, Notifications.CHANNEL_GENERAL)
.setContentTitle("Beacons")
.setContentText(text)
.setSmallIcon(R.drawable.ic_notification_white)
.build()
manager.notify(NotificationIdGenerator.nextID(), notification)
}
}
onHandleIntent is called, and the Intent is not null; yet for some reason onFound() and onLost() are never called. Why would this be the case?
It's not really a solution but what I found is this (credit to this answer):
I've tried a few configurations including a BroadcastReceiver and adding a JobIntentService to run the code in the background, but every time I got this the onExpired callback which you can set to the SubscribeOptions:
options.setCallback(new SubscribeCallback() {
#Override
public void onExpired() {
super.onExpired();
Toast.makeText(context.get(), "No longer Subscribing!", Toast.LENGTH_SHORT).show();
}
}
When the subscribe occurred in the background it was delayed, but it was still called.
Notes:
1. When I've tested with Strategy.BLE_ONLY I did not get the onFound callback.
2. From Google's documentation:
Background subscriptions consumes less power than foreground
subscriptions, but have higher latency and lower reliability
When testing I found this "lower reliability" to be an understatement: onFound was rarely called and I never got the onLost.
I know this is a late reply, but I had the same problem and found out by debugging that it is an issue related to this error: "Attempting to perform a high-power operation from a non-Activity Context". This can be solved when calling Nearby.getMessagesClient(this) by passing in an activity context instead of this.
In my case I added a class extending Application which helps in returning this context (the below is in java but should be translatable to kotlin easily)
public class MyApplication extends Application {
private Activity currentActivity = null;
public Activity getCurrentActivity(){
return currentActivity;
}
public void setCurrentActivity(Activity mCurrentActivity){
this.currentActivity = mCurrentActivity;
}
}
And in my base activity, from which all activities extend, I set the current activity by calling ((MyApplication) this.getApplicationContext()).setCurrentActivity(this); in the constructor.
My service can then call getMessagesClient with the correct context like below:
final Activity context = ((MyApplication)getApplicationContext()).getCurrentActivity();
Nearby.getMessagesClient(context).[...]
Do not forget to register your Application class in the AndroidManifest:
<application
android:name="com.example.xxx.MyApplication"`