@capacitor/background-runner 1.0.0-rc.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CapacitorBackgroundRunner.podspec +17 -0
- package/README.md +310 -0
- package/android/build.gradle +67 -0
- package/android/src/main/AndroidManifest.xml +12 -0
- package/android/src/main/java/io/ionic/backgroundrunner/plugin/BackgroundRunner.kt +166 -0
- package/android/src/main/java/io/ionic/backgroundrunner/plugin/BackgroundRunnerPlugin.kt +102 -0
- package/android/src/main/java/io/ionic/backgroundrunner/plugin/CoroutineHelper.kt +21 -0
- package/android/src/main/java/io/ionic/backgroundrunner/plugin/RunnerConfig.kt +33 -0
- package/android/src/main/java/io/ionic/backgroundrunner/plugin/RunnerWorker.kt +46 -0
- package/android/src/main/java/io/ionic/backgroundrunner/plugin/api/Device.kt +99 -0
- package/android/src/main/java/io/ionic/backgroundrunner/plugin/api/Geolocation.kt +55 -0
- package/android/src/main/java/io/ionic/backgroundrunner/plugin/api/KV.kt +29 -0
- package/android/src/main/java/io/ionic/backgroundrunner/plugin/api/NetworkStatus.kt +10 -0
- package/android/src/main/java/io/ionic/backgroundrunner/plugin/api/Notification.kt +59 -0
- package/android/src/main/java/io/ionic/backgroundrunner/plugin/api/Notifications.kt +158 -0
- package/android/src/main/java/io/ionic/backgroundrunner/plugin/api/TimedNotificationPublisher.kt +40 -0
- package/android/src/main/libs/android-js-engine-release.aar +0 -0
- package/android/src/main/res/.gitkeep +0 -0
- package/dist/docs.json +208 -0
- package/dist/esm/apis.d.ts +52 -0
- package/dist/esm/apis.js +2 -0
- package/dist/esm/apis.js.map +1 -0
- package/dist/esm/definitions.d.ts +82 -0
- package/dist/esm/definitions.js +3 -0
- package/dist/esm/definitions.js.map +1 -0
- package/dist/esm/index.d.ts +5 -0
- package/dist/esm/index.js +8 -0
- package/dist/esm/index.js.map +1 -0
- package/dist/esm/web.d.ts +8 -0
- package/dist/esm/web.js +16 -0
- package/dist/esm/web.js.map +1 -0
- package/dist/plugin.cjs.js +32 -0
- package/dist/plugin.cjs.js.map +1 -0
- package/dist/plugin.js +35 -0
- package/dist/plugin.js.map +1 -0
- package/ios/Plugin/BackgroundRunner.swift +223 -0
- package/ios/Plugin/BackgroundRunnerErrors.swift +8 -0
- package/ios/Plugin/BackgroundRunnerPlugin.h +10 -0
- package/ios/Plugin/BackgroundRunnerPlugin.m +11 -0
- package/ios/Plugin/BackgroundRunnerPlugin.swift +161 -0
- package/ios/Plugin/CapacitorAPI/Device.swift +116 -0
- package/ios/Plugin/CapacitorAPI/Geolocation.swift +190 -0
- package/ios/Plugin/CapacitorAPI/KV.swift +22 -0
- package/ios/Plugin/CapacitorAPI/Notifications.swift +165 -0
- package/ios/Plugin/CapacitorAPI/Reachability.swift +395 -0
- package/ios/Plugin/CapacitorAPI/Wearable.swift +48 -0
- package/ios/Plugin/Context+CapacitorAPI.swift +11 -0
- package/ios/Plugin/Info.plist +24 -0
- package/ios/Plugin/RunnerConfig.swift +36 -0
- package/ios/Plugin/RunnerEngine/Context.swift +151 -0
- package/ios/Plugin/RunnerEngine/Errors.swift +8 -0
- package/ios/Plugin/RunnerEngine/JSConsole.swift +38 -0
- package/ios/Plugin/RunnerEngine/JSCrypto.swift +38 -0
- package/ios/Plugin/RunnerEngine/JSError.swift +33 -0
- package/ios/Plugin/RunnerEngine/JSFetch.swift +50 -0
- package/ios/Plugin/RunnerEngine/JSResponse.swift +69 -0
- package/ios/Plugin/RunnerEngine/JSTextDecoder.swift +60 -0
- package/ios/Plugin/RunnerEngine/JSTextEncoder.swift +12 -0
- package/ios/Plugin/RunnerEngine/Runner.swift +23 -0
- package/ios/Plugin/WatchConnectivtySessionDelegate.swift +34 -0
- package/package.json +81 -0
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
package io.ionic.backgroundrunner.plugin
|
|
2
|
+
|
|
3
|
+
import kotlinx.coroutines.flow.StateFlow
|
|
4
|
+
import kotlinx.coroutines.flow.first
|
|
5
|
+
|
|
6
|
+
class ConditionalAwait<T>(
|
|
7
|
+
private val stateFlow: StateFlow<T>,
|
|
8
|
+
private val condition: (T) -> Boolean
|
|
9
|
+
) {
|
|
10
|
+
suspend fun await(): T {
|
|
11
|
+
val nowValue = stateFlow.value
|
|
12
|
+
return if (condition(nowValue)) {
|
|
13
|
+
nowValue
|
|
14
|
+
} else {
|
|
15
|
+
stateFlow.first { condition(it) }
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
suspend fun <T> StateFlow<T>.conditionalAwait(condition: (T) -> Boolean): T =
|
|
21
|
+
ConditionalAwait(this, condition).await()
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
package io.ionic.backgroundrunner.plugin;
|
|
2
|
+
|
|
3
|
+
import org.json.JSONObject
|
|
4
|
+
import java.lang.Exception
|
|
5
|
+
|
|
6
|
+
class RunnerConfig(configObject: JSONObject) {
|
|
7
|
+
val label: String
|
|
8
|
+
val src: String
|
|
9
|
+
val autoSchedule: Boolean
|
|
10
|
+
val repeats: Boolean
|
|
11
|
+
var event: String
|
|
12
|
+
val interval: Int?
|
|
13
|
+
|
|
14
|
+
init {
|
|
15
|
+
val labelStr = configObject.getString("label")
|
|
16
|
+
if (labelStr.isNullOrEmpty()) {
|
|
17
|
+
throw Exception("runner label is missing or invalid")
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
val srcStr = configObject.getString("src")
|
|
21
|
+
if (srcStr.isNullOrEmpty()) {
|
|
22
|
+
throw Exception("runner source file path is missing or invalid")
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
label = labelStr
|
|
26
|
+
src = srcStr
|
|
27
|
+
autoSchedule = configObject.optBoolean("autoStart", false)
|
|
28
|
+
event = configObject.optString("event")
|
|
29
|
+
repeats = configObject.optBoolean("repeat")
|
|
30
|
+
interval = configObject.optInt("interval", 0)
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
package io.ionic.backgroundrunner.plugin;
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import androidx.work.Data
|
|
5
|
+
import androidx.work.Worker
|
|
6
|
+
import androidx.work.WorkerParameters
|
|
7
|
+
import kotlinx.coroutines.flow.MutableStateFlow
|
|
8
|
+
import kotlinx.coroutines.runBlocking
|
|
9
|
+
import org.json.JSONObject
|
|
10
|
+
|
|
11
|
+
class RunnerWorker(context: Context, workerParams: WorkerParameters) : Worker(context, workerParams) {
|
|
12
|
+
override fun doWork(): Result {
|
|
13
|
+
try {
|
|
14
|
+
val label = this.inputData.getString("label") ?: ""
|
|
15
|
+
val src = this.inputData.getString("src") ?: ""
|
|
16
|
+
val event = this.inputData.getString("event") ?: ""
|
|
17
|
+
|
|
18
|
+
if (label.isEmpty() || src.isEmpty() || event.isEmpty()) {
|
|
19
|
+
throw Exception("label is empty")
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
val runnerConfigObj = JSONObject()
|
|
23
|
+
runnerConfigObj.put("label", label)
|
|
24
|
+
runnerConfigObj.put("src", src)
|
|
25
|
+
runnerConfigObj.put("event", event)
|
|
26
|
+
runnerConfigObj.put("autoStart", false)
|
|
27
|
+
runnerConfigObj.put("repeats", false)
|
|
28
|
+
runnerConfigObj.put("interval", 0)
|
|
29
|
+
|
|
30
|
+
val config = RunnerConfig(runnerConfigObj)
|
|
31
|
+
|
|
32
|
+
runBlocking {
|
|
33
|
+
val impl = BackgroundRunner.getInstance(this@RunnerWorker.applicationContext)
|
|
34
|
+
impl.execute(this@RunnerWorker.applicationContext, config, JSONObject())
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return Result.success()
|
|
38
|
+
} catch (ex: Exception) {
|
|
39
|
+
val data = Data.Builder()
|
|
40
|
+
.putString("error", ex.toString())
|
|
41
|
+
.build()
|
|
42
|
+
|
|
43
|
+
return Result.failure(data)
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
package io.ionic.backgroundrunner.plugin.api
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import android.content.Intent
|
|
5
|
+
import android.content.IntentFilter
|
|
6
|
+
import android.net.ConnectivityManager
|
|
7
|
+
import android.net.Network
|
|
8
|
+
import android.net.NetworkCapabilities
|
|
9
|
+
import android.os.BatteryManager
|
|
10
|
+
import android.os.Build
|
|
11
|
+
import io.ionic.android_js_engine.api.DeviceAPI
|
|
12
|
+
import org.json.JSONObject
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class Device (context: android.content.Context) : DeviceAPI {
|
|
16
|
+
private val context: android.content.Context
|
|
17
|
+
private val connectivityManager: ConnectivityManager
|
|
18
|
+
|
|
19
|
+
init {
|
|
20
|
+
this.context = context
|
|
21
|
+
this.connectivityManager = this.context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
override fun getBatteryStatus(): String {
|
|
25
|
+
val batteryInfo = JSONObject()
|
|
26
|
+
batteryInfo.put("batteryLevel", this.getBatteryLevel())
|
|
27
|
+
batteryInfo.put("isCharging", this.isCharging())
|
|
28
|
+
|
|
29
|
+
return batteryInfo.toString()
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
override fun getNetworkStatus(): String {
|
|
33
|
+
var status = NetworkStatus()
|
|
34
|
+
|
|
35
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
|
|
36
|
+
val activeNetwork: Network? = this.connectivityManager.activeNetwork
|
|
37
|
+
val capabilities: NetworkCapabilities? =
|
|
38
|
+
this.connectivityManager.getNetworkCapabilities(this.connectivityManager.activeNetwork)
|
|
39
|
+
if (activeNetwork != null && capabilities != null) {
|
|
40
|
+
status.connected =
|
|
41
|
+
capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_VALIDATED) &&
|
|
42
|
+
capabilities.hasCapability(NetworkCapabilities.NET_CAPABILITY_INTERNET)
|
|
43
|
+
if (capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI)) {
|
|
44
|
+
status.connectionType = NetworkStatus.ConnectionType.WIFI
|
|
45
|
+
} else if (capabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR)) {
|
|
46
|
+
status.connectionType = NetworkStatus.ConnectionType.CELLULAR
|
|
47
|
+
} else {
|
|
48
|
+
status.connectionType = NetworkStatus.ConnectionType.UNKNOWN
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
} else {
|
|
52
|
+
status = getAndParseNetworkInfo()
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
val networkInfo = JSONObject()
|
|
56
|
+
networkInfo.put("connected", status.connected)
|
|
57
|
+
networkInfo.put("connectionType", status.connectionType)
|
|
58
|
+
|
|
59
|
+
return networkInfo.toString()
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
private fun getBatteryLevel(): Float {
|
|
63
|
+
val ifilter = IntentFilter(Intent.ACTION_BATTERY_CHANGED)
|
|
64
|
+
val batteryStatus = context.registerReceiver(null, ifilter)
|
|
65
|
+
var level = -1
|
|
66
|
+
var scale = -1
|
|
67
|
+
if (batteryStatus != null) {
|
|
68
|
+
level = batteryStatus.getIntExtra(BatteryManager.EXTRA_LEVEL, -1)
|
|
69
|
+
scale = batteryStatus.getIntExtra(BatteryManager.EXTRA_SCALE, -1)
|
|
70
|
+
}
|
|
71
|
+
return level / scale.toFloat()
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
private fun isCharging(): Boolean {
|
|
75
|
+
val ifilter = IntentFilter(Intent.ACTION_BATTERY_CHANGED)
|
|
76
|
+
val batteryStatus = context.registerReceiver(null, ifilter)
|
|
77
|
+
if (batteryStatus != null) {
|
|
78
|
+
val status = batteryStatus.getIntExtra(BatteryManager.EXTRA_STATUS, -1)
|
|
79
|
+
return status == BatteryManager.BATTERY_STATUS_CHARGING || status == BatteryManager.BATTERY_STATUS_FULL
|
|
80
|
+
}
|
|
81
|
+
return false
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
@SuppressWarnings("deprecation")
|
|
85
|
+
private fun getAndParseNetworkInfo(): NetworkStatus {
|
|
86
|
+
val networkStatus = NetworkStatus()
|
|
87
|
+
val networkInfo = connectivityManager.activeNetworkInfo
|
|
88
|
+
if (networkInfo != null) {
|
|
89
|
+
networkStatus.connected = networkInfo.isConnected
|
|
90
|
+
val typeName = networkInfo.typeName
|
|
91
|
+
if (typeName == "WIFI") {
|
|
92
|
+
networkStatus.connectionType = NetworkStatus.ConnectionType.WIFI
|
|
93
|
+
} else if (typeName == "MOBILE") {
|
|
94
|
+
networkStatus.connectionType = NetworkStatus.ConnectionType.CELLULAR
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
return networkStatus
|
|
98
|
+
}
|
|
99
|
+
}
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
package io.ionic.backgroundrunner.plugin.api
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import android.location.Location
|
|
5
|
+
import android.location.LocationManager
|
|
6
|
+
import androidx.core.location.LocationManagerCompat
|
|
7
|
+
import io.ionic.android_js_engine.api.GeolocationAPI
|
|
8
|
+
import org.json.JSONObject
|
|
9
|
+
import java.lang.Exception
|
|
10
|
+
|
|
11
|
+
class Geolocation(context: android.content.Context) : GeolocationAPI {
|
|
12
|
+
val manager: LocationManager
|
|
13
|
+
val context: android.content.Context
|
|
14
|
+
|
|
15
|
+
init {
|
|
16
|
+
this.context = context
|
|
17
|
+
this.manager = context.getSystemService(Context.LOCATION_SERVICE) as LocationManager
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
companion object {
|
|
21
|
+
fun isEnabled(context: Context): Boolean {
|
|
22
|
+
val manager = context.getSystemService(Context.LOCATION_SERVICE) as LocationManager
|
|
23
|
+
return LocationManagerCompat.isLocationEnabled(manager);
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
override fun getCurrentPosition(): String? {
|
|
28
|
+
if (!isEnabled(this.context)) {
|
|
29
|
+
throw Exception("Capacitor Geolocation location services not enabled")
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
var lastLocation: Location? = null
|
|
33
|
+
|
|
34
|
+
this.manager.allProviders.forEach {
|
|
35
|
+
val tempLocation = this.manager.getLastKnownLocation(it)
|
|
36
|
+
if (tempLocation != null) {
|
|
37
|
+
if (lastLocation == null || lastLocation!!.elapsedRealtimeNanos > tempLocation.elapsedRealtimeNanos) {
|
|
38
|
+
lastLocation = tempLocation
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
val location = lastLocation ?: return null
|
|
44
|
+
|
|
45
|
+
val locationInfo = JSONObject();
|
|
46
|
+
locationInfo.put("latitude", location.latitude)
|
|
47
|
+
locationInfo.put("longitude", location.longitude)
|
|
48
|
+
locationInfo.put("accuracy", location.accuracy)
|
|
49
|
+
locationInfo.put("altitude", location.altitude)
|
|
50
|
+
locationInfo.put("speed", location.speed)
|
|
51
|
+
locationInfo.put("heading", location.bearing)
|
|
52
|
+
|
|
53
|
+
return locationInfo.toString()
|
|
54
|
+
}
|
|
55
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
package io.ionic.backgroundrunner.plugin.api
|
|
2
|
+
|
|
3
|
+
import android.content.Context
|
|
4
|
+
import android.content.SharedPreferences
|
|
5
|
+
import io.ionic.android_js_engine.api.KVAPI
|
|
6
|
+
|
|
7
|
+
class KV (context: android.content.Context, label: String) : KVAPI {
|
|
8
|
+
private val prefs: SharedPreferences
|
|
9
|
+
|
|
10
|
+
init {
|
|
11
|
+
this.prefs = context.getSharedPreferences(label, Context.MODE_PRIVATE)
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
override fun set(key: String, value: String) {
|
|
15
|
+
val editor: SharedPreferences.Editor = prefs.edit()
|
|
16
|
+
editor.putString(key, value)
|
|
17
|
+
editor.apply()
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
override fun get(key: String): String? {
|
|
21
|
+
return prefs.getString(key, null)
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
override fun remove(key: String) {
|
|
25
|
+
val editor: SharedPreferences.Editor = prefs.edit()
|
|
26
|
+
editor.remove(key)
|
|
27
|
+
editor.apply()
|
|
28
|
+
}
|
|
29
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
package io.ionic.backgroundrunner.plugin.api
|
|
2
|
+
|
|
3
|
+
class NetworkStatus {
|
|
4
|
+
enum class ConnectionType(val connectionType: String) {
|
|
5
|
+
WIFI("wifi"), CELLULAR("cellular"), NONE("none"), UNKNOWN("unknown");
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
var connected = false
|
|
9
|
+
var connectionType = ConnectionType.NONE
|
|
10
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
package io.ionic.backgroundrunner.plugin.api
|
|
2
|
+
|
|
3
|
+
import android.annotation.SuppressLint
|
|
4
|
+
import com.getcapacitor.plugin.util.AssetUtil
|
|
5
|
+
import org.json.JSONObject
|
|
6
|
+
import java.text.SimpleDateFormat
|
|
7
|
+
import java.util.Date
|
|
8
|
+
import java.util.TimeZone
|
|
9
|
+
|
|
10
|
+
@SuppressLint("SimpleDateFormat")
|
|
11
|
+
class Notification(jsonObject: JSONObject) {
|
|
12
|
+
var id: Int
|
|
13
|
+
var title: String
|
|
14
|
+
var body: String
|
|
15
|
+
var ongoing: Boolean
|
|
16
|
+
var autoCancel: Boolean
|
|
17
|
+
var scheduleAt: Date
|
|
18
|
+
var largeBody: String? = null
|
|
19
|
+
var summaryText: String? = null
|
|
20
|
+
var sound: String? = null
|
|
21
|
+
var smallIcon: String? = null
|
|
22
|
+
var largeIcon: String? = null
|
|
23
|
+
var actionTypeID: String? = null
|
|
24
|
+
var extra: HashMap<String, Any>? = null
|
|
25
|
+
var group: String? = null
|
|
26
|
+
var groupSummary: String? = null
|
|
27
|
+
var channelId: String? = null
|
|
28
|
+
|
|
29
|
+
private val jsDateFormat = "yyyy-MM-dd'T'HH:mm:ss.SSS'Z'";
|
|
30
|
+
|
|
31
|
+
init {
|
|
32
|
+
id = jsonObject.optInt("id", -1)
|
|
33
|
+
title = jsonObject.optString("title", "")
|
|
34
|
+
body = jsonObject.optString("body", "")
|
|
35
|
+
ongoing = jsonObject.optBoolean("ongoing", false)
|
|
36
|
+
autoCancel = jsonObject.optBoolean("autoCancel", false)
|
|
37
|
+
|
|
38
|
+
val scheduleDateString = jsonObject.getString("scheduleAt")
|
|
39
|
+
|
|
40
|
+
val sdf = SimpleDateFormat(jsDateFormat)
|
|
41
|
+
val parsedDate = sdf.parse(scheduleDateString)
|
|
42
|
+
|
|
43
|
+
scheduleAt = parsedDate ?: Date()
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
fun smallIcon(context: android.content.Context, defaultIcon: Int): Int {
|
|
47
|
+
var resId = AssetUtil.RESOURCE_ID_ZERO_VALUE
|
|
48
|
+
|
|
49
|
+
if (smallIcon != null) {
|
|
50
|
+
resId = AssetUtil.getResourceID(context, smallIcon, "drawable")
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
if (resId == AssetUtil.RESOURCE_ID_ZERO_VALUE) {
|
|
54
|
+
resId = defaultIcon
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
return resId
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
package io.ionic.backgroundrunner.plugin.api
|
|
2
|
+
|
|
3
|
+
import android.R.drawable
|
|
4
|
+
import android.app.AlarmManager
|
|
5
|
+
import android.app.NotificationChannel
|
|
6
|
+
import android.app.NotificationManager
|
|
7
|
+
import android.app.PendingIntent
|
|
8
|
+
import android.content.ContentResolver
|
|
9
|
+
import android.content.Context
|
|
10
|
+
import android.content.Intent
|
|
11
|
+
import android.media.AudioAttributes
|
|
12
|
+
import android.net.Uri
|
|
13
|
+
import android.os.Build
|
|
14
|
+
import androidx.core.app.NotificationCompat
|
|
15
|
+
import androidx.core.app.NotificationManagerCompat
|
|
16
|
+
import com.getcapacitor.Logger.config
|
|
17
|
+
import com.getcapacitor.plugin.util.AssetUtil
|
|
18
|
+
import io.ionic.android_js_engine.api.NotificationsAPI
|
|
19
|
+
import org.json.JSONArray
|
|
20
|
+
|
|
21
|
+
class Notifications(context: Context) : NotificationsAPI {
|
|
22
|
+
private val manager: NotificationManagerCompat
|
|
23
|
+
private val context: Context
|
|
24
|
+
|
|
25
|
+
var defaultSmallIconID = AssetUtil.RESOURCE_ID_ZERO_VALUE
|
|
26
|
+
var defaultSoundID = AssetUtil.RESOURCE_ID_ZERO_VALUE
|
|
27
|
+
|
|
28
|
+
init {
|
|
29
|
+
this.context = context
|
|
30
|
+
this.manager = NotificationManagerCompat.from(context)
|
|
31
|
+
|
|
32
|
+
this.createNotificationChannel()
|
|
33
|
+
}
|
|
34
|
+
companion object {
|
|
35
|
+
const val notificationIntentKey = "LocalNotificationId"
|
|
36
|
+
const val defaultNotificationChannelID = "default"
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
override fun schedule(jsonString: String) {
|
|
40
|
+
val notifications = mutableListOf<Notification>()
|
|
41
|
+
|
|
42
|
+
try {
|
|
43
|
+
val options = JSONArray(jsonString)
|
|
44
|
+
for(i in 0 until options.length()) {
|
|
45
|
+
notifications.add(i, Notification(options.getJSONObject(i)))
|
|
46
|
+
}
|
|
47
|
+
} catch(ex: Exception) {
|
|
48
|
+
throw Exception("options must be an array: ${ex.localizedMessage}")
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
notifications.forEach {
|
|
52
|
+
val channelId = it.channelId ?: defaultNotificationChannelID
|
|
53
|
+
|
|
54
|
+
val builder = NotificationCompat.Builder(context, channelId)
|
|
55
|
+
builder.setContentTitle(it.title)
|
|
56
|
+
builder.setContentText(it.body)
|
|
57
|
+
builder.setAutoCancel(it.autoCancel)
|
|
58
|
+
builder.setOngoing(it.ongoing)
|
|
59
|
+
builder.priority = NotificationCompat.PRIORITY_DEFAULT
|
|
60
|
+
builder.setGroupSummary(it.groupSummary != null)
|
|
61
|
+
builder.setSmallIcon(it.smallIcon(this.context, getDefaultSmallIcon()))
|
|
62
|
+
|
|
63
|
+
val largeBody = it.largeBody
|
|
64
|
+
if (largeBody != null) {
|
|
65
|
+
val style = NotificationCompat.BigTextStyle()
|
|
66
|
+
style.bigText(largeBody)
|
|
67
|
+
style.setSummaryText(it.summaryText)
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
scheduleNotification(builder.build(), it)
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
private fun createNotificationChannel() {
|
|
75
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
|
76
|
+
val name: CharSequence = "Default"
|
|
77
|
+
val description = "Default"
|
|
78
|
+
val importance = NotificationManager.IMPORTANCE_DEFAULT
|
|
79
|
+
val channel = NotificationChannel(defaultNotificationChannelID, name, importance)
|
|
80
|
+
channel.description = description
|
|
81
|
+
val audioAttributes = AudioAttributes.Builder()
|
|
82
|
+
.setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
|
|
83
|
+
.setUsage(AudioAttributes.USAGE_ALARM)
|
|
84
|
+
.build()
|
|
85
|
+
val soundUri = this.getDefaultSoundUrl(context)
|
|
86
|
+
if (soundUri != null) {
|
|
87
|
+
channel.setSound(soundUri, audioAttributes)
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
val notificationManager = context.getSystemService(
|
|
91
|
+
NotificationManager::class.java
|
|
92
|
+
)
|
|
93
|
+
notificationManager.createNotificationChannel(channel)
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
private fun getDefaultSoundUrl(context: Context): Uri? {
|
|
98
|
+
val soundId = getDefaultSound(context)
|
|
99
|
+
return if (soundId != AssetUtil.RESOURCE_ID_ZERO_VALUE) {
|
|
100
|
+
Uri.parse(ContentResolver.SCHEME_ANDROID_RESOURCE + "://" + context.packageName + "/" + soundId)
|
|
101
|
+
} else null
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
private fun getDefaultSmallIcon(): Int {
|
|
105
|
+
if (defaultSmallIconID != AssetUtil.RESOURCE_ID_ZERO_VALUE) return defaultSmallIconID
|
|
106
|
+
var resId = AssetUtil.RESOURCE_ID_ZERO_VALUE
|
|
107
|
+
var smallIconConfigResourceName = config.getString("smallIcon")
|
|
108
|
+
smallIconConfigResourceName = AssetUtil.getResourceBaseName(smallIconConfigResourceName)
|
|
109
|
+
if (smallIconConfigResourceName != null) {
|
|
110
|
+
resId = AssetUtil.getResourceID(context, smallIconConfigResourceName, "drawable")
|
|
111
|
+
}
|
|
112
|
+
if (resId == AssetUtil.RESOURCE_ID_ZERO_VALUE) {
|
|
113
|
+
resId = drawable.ic_dialog_info
|
|
114
|
+
}
|
|
115
|
+
defaultSmallIconID = resId
|
|
116
|
+
return resId
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
private fun getDefaultSound(context: Context): Int {
|
|
120
|
+
if (defaultSoundID != AssetUtil.RESOURCE_ID_ZERO_VALUE) return defaultSoundID
|
|
121
|
+
var resId = AssetUtil.RESOURCE_ID_ZERO_VALUE
|
|
122
|
+
var soundConfigResourceName = config.getString("sound")
|
|
123
|
+
soundConfigResourceName = AssetUtil.getResourceBaseName(soundConfigResourceName)
|
|
124
|
+
if (soundConfigResourceName != null) {
|
|
125
|
+
resId = AssetUtil.getResourceID(context, soundConfigResourceName, "raw")
|
|
126
|
+
}
|
|
127
|
+
defaultSoundID = resId
|
|
128
|
+
return resId
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
private fun scheduleNotification(notification: android.app.Notification, request: Notification) {
|
|
132
|
+
val alarmManager = context.getSystemService(Context.ALARM_SERVICE) as AlarmManager
|
|
133
|
+
val intent = Intent(context, TimedNotificationPublisher::class.java)
|
|
134
|
+
intent.putExtra(notificationIntentKey, request.id)
|
|
135
|
+
intent.putExtra(TimedNotificationPublisher.NOTIFICATION_KEY, notification)
|
|
136
|
+
var flags = PendingIntent.FLAG_CANCEL_CURRENT
|
|
137
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
|
|
138
|
+
flags = flags or PendingIntent.FLAG_MUTABLE
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
val pendingIntent =
|
|
142
|
+
PendingIntent.getBroadcast(context, request.id, intent, flags)
|
|
143
|
+
|
|
144
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S && !alarmManager.canScheduleExactAlarms()) {
|
|
145
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
|
146
|
+
alarmManager.setAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, request.scheduleAt.time, pendingIntent);
|
|
147
|
+
} else {
|
|
148
|
+
alarmManager.set(AlarmManager.RTC, request.scheduleAt.time, pendingIntent);
|
|
149
|
+
}
|
|
150
|
+
} else {
|
|
151
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
|
|
152
|
+
alarmManager.setExactAndAllowWhileIdle(AlarmManager.RTC_WAKEUP, request.scheduleAt.time, pendingIntent);
|
|
153
|
+
} else {
|
|
154
|
+
alarmManager.setExact(AlarmManager.RTC, request.scheduleAt.time, pendingIntent);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
}
|
package/android/src/main/java/io/ionic/backgroundrunner/plugin/api/TimedNotificationPublisher.kt
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
package io.ionic.backgroundrunner.plugin.api
|
|
2
|
+
|
|
3
|
+
import android.app.NotificationManager
|
|
4
|
+
import android.content.BroadcastReceiver
|
|
5
|
+
import android.content.Context
|
|
6
|
+
import android.content.Intent
|
|
7
|
+
import android.os.Build
|
|
8
|
+
import com.getcapacitor.Logger
|
|
9
|
+
|
|
10
|
+
class TimedNotificationPublisher : BroadcastReceiver() {
|
|
11
|
+
companion object {
|
|
12
|
+
const val NOTIFICATION_KEY = "NotificationPublisher.notification"
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
override fun onReceive(context: Context?, intent: Intent?) {
|
|
16
|
+
val notificationManager =
|
|
17
|
+
context!!.getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
|
18
|
+
|
|
19
|
+
val notification: android.app.Notification?
|
|
20
|
+
|
|
21
|
+
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
|
|
22
|
+
notification = intent!!.getParcelableExtra(NOTIFICATION_KEY, android.app.Notification::class.java)
|
|
23
|
+
} else {
|
|
24
|
+
notification = getParcelableExtraLegacy(intent!!)
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
notification?.`when` = System.currentTimeMillis()
|
|
28
|
+
|
|
29
|
+
val id =
|
|
30
|
+
intent.getIntExtra(Notifications.notificationIntentKey, Int.MIN_VALUE)
|
|
31
|
+
if (id == Int.MIN_VALUE) {
|
|
32
|
+
Logger.error(Logger.tags("LN"), "No valid id supplied", null)
|
|
33
|
+
}
|
|
34
|
+
notificationManager.notify(id, notification)
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
private fun getParcelableExtraLegacy(intent: Intent): android.app.Notification? {
|
|
38
|
+
return intent.getParcelableExtra(NOTIFICATION_KEY)
|
|
39
|
+
}
|
|
40
|
+
}
|
|
Binary file
|
|
File without changes
|