Scanning Bluetooth Low Energy in Background Android 11 - android

I'm developing an app in which I want to continuously scan for Ble advertisement packets, even if the user locks the screen. With my current implementation this works fine with Android 10, but with Android 11 it stops once the user locks the screen. For scanning Ble packets I first request a few permissions, namely:
coarse and fine location
bluetooth scan
access background location
I start a simple foreground service (also added foreground service permission to my Manifest) with:
private fun startBleService() {
serviceIntent = Intent(baseContext, ScanService::class.java)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
Log.i("Background Act", "Starting foreground service for android 8.0+")
applicationContext.startForegroundService(serviceIntent)
} else {
Log.i("Background Act", "Starting foreground service for versions < android 8.0")
applicationContext.startService(serviceIntent)
}
}
This will call startForeground(notificationID, notification) in the onStartCommand function of my ScanService, thus requesting to run in foreground. After this I start the actual Ble scan functionalities. I also added android:foregroundServiceType="location" to the service in the Manifest.
My ScanService Code:
class ScanService : Service() {
private val channelID = "CustomChannelID"
private val notificationID = 7
private lateinit var bluetoothManager: BluetoothManager
private lateinit var bluetoothAdapter: BluetoothAdapter
private lateinit var bluetoothLeScanner: BluetoothLeScanner
private var scanCounter = 0
private var _bleSingleScanResults = HashMap<String, MutableList<String>>()
// all scans combined in an array, internally used
private var _bleAllScanResults = arrayListOf<HashMap<String, MutableList<String>>>()
// only starts a new scan if its not already scanning
private var scanning = false
private val notificationManager by lazy {getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager}
#RequiresApi(Build.VERSION_CODES.M)
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
Log.i("OnStartCommand Service", "Is started")
val notification: Notification =
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
createChannel(notificationManager)
Notification.Builder(this, channelID)
.setContentTitle("Content Title")
.setContentText("Content Text")
.setTicker("Ticker")
.build()
} else {
NotificationCompat.Builder(this, channelID)
.setContentTitle("BLE Scanning Service")
.setContentText("Scanning BLE in the background")
.setTicker("Ticker")
.build()
}
// use custom non-zero notification ID
startForeground(notificationID, notification)
//TODO: start in new thread
scanBle()
// If we get killed, after returning from here, restart, recreating notification again though
return START_STICKY
}
#RequiresApi(Build.VERSION_CODES.M)
fun scanBle() {
bluetoothManager = this.getSystemService(Context.BLUETOOTH_SERVICE) as BluetoothManager
bluetoothAdapter = bluetoothManager.adapter
if (bluetoothAdapter.bluetoothLeScanner == null) {
Log.d("BLE", "Device doesn't support BLE")
Toast.makeText(
this,
"It seems like your device does not support BLE. This is a crucial part of this app. \n " +
"Unfortunately you can't contribute to the dataset of scanned locations.",
Toast.LENGTH_LONG
).show()
return
}
bluetoothLeScanner = bluetoothAdapter.bluetoothLeScanner
runBLEScan()
}
#SuppressLint("MissingPermission") //since we check beforehand in the MainActivity for permissions already
private fun runBLEScan() {
val scanSettings = ScanSettings.Builder()
.setScanMode(ScanSettings.SCAN_MODE_LOW_LATENCY)
.build()
val scanFilters = listOf(ScanFilter.Builder().build())
if (!scanning) {
scanning = true
Log.i("BLE", "--- STARTING BLE SCAN ---")
bluetoothLeScanner.startScan(scanFilters, scanSettings, scanCallBackLe)
} else Log.d("BLE Scan", "Called scanning function but is currently already scanning!")
}
// ALWAYS ON UI-THREAD
private val scanCallBackLe = object : ScanCallback() {
override fun onScanResult(callbackType: Int, result: ScanResult) {
super.onScanResult(callbackType, result)
// discard result if payload is null
if(result.scanRecord == null || result.scanRecord!!.bytes == null) {
return
}
println("Payload: ${result.scanRecord?.bytes?.toHexString()}")
// check if device already discovered in a scan, if so increase counter, else make new
// entry in the result HashMap
if (_bleSingleScanResults.isEmpty() || !_bleSingleScanResults.containsKey(result.device.toString())) {
// device wasn't seen before
_bleSingleScanResults[result.device.toString()] =
mutableListOf(result.rssi.toString(), result.scanRecord!!.bytes.toHexString(), "1")
} else {
// update already existing entry
val cntr = _bleSingleScanResults[result.device.toString()]!![2].toInt() + 1
_bleSingleScanResults[result.device.toString()]!![2] = cntr.toString()
}
}
override fun onScanFailed(errorCode: Int) {
super.onScanFailed(errorCode)
Log.d("BLE ScanResult", "Scan failed code: $errorCode")
}
}
private fun ByteArray.toHexString() = joinToString("", "[0x", "]") { "%02X".format(it) }
private fun createChannel(notificationManager: NotificationManager) {
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.O) {
return
}
val channel =
NotificationChannel(channelID, "Scan Service", NotificationManager.IMPORTANCE_DEFAULT)
channel.description = "Hello! This is a notification."
notificationManager.createNotificationChannel(channel)
}
#SuppressLint("MissingPermission")
override fun onDestroy() {
Log.d("Destroyed Service", "That's even worse")
bluetoothLeScanner.stopScan(scanCallBackLe)
super.onDestroy()
}
#SuppressLint("MissingPermission")
override fun stopService(name: Intent?): Boolean {
Log.d("Stopped Service", "That's bad")
bluetoothLeScanner.stopScan(scanCallBackLe)
stopSelf()
return super.stopService(name)
}
override fun onBind(intent: Intent): IBinder {
TODO("Return the communication channel to the service.")
}
Parts of my Manifest:
<uses-permission android:name="android.permission.BLUETOOTH" />
<uses-permission android:name="android.permission.BLUETOOTH_SCAN" />
<uses-permission android:name="android.permission.BLUETOOTH_ADMIN" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_BACKGROUND_LOCATION" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<application
...
<service
android:name=".ScanService"
android:enabled="true"
android:exported="false"
android:foregroundServiceType="location" />
...
</application>
This works fine with Android 10 (tested on a Huawei device), but unfortunately not on Android 11 (Samsung A22). Is there any other permission I need to be able to keep scanning even if the user locks the screen in Android 11?

