Starting Bluetooth discoverable request from an Android Context and handling user rejection - android

I have Kotlin code that starts device discovery by using the ACTION_REQUEST_DISCOVERABLE activity. The code is:
val intentFilter = IntentFilter()
intentFilter.addAction(BluetoothDevice.ACTION_ACL_CONNECTED)
intentFilter.addAction(BluetoothDevice.ACTION_PAIRING_REQUEST)
intentFilter.addAction(BluetoothAdapter.ACTION_DISCOVERY_FINISHED)
discoveryBroadcastReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
when (intent.action) {
BluetoothDevice.ACTION_ACL_CONNECTED -> onAclConnected(intent, onFoundNewPairedDevice)
BluetoothDevice.ACTION_PAIRING_REQUEST -> onPairingRequest(intent)
BluetoothAdapter.ACTION_DISCOVERY_FINISHED -> onDiscoveryFinished()
else -> Unit
}
}
}
androidContext.registerReceiver(discoveryBroadcastReceiver, intentFilter)
val discoverableIntent = Intent(BluetoothAdapter.ACTION_REQUEST_DISCOVERABLE).apply {
putExtra(BluetoothAdapter.EXTRA_DISCOVERABLE_DURATION, discoveryDuration)
putExtra(BluetoothAdapter.EXTRA_SCAN_MODE, BluetoothAdapter.SCAN_MODE_CONNECTABLE_DISCOVERABLE)
flags = flags or Intent.FLAG_ACTIVITY_NEW_TASK
}
androidContext.startActivity(discoverableIntent)
This brings up a dialog box where the user can accept or reject the request to make the device discoverable for discoveryDuration seconds.
The problem is that this code does not notify me when the user rejects this request. I do not get any such notification through the BroadcastReceiver from what I see. Instead, it is given to me through the onActivityResult callback. But I can't write such a callback, since the code for the ACTION_REQUEST_DISCOVERABLE activity is pre-written (it is part of Android itself). So I do not know how to proceed. Furthermore, androidContext is really a Context, not Activity, so I also do not have startActivityForResult. Perhaps I could create my own sub-activity and use startActivityForResult from within that, but I am uncertain if this is the correct approach.
Since I am rather new to Android, I could use some help here to know how to proceed, especially since there are apparently multiple approaches to handling this.

Related

Intent service can't be started in background

In my android app I want to detect activity change from still to walking and start tracking location, regardless of the state of the app (in background or shut down completely).
I was able to create location tracking service which works while app in background by making it a foreground service (showing notification), but I have not been able to start tracking based on activity detection.
This is fragment of code of IntentService, which supposed to start location tracking service, after receiving intent with activity transition detected:
class ActivityDetectionIntent : IntentService(TAG) {
override fun onHandleIntent(intent: Intent?) {
val i = Intent(this#ActivityDetectionIntent, LocationTracking::class.java)
if (Build.VERSION.SDK_INT >= 26) {
startForegroundService(i)
// this followed by foregroundService call in LocationTracking service
} else {
startService(i)
}
}
// ...
}
This is the error message I'm getting:
2019-12-04 19:57:59.797 3866-15015/? W/ActivityManager: Background start not
allowed: service Intent { cmp=com.anatoliymakesapps.myapplication/.ActivityDetectionIntent
(has extras) } to com.anatoliymakesapps.myapplication/.ActivityDetectionIntent
from pid=-1 uid=10377 pkg=com.anatoliymakesapps.myapplication startFg?=false
I wonder if I miss something obvious, or maybe this whole approach is wrong and I need to try something else? Any piece of advice to achieve the desired result is appreciated.
I tried changing IntentService to JobIntentService but it made no difference, error looks the same.
Turns out intent service can not be started directly, but with help of broadcast receiver it can be achieved indirectly.
This is what I used instead of IntentService:
class ActivityTransitionBroadcastReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
Log.i(TAG, "got activity transition signal")
val i = Intent(context, LocationTrackingService::class.java)
if (Build.VERSION.SDK_INT >= 26) {
startForegroundService(context, i)
} else {
context.startService(i)
}
}
companion object {
private val TAG = ActivityTransitionBroadcastReceiver::class.java.simpleName
}
}
manifest:
<receiver android:name=".ActivityTransitionBroadcastReceiver" android:exported="true" />

Testing Activity Recognition Transition API