Related

Foreground Service for sensor data stops after a small period of time

I am developing an android application where sensor data are recorded from the sensors, displayed in graphs and also stored in an SQLite database. I want the sensor data process to run when the app is not visible in the screen, so i designed a foreground service for that purpose. The service seems to be running ok for about 5 min and then the app stops recording sensor data.
I am trying to understand why this is occuring. The app in the emulator is not turned off, the screen is lit. I have declared my foreground service, (i call startForeground on onCreate() function of the service).
I am showcasing part of the code of the foreground service:
override fun onCreate() {
super.onCreate()
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.O)
startMyOwnForeground()
else
startForeground(1, Notification())
sensorManager = getSystemService(Context.SENSOR_SERVICE) as SensorManager
initSensors()
registerListeners()
}
#RequiresApi(Build.VERSION_CODES.O)
private fun startMyOwnForeground() {
val chan = NotificationChannel(
"MyChannelId",
"My Foreground Service",
NotificationManager.IMPORTANCE_HIGH
)
val manager = (getSystemService(NOTIFICATION_SERVICE) as NotificationManager)!!
manager!!.createNotificationChannel(chan)
val notificationBuilder: NotificationCompat.Builder = NotificationCompat.Builder(
this, "MyChannelId"
)
val notification: Notification = notificationBuilder.setOngoing(true)
.setContentTitle("App is running on foreground")
.setPriority(NotificationManager.IMPORTANCE_HIGH)
.setSmallIcon(R.drawable.sym_def_app_icon)
.setCategory(Notification.CATEGORY_SERVICE)
.setChannelId("MyChannelId")
.build()
startForeground(1, notification)
}
And small excerpt from the sensor recording process:
private fun initSensors()
{
accelerometerSensor = sensorManager.getDefaultSensor(Sensor.TYPE_ACCELEROMETER)
accelometerSensorEventListener = object: SensorEventListener
{
override fun onSensorChanged(event: SensorEvent?) {
for (i in 0 until event!!.values.size)
{
accelerometerValues[i] = event.values[i]
}
//keep the time stamp for each event
if (!acceleratorTimeStampSet)
{
acceleratorTimeStampSet = true
initialAcceleratorTimeStamp = System.currentTimeMillis()
}
//set the timestamps
val timestampX: Long = System.currentTimeMillis() - initialAcceleratorTimeStamp
accelerometerValuesTimeStampCounter ++
//store the timestamps
accelerometerValuesTimeStamps.add(timestampX)
val accelSensorTriplets = ArrayList<Float>(3)
if (accelSensorTriplets.size == 0)
{
accelSensorTriplets.add(0, event.values[0])
accelSensorTriplets.add(1, event.values[1])
accelSensorTriplets.add(2, event.values[2])
}
else
{
accelSensorTriplets.set(0, event.values[0])
accelSensorTriplets.set(1, event.values[1])
accelSensorTriplets.set(2, event.values[2])
}
//add a dateTime Object value to the array
val dateTime = LocalDateTime.now()
if (!allTimeStampsAccel.contains(dateTime.format(DateTimeFormatter.ofPattern("YYYY-MM-d hh:mm:ss")))) {
allTimeStampsAccel.add(dateTime.format(DateTimeFormatter.ofPattern("YYYY-MM-d hh:mm:ss")))
if (!allTimeStamps.contains(dateTime.format(DateTimeFormatter.ofPattern("YYYY-MM-d hh:mm:ss")))) {
allTimeStamps.add(dateTime.format(DateTimeFormatter.ofPattern("YYYY-MM-d hh:mm:ss")))
}
var pair1 = Pair<Float, String>(
accelSensorTriplets.get(0),
allTimeStampsAccel.get(allTimeStampsAccel.size - 1)
)
var pair2 = Pair<Float, String>(
accelSensorTriplets.get(1),
allTimeStampsAccel.get(allTimeStampsAccel.size - 1)
)
var pair3 = Pair<Float, String>(
accelSensorTriplets.get(2),
allTimeStampsAccel.get(allTimeStampsAccel.size - 1)
)
var pairArrayList = ArrayList<Pair<Float, String>>(3)
pairArrayList.add(pair1)
pairArrayList.add(pair2)
pairArrayList.add(pair3)
accelerometerAllValuesForStore.add(pairArrayList)
Log.d(
"DATA",
"accel data with timestamp : ${allTimeStampsAccel.get(allTimeStampsAccel.size - 1)}"
)
}
//add the data for the accelerometer chart
Log.d("SQL" , "ACCEL ${allTimeStampsAccel.get(allTimeStampsAccel.size-1)}")
}
override fun onAccuracyChanged(sensor: Sensor?, accuracy: Int) {
}
}
Any help on why this behaviour is occuring is appreciated.
Lampros
On a additional note, the onDestroy method of the service is immediatelly called witin the service execution...here is the code from the onStartCommand of the service:
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
super.onStartCommand(intent, flags, startId)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
if (Build.VERSION.SDK_INT > Build.VERSION_CODES.O)
startMyOwnForeground()
else
startForeground(1, Notification()) }
// //Power management
// //Power management
val am = applicationContext.getSystemService(ALARM_SERVICE)
val pm = getSystemService(POWER_SERVICE) as PowerManager
wl = pm.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "myapp:mywakelocktag")
wl!!.acquire()
SetAlarm(applicationContext)
sensorManager = getSystemService(Context.SENSOR_SERVICE) as SensorManager
initSensors()
registerListeners()
return START_STICKY
}
Why does the onDestroy gets called instantly?