I am currently developing an app which uses Activity Recognition Transition API (the new one, not the old, check the link below). My question is, how can I test my app? More exactly, how can I "manually" trigger transition events? Do I really have to put my phone and laptop into my backpack and go for a ride on a bicycle to trigger the ON_BYCICLE/ACTIVITY_TRANSITION_ENTER event? There must be an easier way :) Maybe using adb? https://developer.android.com/studio/test/command-line
API documentation: https://developer.android.com/guide/topics/location/transitions
You can use following code to emulate the event
var intent = Intent()
// Your broadcast receiver action
intent.action = BuildConfig.APPLICATION_ID + "TRANSITIONS_RECEIVER_ACTION"
var events: ArrayList<ActivityTransitionEvent> = arrayListOf()
// You can set desired events with their corresponding state
var transitionEvent = ActivityTransitionEvent(DetectedActivity.IN_VEHICLE, ActivityTransition.ACTIVITY_TRANSITION_ENTER, SystemClock.elapsedRealtimeNanos())
events.add(transitionEvent)
var result = ActivityTransitionResult(events)
SafeParcelableSerializer.serializeToIntentExtra(result, intent, "com.google.android.location.internal.EXTRA_ACTIVITY_TRANSITION_RESULT")
activity?.sendBroadcast(intent)
I have created simple activity with two buttons in my application where I broadcast these events(Start and Stop) respectively. This helps me debug the application gracefully.
I don't know of any way to simulate a transition event from outside your app. But within your app, you can construct a suitable Intent and send it to your receiver.
Add this method to any Context (e.g. an Activity or Service): (Kotlin)
class MyService: Service() {
fun sendFakeActivityTransitionEvent() {
// name your intended recipient class
val intent = Intent(this, MyReceiver::class.java)
val events: ArrayList<ActivityTransitionEvent> = arrayListOf()
// create fake events
events.add(
ActivityTransitionEvent(
DetectedActivity.ON_BICYCLE,
ActivityTransition.ACTIVITY_TRANSITION_ENTER,
SystemClock.elapsedRealtimeNanos()
)
)
// finally, serialize and send
val result = ActivityTransitionResult(events)
SafeParcelableSerializer.serializeToIntentExtra(
result,
intent,
"com.google.android.location.internal.EXTRA_ACTIVITY_TRANSITION_RESULT"
)
this.sendBroadcast(intent)
}
}
Substitute your desired recipient for MyReceiver -- any subclass of BroadcastReceiver should work.
Then call sendFakeActivityTransitionEvent() when desired.

What happens to a PendingIntent if the target app was force-closed?

I'm actually working on an app that should post a notification 5 days in the future.
Using AlarmManager, I send a PendingIntent to my Receiver class.
Everything works fine until I force close my app. In this case, the notification doesn't appear.
So my question:
What happens to this PendingIntent, which was fired and did not reach its target?
When my app is finally restarted, can I check for PendingIntents, that did not reach its target?
EDIT 1:
These are the essential parts of my Broadcast Receiver:
override fun onReceive(context: Context?, intent: Intent?) {
if (context != null && intent?.action != null) {
when (intent.action) {
INTENT_ACTION_BOOT_COMPLETED -> handleDeviceBoot()
INTENT_ACTION_REMINDER -> handleReminder(context, intent.getLongExtra(EXTRA_ITEM_ID, -1))
}
}
}
private suspend fun schedule(context: Context, itemId: Long, fireDate: LocalDateTime) = withContext(Dispatchers.IO) {
AlarmManagerCompat.setAndAllowWhileIdle(
getAlarmManager(context),
AlarmManager.RTC,
fireDate.atZone(ZoneId.systemDefault()).toInstant().toEpochMilli(),
makePendingIntent(context, itemId)
)
with(AppDatabase.get(context).reminderDao()) {
val oldReminder = getItemReminder(itemId)
if (oldReminder == null) {
insert(Reminder(itemId = itemId, fireDate = fireDate))
} else {
update(Reminder(id = oldReminder.id, itemId = itemId, fireDate = fireDate))
}
}
}
private suspend fun cancel(context: Context, itemId: Long) = withContext(Dispatchers.IO) {
val reminderDao = AppDatabase.get(context).reminderDao()
val reminder = reminderDao.getItemReminder(itemId)
reminder?.let {
getAlarmManager(context).cancel(makePendingIntent(context, itemId))
reminderDao.delete(it)
}
}
private fun getAlarmManager(context: Context) = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
private fun makePendingIntent(context: Context, itemId: Long): PendingIntent {
val alarmIntent = Intent(context, ReminderManager::class.java).apply {
action = INTENT_ACTION_REMINDER
putExtra(EXTRA_ITEM_ID, itemId)
}
return PendingIntent.getBroadcast(context, itemId.toInt(), alarmIntent, PendingIntent.FLAG_UPDATE_CURRENT)
}
As defined in Official Android Documentation
A PendingIntent itself is simply a reference to a token maintained by the system describing the original data used to retrieve it. This means that, even if its owning application's process is killed, the PendingIntent itself will remain usable from other processes that have been given it. If the creating application later re-retrieves the same kind of PendingIntent (same operation, same Intent action, data, categories, and components, and same flags), it will receive a PendingIntent representing the same token if that is still valid, and can thus call cancel() to remove it.
Revisit your code to check if there is anything else that would be causing this issue.
When you "force close" an application, the application gets set to the "stopped state". In the "stopped state" your application will NOT be automatically started by Android until the user manually restarts the application. This means that if you "force close" your app, your app will not receive any broadcast Intents until it is manually restarted by the user.
I expect (although I have not tried it myself), that if you schedule an alarm to go off at time X and before time X you "force close" the app, when time X happens, the alarm manager will try to send the PendingIntent, however Android will refuse to actually execute the BroadcastReceiver because the app is in the "stopped state". In this case I expect the trigger is lost. Android will not retry or reschedule it.
Basically, when a user "force close"s an app, he is telling Android that he doesn't want that app to run anymore, including any background processes that the app might have, or want to start in the future.
The answer is short: Active PendingIntents are cancelled on an application force-stop.