Android ForegroundService for background location

From Android 11 I learned there are some restrictions related to background location, but from the documentation is not very clear to me if this affects a ForegroundService which has the foregroundServiceType="location" declared in the AndroidManifest.xml file.
This part of the documentation is confusing for me:
"If your app starts a foreground service while the app is running in
the foreground ("while-in-use"), the service has the following access
restrictions:
If the user has granted the ACCESS_BACKGROUND_LOCATION permission to
your app, the service can access location all the time. Otherwise, if
the user has granted the ACCESS_FINE_LOCATION or
ACCESS_COARSE_LOCATION permission to your app, the service has access
to location only while the app is running in the foreground (also
known as "while-in-use access to location")."
So, if I need background location access is it safe to use only the ForegroundService with type "location" for Android 11 or it is still mandatory to add the ACCESS_BACKGROUND_LOCATION permission?
NOTE: I created a sample project with ForegroundService declared with type "location" for target SDK 30 and seems to work without the background location permission (I receive the location updates every 2 seconds while in background) and this is why I am confused about this. I run the app on Pixel 4 with Android 11.
This is the sample project:
AndroidManifest.xml
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.locationforegroundservice">
<uses-permission android:name="android.permission.ACCESS_FINE_LOCATION" />
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
<application
android:allowBackup="true"
android:icon="#mipmap/ic_launcher"
android:label="#string/app_name"
android:roundIcon="#mipmap/ic_launcher_round"
android:supportsRtl="true"
android:theme="#style/Theme.LocationForegroundService">
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
</activity>
<service
android:name=".LocationService"
android:enabled="true"
android:exported="true"
android:foregroundServiceType="location"/>
</application>
</manifest>
LocationService
class LocationService : Service() {
private var context: Context? = null
private var settingsClient: SettingsClient? = null
private var locationSettingsRequest: LocationSettingsRequest? = null
private var locationManager: LocationManager? = null
private var locationRequest: LocationRequest? = null
private var notificationManager: NotificationManager? = null
private var fusedLocationClient: FusedLocationProviderClient? = null
private val binder: IBinder = LocalBinder()
private var locationCallback: LocationCallback? = null
private var location: Location? = null
override fun onBind(intent: Intent?): IBinder {
// Called when a client (MainActivity in case of this sample) comes to the foreground
// and binds with this service. The service should cease to be a foreground service
// when that happens.
Log.i(TAG, "in onBind()")
return binder
}
override fun onCreate() {
super.onCreate()
context = this
fusedLocationClient = LocationServices.getFusedLocationProviderClient(this)
createLocationRequest()
locationCallback = object : LocationCallback() {
#RequiresApi(Build.VERSION_CODES.O)
override fun onLocationResult(locationResult: LocationResult) {
super.onLocationResult(locationResult)
for (location in locationResult.locations) {
onNewLocation(location)
}
}
}
val handlerThread = HandlerThread(TAG)
handlerThread.start()
notificationManager = getSystemService(NOTIFICATION_SERVICE) as NotificationManager?
// Android O requires a Notification Channel.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val name: CharSequence = "service"
val mChannel = NotificationChannel(CHANNEL_ID, name, NotificationManager.IMPORTANCE_DEFAULT)
// Set the Notification Channel for the Notification Manager.
notificationManager?.createNotificationChannel(mChannel)
}
}
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
Log.i(TAG, "Service started")
val startedFromNotification =
intent?.getBooleanExtra(EXTRA_STARTED_FROM_NOTIFICATION, false)
// We got here because the user decided to remove location updates from the notification.
if (startedFromNotification == true) {
removeLocationUpdates()
stopSelf()
}
// Tells the system to not try to recreate the service after it has been killed.
return START_NOT_STICKY
}
/**
* Returns the [NotificationCompat] used as part of the foreground service.
*/
private val notification: Notification
private get() {
val intent = Intent(this, LocationService::class.java)
// Extra to help us figure out if we arrived in onStartCommand via the notification or not.
intent.putExtra(EXTRA_STARTED_FROM_NOTIFICATION, true)
// The PendingIntent that leads to a call to onStartCommand() in this service.
val servicePendingIntent =
PendingIntent.getService(this, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
// The PendingIntent to launch activity.
val activityPendingIntent =
PendingIntent.getActivity(this, 0, Intent(this, MainActivity::class.java), 0)
val builder = NotificationCompat.Builder(this)
.addAction(R.drawable.ic_delete, "title", activityPendingIntent)
.addAction(R.drawable.ic_delete, "remove", servicePendingIntent)
.setContentTitle("location title").setOngoing(true)
.setPriority(Notification.PRIORITY_HIGH).setSmallIcon(R.drawable.btn_dialog)
.setWhen(System.currentTimeMillis())
// Set the Channel ID for Android O.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
builder.setChannelId(CHANNEL_ID) // Channel ID
}
return builder.build()
}
/**
* Makes a request for location updates. Note that in this sample we merely log the
* [SecurityException].
*/
fun requestLocationUpdates() {
Log.i(TAG, "Requesting location updates")
startForeground(NOTIFICATION_ID, notification)
try {
fusedLocationClient?.requestLocationUpdates(locationRequest, locationCallback, null)
} catch (unlikely: SecurityException) {
Log.e(TAG, "Lost location permission. Could not request updates. $unlikely")
}
}
#RequiresApi(Build.VERSION_CODES.O)
private fun onNewLocation(location: Location) {
Log.i(TAG, "New location ${LocalDateTime.now()}: $location")
this.location = location
// Notify anyone listening for broadcasts about the new location.
val intent = Intent(ACTION_BROADCAST)
intent.putExtra(EXTRA_LOCATION, location)
LocalBroadcastManager.getInstance(applicationContext).sendBroadcast(intent)
// Update notification content if running as a foreground service.
if (serviceIsRunningInForeground(this)) {
notificationManager?.notify(NOTIFICATION_ID, notification)
}
}
/**
* Sets the location request parameters.
*/
private fun createLocationRequest() {
locationManager = context?.getSystemService(LOCATION_SERVICE) as LocationManager
settingsClient = LocationServices.getSettingsClient(context)
locationRequest = LocationRequest.create()
locationRequest?.priority = LocationRequest.PRIORITY_HIGH_ACCURACY
locationRequest?.interval = 1000
locationRequest?.fastestInterval = 1000
val builder = LocationSettingsRequest.Builder().addLocationRequest(locationRequest)
locationSettingsRequest = builder.build()
builder.setAlwaysShow(true) //this is the key ingredient
}
/**
* Removes location updates. Note that in this sample we merely log the
* [SecurityException].
*/
fun removeLocationUpdates() {
Log.i(TAG, "Removing location updates")
try {
fusedLocationClient?.removeLocationUpdates(locationCallback)
stopSelf()
} catch (unlikely: SecurityException) {
Log.e(TAG, "Lost location permission. Could not remove updates. $unlikely")
}
}
/**
* Class used for the client Binder. Since this service runs in the same process as its
* clients, we don't need to deal with IPC.
*/
inner class LocalBinder : Binder() {
val service: LocationService
get() = this#LocationService
}
/**
* Returns true if this is a foreground service.
*
* #param context The [Context].
*/
fun serviceIsRunningInForeground(context: Context): Boolean {
val manager = context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
for (service in manager.getRunningServices(Int.MAX_VALUE)) {
if (javaClass.name == service.service.className) {
if (service.foreground) {
return true
}
}
}
return false
}
companion object {
private const val PACKAGE_NAME = "com.example.locationforegroundservice"
private val TAG = "TEST"
/**
* The name of the channel for notifications.
*/
private const val CHANNEL_ID = "channel_01"
const val ACTION_BROADCAST = PACKAGE_NAME + ".broadcast"
const val EXTRA_LOCATION = PACKAGE_NAME + ".location"
private const val EXTRA_STARTED_FROM_NOTIFICATION =
PACKAGE_NAME + ".started_from_notification"
/**
* The desired interval for location updates. Inexact. Updates may be more or less frequent.
*/
private const val UPDATE_INTERVAL_IN_MILLISECONDS: Long = 1000
/**
* The fastest rate for active location updates. Updates will never be more frequent
* than this value.
*/
private const val FASTEST_UPDATE_INTERVAL_IN_MILLISECONDS =
UPDATE_INTERVAL_IN_MILLISECONDS / 2
/**
* The identifier for the notification displayed for the foreground service.
*/
private const val NOTIFICATION_ID = 12345678
}
MainActivity
class MainActivity : AppCompatActivity() {
private val TAG = "TEST"
private val FOREGROUND_LOCATION_CODE = 2
// The BroadcastReceiver used to listen from broadcasts from the service.
private var myReceiver: MyReceiver? = null
// A reference to the service used to get location updates.
private var mService: LocationService? = null
// Monitors the state of the connection to the service.
private val mServiceConnection: ServiceConnection = object : ServiceConnection {
override fun onServiceConnected(name: ComponentName, service: IBinder) {
val binder: LocationService.LocalBinder = service as LocationService.LocalBinder
mService = binder.service
}
override fun onServiceDisconnected(name: ComponentName) {
mService = null
}
}
#RequiresApi(Build.VERSION_CODES.M)
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
checkForegroundLocationPermission()
myReceiver = MyReceiver()
myReceiver?.let {
LocalBroadcastManager.getInstance(this)
.registerReceiver(it, IntentFilter(LocationService.ACTION_BROADCAST))
}
findViewById<Button>(R.id.start).setOnClickListener { view ->
Snackbar.make(view, "Start listening...", Snackbar.LENGTH_LONG).show()
Log.d("TEST", "Start listening...")
mService?.requestLocationUpdates();
}
findViewById<Button>(R.id.stop).setOnClickListener { view ->
Snackbar.make(view, "Stop listening...", Snackbar.LENGTH_LONG).show()
Log.d("TEST", "Stop listening...")
mService?.removeLocationUpdates()
}
}
override fun onStart() {
super.onStart()
// Bind to the service. If the service is in foreground mode, this signals to the service
// that since this activity is in the foreground, the service can exit foreground mode.
// Bind to the service. If the service is in foreground mode, this signals to the service
// that since this activity is in the foreground, the service can exit foreground mode.
Intent(this, LocationService::class.java).also {
bindService(it, mServiceConnection, BIND_AUTO_CREATE)
}
}
override fun onResume() {
super.onResume()
Log.d(TAG, "onResume")
}
override fun onStop() {
Log.d(TAG, "onStop")
super.onStop()
}
#RequiresApi(Build.VERSION_CODES.M)
private fun checkForegroundLocationPermission() {
if (ContextCompat.checkSelfPermission(this, Manifest.permission.ACCESS_FINE_LOCATION) != PackageManager.PERMISSION_GRANTED) {
// Check if permission is not granted
Log.d(TAG, "Permission for foreground location is not granted")
requestPermissions(arrayOf(Manifest.permission.ACCESS_FINE_LOCATION),
FOREGROUND_LOCATION_CODE)
} else {
// Permission is already granted, do your magic here!
Toast.makeText(this, "Permission granted", Toast.LENGTH_SHORT).show()
}
}
#RequiresApi(Build.VERSION_CODES.Q)
override fun onRequestPermissionsResult(requestCode: Int,
permissions: Array<out String>,
grantResults: IntArray) {
when (requestCode) {
FOREGROUND_LOCATION_CODE -> {
Log.d(TAG, "onRequestPermissionsResult -> FOREGROUND_LOCATION_CODE")
if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
Toast.makeText(this, "Foreground Permission granted", Toast.LENGTH_SHORT).show()
} else {
Toast.makeText(this, "Foreground Permission denied", Toast.LENGTH_SHORT).show()
}
return
}
}
}
private class MyReceiver : BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
val location: Location? = intent.getParcelableExtra(LocationService.EXTRA_LOCATION)
if (location != null) {
Log.d("TEST", "Location = $location")
}
}
}
}

Android Kotlin Service issue

I have implemented a service that runs in the background of the application. The service calls the function every minute, which checks if the time has come to trigger the notification - reminder (retrieves the current date and time and compares it with the data saved in the database. If the date and time match, the notification is triggered. The function is called every 1 minute. Function this one must check every 1 minute the condition when the user uses the application, the application runs in the background, does not use the application and how the phone is asleep. By implementing reminders the previous day evening to the evening and the next day. Reminders appear for the same day for the evening and the first reminder for the next day. The next ones do not appear. On the other hand, the reminder set for 5 minutes works and for 30 no longer. Looking for information I found on the internet that the problem lies in:
time drift - When the screen is on and the phone is fully awake, the interval between two consecutive events will remain constant most of the time, but may jump from time to time (lengthen)
the phone goes into sleep mode
Any of you have an idea how to solve this, get around it?
ForegroundService
class ForegroundService : Service() {
companion object {
val CHANNEL_ID = "ForegroundServiceChannel"
val CHANNEL_ID_CHILD = "ForegroundServiceChannelCHILD"
private var isRunning = true
}
override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
val input = intent.getIntExtra("time",15)
createNotificationChannel()
val notificationIntent = Intent(this, Menu::class.java)
val pendingIntent = PendingIntent.getActivity(
this,
0, notificationIntent, 0
)
val wakeLock: PowerManager.WakeLock =
(getSystemService(Context.POWER_SERVICE) as PowerManager).run {
newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "MyApp::AR_Apteczka").apply {
acquire()
}
}
val notification = NotificationCompat.Builder(this, CHANNEL_ID)
.setContentTitle("Twoja Elektroniczna Apteczka")
.setContentText("Dbamy o Twoje zdrowie")
.setSmallIcon(com.example.agnieszka.ar_apteczka.R.drawable.pills)
.setOnlyAlertOnce(true)
//.setContentIntent(pendingIntent)
//.setSound(null)
.build()
startForeground(1, notification)
isRunning = true
val context = this
val intent = Intent(this, ShowAllTodaysMedicines::class.java)
val pendingIntentNotification = PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
doAsync {
while(true)
{
var message= createReminderMessage(context)
uiThread {
if(true) {
if (message != "Nadszedł czas by zażyć: ") {
val notification =
NotificationCompat.Builder(context, CHANNEL_ID_CHILD)
.setContentTitle("Zażyj leki")
.setContentText(message)
.setSmallIcon(com.example.agnieszka.ar_apteczka.R.drawable.pills)
.setContentIntent(pendingIntentNotification)
.setAutoCancel(true)
.build()
with(NotificationManagerCompat.from(context)) {
notificationManager.notify(2, notification)
}
}
}
}
//SystemClock.sleep(60000)
SystemClock.sleep(60*1000-SystemClock.elapsedRealtime()%1000)
// SystemClock.sleep(50000) //10*1000-SystemClock.elapsedRealtime()%1000
}
}
return START_NOT_STICKY
}
override fun onBind(intent: Intent): IBinder? {
return null
}
override fun onDestroy() {
super.onDestroy()
isRunning = false
}
private fun createNotificationChannel() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val serviceChannel = NotificationChannel(
CHANNEL_ID,
"Foreground Service Channel",
//NotificationManager.IMPORTANCE_DEFAULT
NotificationManager.IMPORTANCE_DEFAULT
)
//serviceChannel.setSound(null, null) //
val serviceChannel2 = NotificationChannel(
CHANNEL_ID_CHILD,
"Foreground Service ChannelChild ",
NotificationManager.IMPORTANCE_DEFAULT
//NotificationManager.IMPORTANCE_LOW
)
val manager = getSystemService(NotificationManager::class.java)
manager.createNotificationChannel(serviceChannel)
manager.createNotificationChannel(serviceChannel2)
}
}
fun reminderForNow(context: Context) : ArrayList<Reminder> {
var listOfReminder : ArrayList<Reminder> = ArrayList()
var timetoday = takeTimeNow()
var dateToday = takeTodayDate()
val dbHelper = SQLConector(context)
val allRemindersList = dbHelper.getAllReminders()
for (i: Reminder in allRemindersList) {
if (i.reminderDate == dateToday && i.ReminderTime == timetoday) {
var reminder = Reminder(
i.idReminder,
i.medicineName,
i.reminderDate,
i.ReminderTime
)
listOfReminder.add(reminder)
}
}
return listOfReminder
}
private fun createReminderMessage(p0: Context) : String{
var message = "Nadszedł czas by zażyć: "
var listOfReminders = reminderForNow(p0)
if(listOfReminders.count() > 0){
for (i: Reminder in listOfReminders) {
message += i.medicineName + ", "
}
}
return message
}
private fun takeTodayDate():String{
val current = LocalDateTime.now()
val formatDate = DateTimeFormatter.ofPattern("yyyy-MM-dd")
var currentDate = current.format(formatDate).toString()
return currentDate
}
private fun takeTimeNow() : String{
val current = LocalDateTime.now()
val formatTime = DateTimeFormatter.ofPattern("HH:mm")
return current.format(formatTime).toString()
}
}
**Main Activity**
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val binding = DataBindingUtil.setContentView<ActivityMainBinding>(this, R.layout.activity_main)
binding.buttonStart.setOnClickListener { startService() }
binding.buttonStop.setOnClickListener {stopService() }
startService()
}
private fun startService() {
val serviceIntent = Intent(this, ForegroundService::class.java)
serviceIntent.putExtra("time", 1)
ContextCompat.startForegroundService(this, serviceIntent)
}
private fun stopService() {
val serviceIntent = Intent(this, ForegroundService::class.java)
stopService(serviceIntent)
}
}
*Manifest*
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<application
android:allowBackup="false"
android:icon="#drawable/pills"
android:label="#string/nameOfApplications"
android:roundIcon="#drawable/icon"
android:supportsRtl="true"
android:theme="#style/AppTheme">
<service
android:name=".ForegroundService"
android:enabled="true"
android:exported="true"/>
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
</application>