Xamarin Android: get one app's state information from another

I have two Xamarin Android apps -- let's call them "Archy" and "Mehitabel".
Archy has some persistent state information (which, let's say for the sake of argument, is in a SQLite DB).
If a certain thing happens to Mehitabel, she needs to know a piece of that state information.
To accomplish this feat, I have Mehitabel send an intent to Archy. Archy has a broadcast receiver that hears it, gathers the necessary state, and fires a different intent back to Mehitabel.
Here's the code from Archy:
[BroadcastReceiver(Enabled = true)]
[IntentFilter(new [] { "com.example.Archy.SendStateToMehitabel"})]
public class StateQueryReceiver : BroadcastReceiver
{
public override void OnReceive(Context context, Intent intent)
{
var msg = new Intent("com.example.Mehitabel.StateFromArchy");
msg.PutExtra("ImportantStateInfo", GetSomeState());
context.SendBroadcast(msg);
}
}
And here's the code from Mehitabel:
private async Task AskArchyForState()
{
var filter = new IntentFilter("com.example.Mehitabel.StateFromArchy");
var csrc = new TaskCompletionSource<bool>();
var rcvr = new ActionBroadcastReceiver((context, intent) =>
{
State = intent.GetStringExtra("ImportantStateInfo");
csrc.TrySetResult(State != null);
});
RegisterReceiver(rcvr, filter);
var msg = new Intent("com.example.Archy.SendStateToMehitabel");
SendBroadcast(msg);
var task = await Task.WhenAny(csrc.Task, Task.Delay(Timeout));
UnregisterReceiver(rcvr);
if (task != csrc.Task)
bomb("Archy has not answered state query after {0}ms", Timeout);
if (!csrc.Task.IsCompletedSuccessfully || csrc.Task.Result == false)
bomb("failed to get all necessary state from Archy");
}
That all works great, provided Archy is actually running (i.e., shown in the "recent" list). If Archy isn't running, Archy's receiver code is never executed and Mehitabel times out.
My hope is I'm missing something simple (like a flag in one of the receiver attributes, or some secret sauce in the com.example.Archy.SendStateToMehitabel intent).
Can you tell me what I'm missing here?
Do I need to be using a completely different approach (like having Mehitabel StartActivityForResult() an activity within Archy, or using a service that starts on boot and runs all the time)?
Based on my research, I think you could open Archy before you need the data in Mehitabel. This is demo about opening an app in code.
Intent launchIntent = PackageManager.GetLaunchIntentForPackage("NewTestApp.NewTestApp");
if (launchIntent != null)
{
StartActivity(launchIntent);
}
Note:NewTestApp.NewTestApp is package name of Archy.

Can I see active/pending syncs in :sync process from main process?

Is it possible to determine (from the process an app was started in) sync status for a SyncAdapter that is running in a separate :sync process? I've been toying with the standard ContentResolver methods below and can't get any of them to return true unless the code below executes in the same process as my SyncAdapter (the :sync process).
val currentSyncs = ContentResolver.getCurrentSyncs().any { it.authority == <authority> }
val syncPending = ContentResolver.isSyncPending(account, <authority>)
val syncActive = ContentResolver.isSyncActive(account, <authority>)
None of the sync framework documentation seems to indicate that this isn't possible in cross-process scenarios, so I'm a bit stumped, but it seems like the most likely explanation for this.
I was rather unlucky trying to find a reliable way to get sync status information from ContentResolver.
What worked for me: Use intents to broadcast information from your SyncAdapter to another component:
// when sync started
Intent i = new Intent();
i.setAction(MyIntents.INTENT_ACTION_SYNC_STARTED);
context.sendBroadcast(i);
// when sync completed
Intent i = new Intent();
i.setAction(MyIntents.INTENT_ACTION_SYNC_DONE);
context.sendBroadcast(i);
and then receive the intents:
registerReceiver(new BroadcastReceiver() {
#Override
public void onReceive(Context context, Intent intent) {
Toast.makeText(context, 'Sync started', Toast.LENGTH_LONG).show();
}
}, new IntentFilter(MyIntents.INTENT_ACTION_SYNC_STARTED));

Categories

Resources