Android Kotlin Foregeground Service + Notifications, Why works wrong?

In my application, I need a foreground service that will check a certain condition every minute, and if it is correct, it triggers a notification reminder. The user determines in advance what time and day he would like to have the reminder to. Data is saved in the database. Then the service every minute checks if it has a reminder for a given hour and day and if so sends a notification. The service must work when the user uses the application, when the application runs in the background and when it is closed. Could someone tell me why this code works on one phone but not on others? So-called, if I set a reminder, up to 20 minutes, it works (in all 3 states that I wrote about earlier), but once I set the reminder for the next days, it doesn't work anymore. I am surprised that sometimes the reminder on another phone works and sometimes it doesn't. I checked, the permission for the application to run in the background is selected in the settings. Please help.
Manifest
<uses-permission android:name="android.permission.RECEIVE_BOOT_COMPLETED" />
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<application
android:allowBackup="false"
android:icon="#drawable/pills"
android:label="#string/nameOfApplications"
android:roundIcon="#drawable/icon"
android:supportsRtl="true"
android:theme="#style/AppTheme">
<service
android:name=".ForegroundService"
android:enabled="true"
android:exported="true"/>
<activity android:name=".MainActivity">
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
</application>
ForegroundService
class ForegroundService : Service() {
companion object {
val CHANNEL_ID = "ForegroundServiceChannel"
val CHANNEL_ID_CHILD = "ForegroundServiceChannelCHILD"
private var isRunning = false
}
override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
val input = intent.getIntExtra("time",15)
createNotificationChannel()
val notificationIntent = Intent(this, Menu::class.java)
val pendingIntent = PendingIntent.getActivity(
this,
0, notificationIntent, 0
)
val notification = NotificationCompat.Builder(this, CHANNEL_ID)
.setContentTitle("MotherNotification")
.setContentText("Message")
.setOnlyAlertOnce(true)
.build()
startForeground(1, notification)
isRunning = true
val context = this
val intent = Intent(this, ShowAll::class.java)
val pendingIntentNotification = PendingIntent.getActivity(context, 0, intent, PendingIntent.FLAG_UPDATE_CURRENT)
doAsync {
while(isRunning)
{
var message : String = createReminderMessage(context)
//SystemClock.sleep(input * 10_000L)
SystemClock.sleep(50000)
uiThread {
if(isRunning && (message != "Nadszedł czas by zażyć: ")) {
val notification = NotificationCompat.Builder(context, CHANNEL_ID_CHILD)
.setContentTitle("Title")
.setContentText(message)
.setContentIntent(pendingIntentNotification)
.setAutoCancel(true)
.build()
with(NotificationManagerCompat.from(context)) {
notificationManager.notify(2, notification)
}
}
}
}
}
return START_NOT_STICKY
}
override fun onBind(intent: Intent): IBinder? {
return null
}
override fun onDestroy() {
super.onDestroy()
isRunning = false
}
private fun createNotificationChannel() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
val serviceChannel = NotificationChannel(
CHANNEL_ID,
"Foreground Service Channel",
NotificationManager.IMPORTANCE_DEFAULT
)
val serviceChannel2 = NotificationChannel(
CHANNEL_ID_CHILD,
"Foreground Service ChannelChild ",
NotificationManager.IMPORTANCE_DEFAULT
//NotificationManager.IMPORTANCE_LOW
)
val manager = getSystemService(NotificationManager::class.java)
manager.createNotificationChannel(serviceChannel)
manager.createNotificationChannel(serviceChannel2)
}
}
fun reminderForNow(context: Context) : ArrayList<Reminder> {
var listOfReminder : ArrayList<Reminder> = ArrayList()
var timetoday = takeTimeNow()
var dateToday = takeTodayDate()
val dbHelper = SQLConector(context)
val allRemindersList = dbHelper.getAllReminders()
for (i: Reminder in allRemindersList) {
if (i.reminderDate == dateToday && i.ReminderTime == timetoday) {
var reminder = Reminder(
i.id,
i.Name,
i.reminderDate,
i.ReminderTime
)
listOfReminder.add(reminder)
}
}
return listOfReminder
}
private fun createReminderMessage(p0: Context) : String{
var message : String = "title : "
var listOfReminders = reminderForNow(p0)
if(listOfReminders.count() > 0){
for (i: Reminder in listOfReminders) {
message += i.Name + ", "
}
}
return message
}
private fun takeTodayDate():String{
val current = LocalDateTime.now()
val formatDate = DateTimeFormatter.ofPattern("yyyy-MM-dd")
var dateResult = current.format(formatDate).toString()
return dateResult
}
private fun takeTimeNow() : String{
val current = LocalDateTime.now()
val formatTime = DateTimeFormatter.ofPattern("HH:mm")
var timeResult = current.format(formatTime).toString()
return timeResult
}
}
Main Activity
class MainActivity : AppCompatActivity() {
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
val binding = DataBindingUtil.setContentView<ActivityMainBinding>(this, R.layout.activity_main)
binding.buttonStart.setOnClickListener { startService() }
binding.buttonStop.setOnClickListener {stopService() }
startService()
}
private fun startService() {
val serviceIntent = Intent(this, ForegroundService::class.java)
serviceIntent.putExtra("time", 1)
ContextCompat.startForegroundService(this, serviceIntent)
}
private fun stopService() {
val serviceIntent = Intent(this, ForegroundService::class.java)
stopService(serviceIntent)
}
}
The correct way to handle tasks which require exact fire time is to use the AlarmManagerCompat class.
You can use setExactAndAllowWhileIdle(...) method to force the alarm to start your service even when the device is in Doze mode and you will need a BroadcastReceiver to re-schedule the alarms if the device is rebooted.
You can find some references online on how to implement that.

how to run location tracking service after screen is off?

location can't update when screen is off.
How to run location tracking service after screen is off ?
I start service using
val serviceClass = ControlLocationService::class.java
val intent = Intent(activity, serviceClass)
activity?.startService(intent)
In onLocationChanged method I try Log.e()
but not show latitude and longitude in logcat when screen is off
class ControlLocationService : Service() {
lateinit var locationRequest: LocationRequest
lateinit var fusedLocationProviderClient: FusedLocationProviderClient
private lateinit var locationCallback:LocationCallback
override fun onBind(intent: Intent): IBinder? {
throw UnsupportedOperationException("Not yet implemented")
}
#SuppressLint("InvalidWakeLockTag")
override fun onStartCommand(intent: Intent, flags: Int, startId: Int): Int {
//service is started
updateLocation()
return START_STICKY
}
#SuppressLint("MissingPermission")
private fun updateLocation() {
locationCallback = object : LocationCallback() {
override fun onLocationResult(locationResult: LocationResult?) {
onLocationChanged(locationResult!!.lastLocation)
}
}
buildLocationRequest()
fusedLocationProviderClient = LocationServices.getFusedLocationProviderClient(this)
fusedLocationProviderClient.requestLocationUpdates(locationRequest, locationCallback, Looper.myLooper())
}
private fun onLocationChanged(location: Location) {
Log.e("...", location.latitude.toString()+location.longitude.toString())
}
private fun buildLocationRequest() {
locationRequest = LocationRequest()
locationRequest.priority = LocationRequest.PRIORITY_HIGH_ACCURACY
locationRequest.interval = 3000
locationRequest.fastestInterval = 1000
//locationRequest.smallestDisplacement = 1f
}
}
Help me introduce about service management when the screen is off.
thank you.
You will need a wakelock and I would also make your service foreground. This will make android less likely to kill your app or service. Actually according to Background Location Limits its not said that you need a wakelock - only foreground service. By making your app work continously in the background, you will make it quickly drain your user battery - which is bad. Consider using passive location updates, those might happen few time in a hour - which might not be what you are after.
You might also want to read about doze mode which makes all non white-listed applications ignore wake locks while in screen off mode.
To add wake lock:
val mgr = service.getSystemService(Context.POWER_SERVICE) as PowerManager
val wakeLock = mgr.newWakeLock(PowerManager.PARTIAL_WAKE_LOCK, "myAppName:MyWakeLock")
wakeLock.acquire()
in onDestroy:
wakeLock.release()
Use below code to add notification to notification bar and make service foreground. You should add addServiceNotification(this) in your onStartCommand.
fun addServiceNotification(service: Service) {
val mgr = NotificationManagerCompat.from(service)
val title = "Caption"
val text = "Location"
if (Build.VERSION.SDK_INT >= 26) {
val notificationManager = service.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
if (notificationManager != null) {
var mChannel: NotificationChannel? = notificationManager.getNotificationChannel(NOTIFICATION_CHANNEL_ID)
if (mChannel == null) {
mChannel = NotificationChannel(NOTIFICATION_CHANNEL_ID, "myAppName", NotificationManager.IMPORTANCE_LOW)
notificationManager.createNotificationChannel(mChannel)
}
}
val notification = NotificationCompat.Builder(service, NOTIFICATION_CHANNEL_ID)
.setSmallIcon(R.drawable.app_icon)
.setTicker(title)
.setWhen(System.currentTimeMillis())
.setContentTitle(title)
.setContentText(text)
.setOnlyAlertOnce(true)
.setOngoing(true)
.build()
service.startForeground(LOCATION_NOTIFICATION_ID, notification)
mgr.cancel(LOCATION_NOTIFICATION_ID)
mgr.notify(LOCATION_NOTIFICATION_ID, notification)
}
}
// remove notification for location service
fun removeServiceNotification(service: Service) {
val notificationManager = service.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
notificationManager?.cancel(LOCATION_NOTIFICATION_ID)
}
companion object {
val LOCATION_NOTIFICATION_ID = 1001
val NOTIFICATION_CHANNEL_ID = "myAppName"
}

Categories

Resources