commit a7f9c11a4e2f5fb1dac1058c0c96435d31f29a29
Author: wylyl22 <2373073266@qq.com>
Date: Fri Aug 19 10:16:45 2022 +0800
first commit
diff --git a/CHANGELOG.md b/CHANGELOG.md
new file mode 100644
index 0000000..864f413
--- /dev/null
+++ b/CHANGELOG.md
@@ -0,0 +1,66 @@
+## 0.2.0
+* Upgrade `minSdk` to 23
+* Upgrade `targetSdk` to 31
+* Upgrade min dart sdk to 2.15.1
+* Fix SmsMethodCallHandler error.
+* Added Service Center field in SmsMessage
+
+## 0.1.4
+* Fix SmsType parsing (Contributor: https://github.com/Mabsten)
+* Remove SmsMethodCallHandler trailing comma.
+
+## 0.1.3
+* Fix background execution (Contributor: https://github.com/meomap)
+
+## 0.1.2
+* Change invokeMethod call type for getSms methods to List? (No change to telephony API)
+
+## 0.1.1
+* Added background instance for executing telephony methods in background.
+* Fix type cast issues.
+
+## 0.1.0
+* Feature equivalent of v0.0.9
+* Enabled null-safety
+
+## 0.0.9
+* Fix sendSms Future never completes.
+
+## 0.0.8
+* Upgrade platform version.
+
+## 0.0.7
+* Fix build error when plugin included in iOS project.
+
+## 0.0.6
+* Multipart messages are grouped as one single SMS so that listenSms functions only get triggered once.
+
+## 0.0.5
+* Fix background execution error due to FlutterLoader.getInstance() deprecation.
+
+## 0.0.4
+
+#### New Features:
+* Start phone calls from default dialer or directly from the app.
+
+## 0.0.3
+
+#### Changes:
+* Fix unresponsive foreground methods after starting background isolate.
+
+
+## 0.0.2
+
+#### Possible breaking changes:
+* sendSms functions are now async.
+
+#### Other changes:
+* Adding documentation.
+* Fix conflicting class name (Column --> TelephonyColumn).
+* Update plugin description.
+
+
+## 0.0.1
+
+* First release of telephony
+
diff --git a/LICENSE b/LICENSE
new file mode 100644
index 0000000..90a1630
--- /dev/null
+++ b/LICENSE
@@ -0,0 +1,21 @@
+MIT License
+
+Copyright (c) 2020 Shounak Mulay
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.
diff --git a/README.md b/README.md
new file mode 100644
index 0000000..9e8eafe
--- /dev/null
+++ b/README.md
@@ -0,0 +1,326 @@
+
+
+
+
+
+
+
+
+
+
+# Telephony
+|:exclamation: This plugin currently only works on Android Platform|
+|------------------------------------------------------------------|
+
+
+A Flutter plugin to use telephony features such as
+- Send SMS Messages
+- Query SMS Messages
+- Listen for incoming SMS
+- Retrieve various network parameters
+- Start phone calls
+
+This plugin tries to replicate some of the functionality provided by Android's [Telephony](https://developer.android.com/reference/android/provider/Telephony) class.
+
+Check the [Features section](#Features) to see the list of implemented and missing features.
+
+## Get Started
+### :bulb: View the **[entire documentation here](https://telephony.shounakmulay.dev/)**.
+
+## Usage
+To use this plugin add `telephony` as a [dependency in your pubspec.yaml file](https://flutter.dev/docs/development/packages-and-plugins/using-packages).
+
+##### Versions [0.0.9](https://pub.dev/packages/telephony/versions/0.0.9) and lower are not null safe.
+##### Versions [0.1.0](https://pub.dev/packages/telephony/versions/0.1.0) and above opt into null safety.
+
+
+### Setup
+Import the `telephony` package
+```dart
+import 'package:telephony/telephony.dart';
+```
+
+
+Retrieve the singleton instance of `telephony` by calling
+```dart
+final Telephony telephony = Telephony.instance;
+```
+
+### [Permissions](https://shounakmulay.gitbook.io/telephony/permissions)
+**Although this plugin will check and ask for permissions at runtime, it is advisable to _manually ask for permissions_ before calling any other functions.**
+
+The plugin will only request those permission that are listed in the `AndroidManifest.xml`.
+
+Manually request permission using
+```dart
+bool permissionsGranted = await telephony.requestPhoneAndSmsPermissions;
+```
+You can also request SMS or Phone permissions separately using `requestSmsPermissions` or `requestPhonePermissions` respectively.
+
+### [Send SMS](https://shounakmulay.gitbook.io/telephony/sending-an-sms)
+:exclamation: Requires `SEND_SMS` permission.
+Add the following permission in your `AndroidManifest.xml`
+```xml
+
+```
+
+SMS can either be sent directly or via the default SMS app.
+
+#### Send SMS directly from your app:
+```dart
+telephony.sendSms(
+ to: "1234567890",
+ message: "May the force be with you!"
+ );
+```
+If you want to listen to the status of the message being sent, provide `SmsSendStatusListener` to the `sendSms` function.
+```dart
+final SmsSendStatusListener listener = (SendStatus status) {
+ // Handle the status
+ };
+
+telephony.sendSms(
+ to: "1234567890",
+ message: "May the force be with you!",
+ statusListener: listener
+ );
+```
+If the body of the message is longer than the standard SMS length limit of `160 characters`, you can send a multipart SMS by setting the `isMultipart` flag.
+
+#### Send SMS via the default SMS app:
+```dart
+telephony.sendSmsByDefaultApp(to: "1234567890", message: "May the force be with you!");
+```
+
+### [Query SMS](https://shounakmulay.gitbook.io/telephony/query-sms)
+:exclamation: Requires `READ_SMS` permission.
+Add the following permission in your `AndroidManifest.xml`
+```xml
+
+```
+
+Use one of `getInboxSms()`, `getSentSms()` or `getDraftSms()` functions to query the messages on device.
+
+You can provide the list of `SmsColumns` that need to be returned by the query.
+
+If not explicitly specified, defaults to `[
+ SmsColumn.ID,
+ SmsColumn.ADDRESS,
+ SmsColumn.BODY,
+ SmsColumn.DATE
+]`
+
+Provide a `SmsFilter` to filter the results of the query. Functions like a `SQL WHERE` clause.
+
+Provide a list of `OrderBy` objects to sort the results. The level of importance is determined by the position of `OrderBy` in the list.
+
+All paramaters are optional.
+```dart
+List messages = await telephony.getInboxSms(
+ columns: [SmsColumn.ADDRESS, SmsColumn.BODY],
+ filter: SmsFilter.where(SmsColumn.ADDRESS)
+ .equals("1234567890")
+ .and(SmsColumn.BODY)
+ .like("starwars"),
+ sortOrder: [OrderBy(SmsColumn.ADDRESS, sort: Sort.ASC),
+ OrderBy(SmsColumn.BODY)]
+ );
+```
+
+### [Query Conversations](https://shounakmulay.gitbook.io/telephony/query-conversations)
+:exclamation: Requires `READ_SMS` permission.
+Add the following permission in your `AndroidManifest.xml`
+```xml
+
+```
+
+Works similar to [SMS queries](#query-sms).
+
+All columns are returned with every query. They are `[
+ ConversationColumn.SNIPPET,
+ ConversationColumn.THREAD_ID,
+ ConversationColumn.MSG_COUNT
+]`
+
+Uses `ConversationFilter` instead of `SmsFilter`.
+
+```dart
+List messages = await telephony.getConversations(
+ filter: ConversationFilter.where(ConversationColumn.MSG_COUNT)
+ .equals("4")
+ .and(ConversationColumn.THREAD_ID)
+ .greaterThan("12"),
+ sortOrder: [OrderBy(ConversationColumn.THREAD_ID, sort: Sort.ASC)]
+ );
+```
+
+### [Listen to incoming SMS](https://shounakmulay.gitbook.io/telephony/listen-incoming-sms)
+:exclamation: Requires `RECEIVE_SMS` permission.
+
+1. To listen to incoming SMS add the `RECEIVE_SMS` permission to your `AndroidManifest.xml` file and register the `BroadcastReceiver`.
+
+
+```xml
+
+
+
+
+ ...
+ ...
+
+
+
+
+
+
+
+
+
+```
+
+2. Create a **top-level static function** to handle incoming messages when app is not is foreground.
+
+ :warning: Avoid heavy computations in the background handler as Android system may kill long running operations in the background.
+
+```dart
+backgrounMessageHandler(SmsMessage message) async {
+ //Handle background message
+}
+
+void main() {
+ runApp(MyApp());
+}
+```
+3. Call `listenIncomingSms` with a foreground `MessageHandler` and pass in the static `backgrounMessageHandler`.
+```dart
+telephony.listenIncomingSms(
+ onNewMessage: (SmsMessage message) {
+ // Handle message
+ },
+ onBackgroundMessage: backgroundMessageHandler
+ );
+```
+
+Preferably should be called early in app lifecycle.
+
+4. If you do not wish to receive incoming SMS when the app is in background, just do not pass the `onBackgroundMessage` paramater.
+
+ Alternatively if you prefer to expecility disable background execution, set the `listenInBackground` flag to `false`.
+```dart
+telephony.listenIncomingSms(
+ onNewMessage: (SmsMessage message) {
+ // Handle message
+ },
+ listenInBackground: false
+ );
+```
+5. As of the `1.12` release of Flutter, plugins are automatically registered. This will allow you to use plugins as you normally do even in the background execution context.
+```dart
+backgrounMessageHandler(SmsMessage message) async {
+ // Handle background message
+
+ // Use plugins
+ Vibration.vibrate(duration: 500);
+ }
+```
+### [Network data and metrics](https://shounakmulay.gitbook.io/telephony/network-data-and-metrics)
+
+Fetch various metrics such as `network type`, `sim state`, etc.
+
+```dart
+
+// Check if a device is capable of sending SMS
+bool canSendSms = await telephony.isSmsCapable;
+
+// Get sim state
+SimState simState = await telephony.simState;
+```
+
+Check out the [detailed documentation](https://shounakmulay.gitbook.io/telephony/network-data-and-metrics) to know all possible metrics and their values.
+
+### Executing in background
+If you want to call the `telephony` methods in background, you can do in the following ways.
+
+#### 1. Using only `Telephony.instance`
+If you want to continue using `Telephony.instance` in the background, you will need to make sure that once the app comes back to the front, it again calls `Telephony.instance`.
+```dart
+backgrounMessageHandler(SmsMessage message) async {
+ // Handle background message
+ Telephony.instance.sendSms(to: "123456789", message: "Message from background")
+}
+
+void main() {
+ runApp(MyApp());
+}
+
+class _MyAppState extends State {
+ String _message;
+ // This will not work as the instance will be replaced by
+ // the one in background.
+ final telephony = Telephony.instance;
+
+ @override
+ void initState() {
+ super.initState();
+ // You should make sure call to instance is made every time
+ // app comes to foreground
+ final inbox = Telephony.instance.getInboxSms()
+ }
+
+```
+
+#### 2. Use `backgroundInstance`
+If you cannot make sure that the call to instance would be made every time app comes to foreground, or if you would prefer to maintain a separate background instance,
+you can use `Telephony.backgroundInstance` in the background execution context.
+```dart
+backgrounMessageHandler(SmsMessage message) async {
+ // Handle background message
+ Telephony.backgroundInstance.sendSms(to: "123456789", message: "Message from background")
+}
+
+void main() {
+ runApp(MyApp());
+}
+
+class _MyAppState extends State {
+ String _message;
+ final telephony = Telephony.instance;
+
+ @override
+ void initState() {
+ super.initState();
+ final inbox = telephony.getInboxSms()
+ }
+
+```
+
+
+## Features
+
+ - [x] [Send SMS](#send-sms)
+ - [x] [Query SMS](#query-sms)
+ - [x] Inbox
+ - [x] Sent
+ - [x] Draft
+ - [x] [Query Conversations](#query-conversations)
+ - [x] [Listen to incoming SMS](#listen-to-incoming-sms)
+ - [x] When app is in foreground
+ - [x] When app is in background
+ - [x] [Network data and metrics](#network-data-and-metrics)
+ - [x] Cellular data state
+ - [x] Call state
+ - [x] Data activity
+ - [x] Network operator
+ - [x] Network operator name
+ - [x] Data network type
+ - [x] Phone type
+ - [x] Sim operator
+ - [x] Sim operator name
+ - [x] Sim state
+ - [x] Network roaming
+ - [x] Signal strength
+ - [x] Service state
+ - [x] Start Phone Call
+ - [ ] Schedule a SMS
+ - [ ] SMS Retriever API
diff --git a/android/build.gradle b/android/build.gradle
new file mode 100644
index 0000000..bcc0321
--- /dev/null
+++ b/android/build.gradle
@@ -0,0 +1,48 @@
+group 'com.shounakmulay.telephony'
+version '1.0-SNAPSHOT'
+
+buildscript {
+ ext.kotlin_version = '1.6.21'
+ repositories {
+ google()
+ mavenCentral()
+ }
+
+ dependencies {
+ classpath 'com.android.tools.build:gradle:4.1.3'
+ classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
+ }
+}
+
+rootProject.allprojects {
+ repositories {
+ google()
+ mavenCentral()
+ }
+}
+
+apply plugin: 'com.android.library'
+apply plugin: 'kotlin-android'
+
+android {
+ compileSdkVersion 31
+
+ kotlinOptions {
+ jvmTarget = "1.8"
+ }
+
+ sourceSets {
+ main.java.srcDirs += 'src/main/kotlin'
+ }
+ defaultConfig {
+ minSdkVersion 23
+ }
+ lintOptions {
+ disable 'InvalidPackage'
+ }
+}
+
+dependencies {
+ implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
+ implementation 'androidx.annotation:annotation:1.3.0'
+}
diff --git a/android/gradle.properties b/android/gradle.properties
new file mode 100644
index 0000000..94adc3a
--- /dev/null
+++ b/android/gradle.properties
@@ -0,0 +1,3 @@
+org.gradle.jvmargs=-Xmx1536M
+android.useAndroidX=true
+android.enableJetifier=true
diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..3c7bff3
--- /dev/null
+++ b/android/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,6 @@
+#Sun Aug 23 12:29:06 IST 2020
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-all.zip
diff --git a/android/settings.gradle b/android/settings.gradle
new file mode 100644
index 0000000..0fc5a14
--- /dev/null
+++ b/android/settings.gradle
@@ -0,0 +1 @@
+rootProject.name = 'telephony'
diff --git a/android/src/main/AndroidManifest.xml b/android/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..29aa6c7
--- /dev/null
+++ b/android/src/main/AndroidManifest.xml
@@ -0,0 +1,5 @@
+
+
+
+
diff --git a/android/src/main/kotlin/com/shounakmulay/telephony/PermissionsController.kt b/android/src/main/kotlin/com/shounakmulay/telephony/PermissionsController.kt
new file mode 100644
index 0000000..0aa12ee
--- /dev/null
+++ b/android/src/main/kotlin/com/shounakmulay/telephony/PermissionsController.kt
@@ -0,0 +1,59 @@
+package com.shounakmulay.telephony
+
+import android.app.Activity
+import android.content.Context
+import android.content.pm.PackageManager
+import android.content.pm.PackageManager.PERMISSION_GRANTED
+import android.os.Build
+import androidx.annotation.RequiresApi
+import androidx.core.content.ContextCompat
+import com.shounakmulay.telephony.utils.Constants.PHONE_PERMISSIONS
+import com.shounakmulay.telephony.utils.Constants.SERVICE_STATE_PERMISSIONS
+import com.shounakmulay.telephony.utils.Constants.SMS_PERMISSIONS
+
+class PermissionsController(private val context: Context) {
+
+ var isRequestingPermission: Boolean = false
+
+ fun hasRequiredPermissions(permissions: List): Boolean {
+ var hasPermissions = true
+ for (permission in permissions) {
+ hasPermissions = hasPermissions && checkPermission(permission)
+ }
+ return hasPermissions
+ }
+
+ private fun checkPermission(permission: String): Boolean {
+ return Build.VERSION.SDK_INT < Build.VERSION_CODES.M || context.checkSelfPermission(permission) == PERMISSION_GRANTED
+ }
+
+ @RequiresApi(Build.VERSION_CODES.M)
+ fun requestPermissions(activity: Activity, permissions: List, requestCode: Int) {
+ if (!isRequestingPermission) {
+ isRequestingPermission = true
+ activity.requestPermissions(permissions.toTypedArray(), requestCode)
+ }
+ }
+
+ fun getSmsPermissions(): List {
+ val permissions = getListedPermissions()
+ return permissions.filter { permission -> SMS_PERMISSIONS.contains(permission) }
+ }
+
+ fun getPhonePermissions(): List {
+ val permissions = getListedPermissions()
+ return permissions.filter { permission -> PHONE_PERMISSIONS.contains(permission) }
+ }
+
+ fun getServiceStatePermissions(): List {
+ val permissions = getListedPermissions()
+ return permissions.filter { permission -> SERVICE_STATE_PERMISSIONS.contains(permission) }
+ }
+
+ private fun getListedPermissions(): Array {
+ context.apply {
+ val info = packageManager.getPackageInfo(packageName, PackageManager.GET_PERMISSIONS)
+ return info.requestedPermissions ?: arrayOf()
+ }
+ }
+}
\ No newline at end of file
diff --git a/android/src/main/kotlin/com/shounakmulay/telephony/TelephonyPlugin.kt b/android/src/main/kotlin/com/shounakmulay/telephony/TelephonyPlugin.kt
new file mode 100644
index 0000000..58fc693
--- /dev/null
+++ b/android/src/main/kotlin/com/shounakmulay/telephony/TelephonyPlugin.kt
@@ -0,0 +1,73 @@
+package com.shounakmulay.telephony
+
+import android.content.Context
+import androidx.annotation.NonNull
+import com.shounakmulay.telephony.sms.IncomingSmsHandler
+import com.shounakmulay.telephony.utils.Constants.CHANNEL_SMS
+import com.shounakmulay.telephony.sms.IncomingSmsReceiver
+import com.shounakmulay.telephony.sms.SmsController
+import com.shounakmulay.telephony.sms.SmsMethodCallHandler
+import io.flutter.embedding.engine.plugins.FlutterPlugin
+import io.flutter.embedding.engine.plugins.activity.ActivityAware
+import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding
+import io.flutter.plugin.common.*
+
+
+class TelephonyPlugin : FlutterPlugin, ActivityAware {
+
+ private lateinit var smsChannel: MethodChannel
+
+ private lateinit var smsMethodCallHandler: SmsMethodCallHandler
+
+ private lateinit var smsController: SmsController
+
+ private lateinit var binaryMessenger: BinaryMessenger
+
+ private lateinit var permissionsController: PermissionsController
+
+ override fun onAttachedToEngine(@NonNull flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
+ if (!this::binaryMessenger.isInitialized) {
+ binaryMessenger = flutterPluginBinding.binaryMessenger
+ }
+
+ setupPlugin(flutterPluginBinding.applicationContext, binaryMessenger)
+ }
+
+ override fun onDetachedFromEngine(@NonNull binding: FlutterPlugin.FlutterPluginBinding) {
+ tearDownPlugin()
+ }
+
+ override fun onDetachedFromActivity() {
+ tearDownPlugin()
+ }
+
+ override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) {
+ onAttachedToActivity(binding)
+ }
+
+ override fun onAttachedToActivity(binding: ActivityPluginBinding) {
+ IncomingSmsReceiver.foregroundSmsChannel = smsChannel
+ smsMethodCallHandler.setActivity(binding.activity)
+ binding.addRequestPermissionsResultListener(smsMethodCallHandler)
+ }
+
+ override fun onDetachedFromActivityForConfigChanges() {
+ onDetachedFromActivity()
+ }
+
+ private fun setupPlugin(context: Context, messenger: BinaryMessenger) {
+ smsController = SmsController(context)
+ permissionsController = PermissionsController(context)
+ smsMethodCallHandler = SmsMethodCallHandler(context, smsController, permissionsController)
+
+ smsChannel = MethodChannel(messenger, CHANNEL_SMS)
+ smsChannel.setMethodCallHandler(smsMethodCallHandler)
+ smsMethodCallHandler.setForegroundChannel(smsChannel)
+ }
+
+ private fun tearDownPlugin() {
+ IncomingSmsReceiver.foregroundSmsChannel = null
+ smsChannel.setMethodCallHandler(null)
+ }
+
+}
diff --git a/android/src/main/kotlin/com/shounakmulay/telephony/sms/ContextHolder.kt b/android/src/main/kotlin/com/shounakmulay/telephony/sms/ContextHolder.kt
new file mode 100644
index 0000000..0021468
--- /dev/null
+++ b/android/src/main/kotlin/com/shounakmulay/telephony/sms/ContextHolder.kt
@@ -0,0 +1,8 @@
+package com.shounakmulay.telephony.sms
+
+import android.content.Context
+import com.shounakmulay.telephony.sms.ContextHolder
+
+object ContextHolder {
+ var applicationContext: Context? = null
+}
\ No newline at end of file
diff --git a/android/src/main/kotlin/com/shounakmulay/telephony/sms/IncomingSmsHandler.kt b/android/src/main/kotlin/com/shounakmulay/telephony/sms/IncomingSmsHandler.kt
new file mode 100644
index 0000000..b22ca6a
--- /dev/null
+++ b/android/src/main/kotlin/com/shounakmulay/telephony/sms/IncomingSmsHandler.kt
@@ -0,0 +1,269 @@
+package com.shounakmulay.telephony.sms
+
+import android.app.ActivityManager
+import android.app.KeyguardManager
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import android.os.Process
+import android.provider.Telephony
+import android.telephony.SmsMessage
+import com.shounakmulay.telephony.utils.Constants
+import com.shounakmulay.telephony.utils.Constants.HANDLE
+import com.shounakmulay.telephony.utils.Constants.HANDLE_BACKGROUND_MESSAGE
+import com.shounakmulay.telephony.utils.Constants.MESSAGE
+import com.shounakmulay.telephony.utils.Constants.MESSAGE_BODY
+import com.shounakmulay.telephony.utils.Constants.ON_MESSAGE
+import com.shounakmulay.telephony.utils.Constants.ORIGINATING_ADDRESS
+import com.shounakmulay.telephony.utils.Constants.SERVICE_CENTER_ADDRESS
+import com.shounakmulay.telephony.utils.Constants.SHARED_PREFERENCES_NAME
+import com.shounakmulay.telephony.utils.Constants.SHARED_PREFS_BACKGROUND_MESSAGE_HANDLE
+import com.shounakmulay.telephony.utils.Constants.SHARED_PREFS_BACKGROUND_SETUP_HANDLE
+import com.shounakmulay.telephony.utils.Constants.SHARED_PREFS_DISABLE_BACKGROUND_EXE
+import com.shounakmulay.telephony.utils.Constants.STATUS
+import com.shounakmulay.telephony.utils.Constants.TIMESTAMP
+import com.shounakmulay.telephony.utils.SmsAction
+import io.flutter.FlutterInjector
+import io.flutter.embedding.engine.FlutterEngine
+import io.flutter.embedding.engine.FlutterJNI
+import io.flutter.embedding.engine.dart.DartExecutor
+import io.flutter.embedding.engine.loader.FlutterLoader
+import io.flutter.plugin.common.MethodCall
+import io.flutter.plugin.common.MethodChannel
+import io.flutter.view.FlutterCallbackInformation
+import java.util.*
+import java.util.concurrent.atomic.AtomicBoolean
+import kotlin.collections.HashMap
+
+
+class IncomingSmsReceiver : BroadcastReceiver() {
+
+ companion object {
+ var foregroundSmsChannel: MethodChannel? = null
+ }
+
+ override fun onReceive(context: Context, intent: Intent?) {
+ ContextHolder.applicationContext = context.applicationContext
+ val smsList = Telephony.Sms.Intents.getMessagesFromIntent(intent)
+ val messagesGroupedByOriginatingAddress = smsList.groupBy { it.originatingAddress }
+ messagesGroupedByOriginatingAddress.forEach { group ->
+ processIncomingSms(context, group.value)
+ }
+ }
+
+ /**
+ * Calls [ON_MESSAGE] method on the Foreground Channel if the application is in foreground.
+ *
+ * If the application is not in the foreground and the background isolate is not running, it initializes the
+ * background isolate. The SMS is added to a background queue that will be processed on the isolate is initialized.
+ *
+ * If the application is not in the foreground but the the background isolate is running, it calls the
+ * [IncomingSmsHandler.executeDartCallbackInBackgroundIsolate] with the SMS.
+ *
+ */
+ private fun processIncomingSms(context: Context, smsList: List) {
+ val messageMap = smsList.first().toMap()
+ smsList.forEachIndexed { index, smsMessage ->
+ if (index > 0) {
+ messageMap[MESSAGE_BODY] = (messageMap[MESSAGE_BODY] as String)
+ .plus(smsMessage.messageBody.trim())
+ }
+ }
+ if (IncomingSmsHandler.isApplicationForeground(context)) {
+ val args = HashMap()
+ args[MESSAGE] = messageMap
+ foregroundSmsChannel?.invokeMethod(ON_MESSAGE, args)
+ } else {
+ val preferences =
+ context.getSharedPreferences(SHARED_PREFERENCES_NAME, Context.MODE_PRIVATE)
+ val disableBackground =
+ preferences.getBoolean(SHARED_PREFS_DISABLE_BACKGROUND_EXE, false)
+ if (!disableBackground) {
+ processInBackground(context, messageMap)
+ }
+ }
+ }
+
+ private fun processInBackground(context: Context, sms: HashMap) {
+ IncomingSmsHandler.apply {
+ if (!isIsolateRunning.get()) {
+ initialize(context)
+ val preferences =
+ context.getSharedPreferences(SHARED_PREFERENCES_NAME, Context.MODE_PRIVATE)
+ val backgroundCallbackHandle =
+ preferences.getLong(SHARED_PREFS_BACKGROUND_SETUP_HANDLE, 0)
+ startBackgroundIsolate(context, backgroundCallbackHandle)
+ backgroundMessageQueue.add(sms)
+ } else {
+ executeDartCallbackInBackgroundIsolate(context, sms)
+ }
+ }
+ }
+}
+
+/**
+ * Convert the [SmsMessage] to a [HashMap]
+ */
+fun SmsMessage.toMap(): HashMap {
+ val smsMap = HashMap()
+ this.apply {
+ smsMap[MESSAGE_BODY] = messageBody
+ smsMap[TIMESTAMP] = timestampMillis.toString()
+ smsMap[ORIGINATING_ADDRESS] = originatingAddress
+ smsMap[STATUS] = status.toString()
+ smsMap[SERVICE_CENTER_ADDRESS] = serviceCenterAddress
+ }
+ return smsMap
+}
+
+/**
+ * Handle all the background processing on received SMS
+ *
+ * Call [setBackgroundSetupHandle] and [setBackgroundMessageHandle] before performing any other operations.
+ *
+ *
+ * Will throw [RuntimeException] if [backgroundChannel] was not initialized by calling [startBackgroundIsolate]
+ * before calling [executeDartCallbackInBackgroundIsolate]
+ */
+object IncomingSmsHandler : MethodChannel.MethodCallHandler {
+
+ internal val backgroundMessageQueue =
+ Collections.synchronizedList(mutableListOf>())
+ internal var isIsolateRunning = AtomicBoolean(false)
+
+ private lateinit var backgroundChannel: MethodChannel
+ private lateinit var backgroundFlutterEngine: FlutterEngine
+ private lateinit var flutterLoader: FlutterLoader
+
+ private var backgroundMessageHandle: Long? = null
+
+ /**
+ * Initializes a background flutter execution environment and executes the callback
+ * to setup the background [MethodChannel]
+ *
+ * Also initializes the method channel on the android side
+ */
+ fun startBackgroundIsolate(context: Context, callbackHandle: Long) {
+ val appBundlePath = flutterLoader.findAppBundlePath()
+ val flutterCallback = FlutterCallbackInformation.lookupCallbackInformation(callbackHandle)
+
+ val dartEntryPoint =
+ DartExecutor.DartCallback(context.assets, appBundlePath, flutterCallback)
+
+ backgroundFlutterEngine = FlutterEngine(context, flutterLoader, FlutterJNI())
+ backgroundFlutterEngine.dartExecutor.executeDartCallback(dartEntryPoint)
+
+ backgroundChannel =
+ MethodChannel(backgroundFlutterEngine.dartExecutor, Constants.CHANNEL_SMS_BACKGROUND)
+ backgroundChannel.setMethodCallHandler(this)
+ }
+
+ /**
+ * Called when the background dart isolate has completed setting up the method channel
+ *
+ * If any SMS were received during the background isolate was being initialized, it will process
+ * all those messages.
+ */
+ fun onChannelInitialized(applicationContext: Context) {
+ isIsolateRunning.set(true)
+ synchronized(backgroundMessageQueue) {
+
+ // Handle all the messages received before the Dart isolate was
+ // initialized, then clear the queue.
+ val iterator = backgroundMessageQueue.iterator()
+ while (iterator.hasNext()) {
+ executeDartCallbackInBackgroundIsolate(applicationContext, iterator.next())
+ }
+ backgroundMessageQueue.clear()
+ }
+ }
+
+ /**
+ * Invoke the method on background channel to handle the message
+ */
+ internal fun executeDartCallbackInBackgroundIsolate(
+ context: Context,
+ message: HashMap
+ ) {
+ if (!this::backgroundChannel.isInitialized) {
+ throw RuntimeException(
+ "setBackgroundChannel was not called before messages came in, exiting."
+ )
+ }
+
+ val args: MutableMap = HashMap()
+ if (backgroundMessageHandle == null) {
+ backgroundMessageHandle = getBackgroundMessageHandle(context)
+ }
+ args[HANDLE] = backgroundMessageHandle
+ args[MESSAGE] = message
+ backgroundChannel.invokeMethod(HANDLE_BACKGROUND_MESSAGE, args)
+ }
+
+ /**
+ * Gets an instance of FlutterLoader from the FlutterInjector, starts initialization and
+ * waits until initialization is complete.
+ *
+ * Should be called before invoking any other background methods.
+ */
+ internal fun initialize(context: Context) {
+ val flutterInjector = FlutterInjector.instance()
+ flutterLoader = flutterInjector.flutterLoader()
+ flutterLoader.startInitialization(context)
+ flutterLoader.ensureInitializationComplete(context.applicationContext, null)
+ }
+
+ fun setBackgroundMessageHandle(context: Context, handle: Long) {
+ backgroundMessageHandle = handle
+
+ // Store background message handle in shared preferences so it can be retrieved
+ // by other application instances.
+ val preferences =
+ context.getSharedPreferences(SHARED_PREFERENCES_NAME, Context.MODE_PRIVATE)
+ preferences.edit().putLong(SHARED_PREFS_BACKGROUND_MESSAGE_HANDLE, handle).apply()
+
+ }
+
+ fun setBackgroundSetupHandle(context: Context, setupBackgroundHandle: Long) {
+ // Store background setup handle in shared preferences so it can be retrieved
+ // by other application instances.
+ val preferences =
+ context.getSharedPreferences(SHARED_PREFERENCES_NAME, Context.MODE_PRIVATE)
+ preferences.edit().putLong(SHARED_PREFS_BACKGROUND_SETUP_HANDLE, setupBackgroundHandle)
+ .apply()
+ }
+
+ private fun getBackgroundMessageHandle(context: Context): Long {
+ return context
+ .getSharedPreferences(SHARED_PREFERENCES_NAME, Context.MODE_PRIVATE)
+ .getLong(SHARED_PREFS_BACKGROUND_MESSAGE_HANDLE, 0)
+ }
+
+ fun isApplicationForeground(context: Context): Boolean {
+ val keyguardManager = context.getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager
+ if (keyguardManager.isKeyguardLocked) {
+ return false
+ }
+ val myPid = Process.myPid()
+ val activityManager = context.getSystemService(Context.ACTIVITY_SERVICE) as ActivityManager
+ var list: List
+ if (activityManager.runningAppProcesses.also { list = it } != null) {
+ for (aList in list) {
+ var info: ActivityManager.RunningAppProcessInfo
+ if (aList.also { info = it }.pid == myPid) {
+ return info.importance == ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND
+ }
+ }
+ }
+ return false
+ }
+
+ override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
+ if (SmsAction.fromMethod(call.method) == SmsAction.BACKGROUND_SERVICE_INITIALIZED) {
+ onChannelInitialized(
+ ContextHolder.applicationContext
+ ?: throw RuntimeException("Context not initialised!")
+ )
+ }
+ }
+}
\ No newline at end of file
diff --git a/android/src/main/kotlin/com/shounakmulay/telephony/sms/SmsController.kt b/android/src/main/kotlin/com/shounakmulay/telephony/sms/SmsController.kt
new file mode 100644
index 0000000..dea8ce4
--- /dev/null
+++ b/android/src/main/kotlin/com/shounakmulay/telephony/sms/SmsController.kt
@@ -0,0 +1,266 @@
+package com.shounakmulay.telephony.sms
+
+import android.Manifest
+import android.Manifest.permission.READ_PHONE_STATE
+import android.annotation.SuppressLint
+import android.app.PendingIntent
+import android.content.Context
+import android.content.Intent
+import android.content.pm.PackageManager
+import android.net.Uri
+import android.os.Build
+import android.telephony.*
+import androidx.annotation.RequiresApi
+import androidx.annotation.RequiresPermission
+import androidx.core.content.ContextCompat
+import androidx.core.content.ContextCompat.getSystemService
+import com.shounakmulay.telephony.utils.Constants.ACTION_SMS_DELIVERED
+import com.shounakmulay.telephony.utils.Constants.ACTION_SMS_SENT
+import com.shounakmulay.telephony.utils.Constants.SMS_BODY
+import com.shounakmulay.telephony.utils.Constants.SMS_DELIVERED_BROADCAST_REQUEST_CODE
+import com.shounakmulay.telephony.utils.Constants.SMS_SENT_BROADCAST_REQUEST_CODE
+import com.shounakmulay.telephony.utils.Constants.SMS_TO
+import com.shounakmulay.telephony.utils.ContentUri
+import java.lang.RuntimeException
+
+
+class SmsController(private val context: Context) {
+
+ // FETCH SMS
+ fun getMessages(
+ contentUri: ContentUri,
+ projection: List,
+ selection: String?,
+ selectionArgs: List?,
+ sortOrder: String?
+ ): List> {
+ val messages = mutableListOf>()
+
+ val cursor = context.contentResolver.query(
+ contentUri.uri,
+ projection.toTypedArray(),
+ selection,
+ selectionArgs?.toTypedArray(),
+ sortOrder
+ )
+
+ while (cursor != null && cursor.moveToNext()) {
+ val dataObject = HashMap(projection.size)
+ for (columnName in cursor.columnNames) {
+ val columnIndex = cursor.getColumnIndex(columnName)
+ if (columnIndex >= 0) {
+ val value = cursor.getString(columnIndex)
+ dataObject[columnName] = value
+ }
+ }
+ messages.add(dataObject)
+ }
+ cursor?.close()
+ return messages
+ }
+
+ // SEND SMS
+ fun sendSms(destinationAddress: String, messageBody: String, listenStatus: Boolean) {
+ val smsManager = getSmsManager()
+ if (listenStatus) {
+ val pendingIntents = getPendingIntents()
+ smsManager.sendTextMessage(
+ destinationAddress,
+ null,
+ messageBody,
+ pendingIntents.first,
+ pendingIntents.second
+ )
+ } else {
+ smsManager.sendTextMessage(destinationAddress, null, messageBody, null, null)
+ }
+ }
+
+ fun sendMultipartSms(destinationAddress: String, messageBody: String, listenStatus: Boolean) {
+ val smsManager = getSmsManager()
+ val messageParts = smsManager.divideMessage(messageBody)
+ if (listenStatus) {
+ val pendingIntents = getMultiplePendingIntents(messageParts.size)
+ smsManager.sendMultipartTextMessage(
+ destinationAddress,
+ null,
+ messageParts,
+ pendingIntents.first,
+ pendingIntents.second
+ )
+ } else {
+ smsManager.sendMultipartTextMessage(destinationAddress, null, messageParts, null, null)
+ }
+ }
+
+ private fun getMultiplePendingIntents(size: Int): Pair, ArrayList> {
+ val sentPendingIntents = arrayListOf()
+ val deliveredPendingIntents = arrayListOf()
+ for (i in 1..size) {
+ val pendingIntents = getPendingIntents()
+ sentPendingIntents.add(pendingIntents.first)
+ deliveredPendingIntents.add(pendingIntents.second)
+ }
+ return Pair(sentPendingIntents, deliveredPendingIntents)
+ }
+
+ fun sendSmsIntent(destinationAddress: String, messageBody: String) {
+ val intent = Intent(Intent.ACTION_SENDTO).apply {
+ data = Uri.parse(SMS_TO + destinationAddress)
+ putExtra(SMS_BODY, messageBody)
+ flags = Intent.FLAG_ACTIVITY_NEW_TASK
+ }
+ context.applicationContext.startActivity(intent)
+ }
+
+ private fun getPendingIntents(): Pair {
+ val sentIntent = Intent(ACTION_SMS_SENT).apply {
+ `package` = context.applicationContext.packageName
+ flags = Intent.FLAG_RECEIVER_REGISTERED_ONLY
+ }
+ val sentPendingIntent = PendingIntent.getBroadcast(
+ context,
+ SMS_SENT_BROADCAST_REQUEST_CODE,
+ sentIntent,
+ PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_IMMUTABLE
+ )
+
+ val deliveredIntent = Intent(ACTION_SMS_DELIVERED).apply {
+ `package` = context.applicationContext.packageName
+ flags = Intent.FLAG_RECEIVER_REGISTERED_ONLY
+ }
+ val deliveredPendingIntent = PendingIntent.getBroadcast(
+ context,
+ SMS_DELIVERED_BROADCAST_REQUEST_CODE,
+ deliveredIntent,
+ PendingIntent.FLAG_ONE_SHOT or PendingIntent.FLAG_IMMUTABLE
+ )
+
+ return Pair(sentPendingIntent, deliveredPendingIntent)
+ }
+
+ private fun getSmsManager(): SmsManager {
+ val subscriptionId = SmsManager.getDefaultSmsSubscriptionId()
+// val smsManager = getSystemService(context, SmsManager::class.java)
+ val smsManager = SmsManager.getDefault()
+ ?: throw RuntimeException("Flutter Telephony: Error getting SmsManager")
+ if (subscriptionId != SubscriptionManager.INVALID_SUBSCRIPTION_ID) {
+ return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
+ smsManager.createForSubscriptionId(subscriptionId)
+ } else {
+ SmsManager.getSmsManagerForSubscriptionId(subscriptionId)
+ }
+ }
+ return smsManager
+ }
+
+ // PHONE
+ fun openDialer(phoneNumber: String) {
+ val dialerIntent = Intent(Intent.ACTION_DIAL).apply {
+ data = Uri.parse("tel:$phoneNumber")
+ flags = Intent.FLAG_ACTIVITY_NEW_TASK
+ }
+
+ context.startActivity(dialerIntent)
+ }
+
+ @RequiresPermission(allOf = [Manifest.permission.CALL_PHONE])
+ fun dialPhoneNumber(phoneNumber: String) {
+ val callIntent = Intent(Intent.ACTION_CALL).apply {
+ data = Uri.parse("tel:$phoneNumber")
+ flags = Intent.FLAG_ACTIVITY_NEW_TASK
+ }
+
+ if (callIntent.resolveActivity(context.packageManager) != null) {
+ context.applicationContext.startActivity(callIntent)
+ }
+ }
+
+ // STATUS
+ fun isSmsCapable(): Boolean {
+ val telephonyManager = getTelephonyManager()
+ return telephonyManager.isSmsCapable
+ }
+
+ fun getCellularDataState(): Int {
+ return getTelephonyManager().dataState
+ }
+
+ @RequiresPermission(Manifest.permission.READ_PHONE_STATE)
+ fun getCallState(): Int {
+ val telephonyManager = getTelephonyManager()
+ return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
+ telephonyManager.callStateForSubscription
+ } else {
+ telephonyManager.callState
+ }
+ }
+
+ fun getDataActivity(): Int {
+ return getTelephonyManager().dataActivity
+ }
+
+ fun getNetworkOperator(): String {
+ return getTelephonyManager().networkOperator
+ }
+
+ fun getNetworkOperatorName(): String {
+ return getTelephonyManager().networkOperatorName
+ }
+
+ @SuppressLint("MissingPermission")
+ fun getDataNetworkType(): Int {
+ val telephonyManager = getTelephonyManager()
+ return if (Build.VERSION.SDK_INT > Build.VERSION_CODES.Q) {
+ telephonyManager.dataNetworkType
+ } else {
+ telephonyManager.networkType
+ }
+ }
+
+ fun getPhoneType(): Int {
+ return getTelephonyManager().phoneType
+ }
+
+ fun getSimOperator(): String {
+ return getTelephonyManager().simOperator
+ }
+
+ fun getSimOperatorName(): String {
+ return getTelephonyManager().simOperatorName
+ }
+
+ fun getSimState(): Int {
+ return getTelephonyManager().simState
+ }
+
+ fun isNetworkRoaming(): Boolean {
+ return getTelephonyManager().isNetworkRoaming
+ }
+
+ @RequiresApi(Build.VERSION_CODES.O)
+ @RequiresPermission(allOf = [Manifest.permission.ACCESS_COARSE_LOCATION, Manifest.permission.READ_PHONE_STATE])
+ fun getServiceState(): Int? {
+ val serviceState = getTelephonyManager().serviceState
+ return serviceState?.state
+ }
+
+ @RequiresApi(Build.VERSION_CODES.Q)
+ fun getSignalStrength(): List? {
+ val signalStrength = getTelephonyManager().signalStrength
+ return signalStrength?.cellSignalStrengths?.map {
+ return@map it.level
+ }
+ }
+
+ private fun getTelephonyManager(): TelephonyManager {
+ val subscriptionId = SmsManager.getDefaultSmsSubscriptionId()
+ val telephonyManager =
+ context.getSystemService(Context.TELEPHONY_SERVICE) as TelephonyManager
+ return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
+ telephonyManager.createForSubscriptionId(subscriptionId)
+ } else {
+ telephonyManager
+ }
+ }
+}
\ No newline at end of file
diff --git a/android/src/main/kotlin/com/shounakmulay/telephony/sms/SmsMethodCallHandler.kt b/android/src/main/kotlin/com/shounakmulay/telephony/sms/SmsMethodCallHandler.kt
new file mode 100644
index 0000000..e220ba2
--- /dev/null
+++ b/android/src/main/kotlin/com/shounakmulay/telephony/sms/SmsMethodCallHandler.kt
@@ -0,0 +1,401 @@
+package com.shounakmulay.telephony.sms
+
+import android.annotation.SuppressLint
+import android.app.Activity
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import android.content.IntentFilter
+import android.content.pm.PackageManager
+import android.os.Build
+import androidx.annotation.RequiresApi
+import com.shounakmulay.telephony.PermissionsController
+import com.shounakmulay.telephony.utils.ActionType
+import com.shounakmulay.telephony.utils.Constants
+import com.shounakmulay.telephony.utils.Constants.ADDRESS
+import com.shounakmulay.telephony.utils.Constants.BACKGROUND_HANDLE
+import com.shounakmulay.telephony.utils.Constants.CALL_REQUEST_CODE
+import com.shounakmulay.telephony.utils.Constants.DEFAULT_CONVERSATION_PROJECTION
+import com.shounakmulay.telephony.utils.Constants.DEFAULT_SMS_PROJECTION
+import com.shounakmulay.telephony.utils.Constants.FAILED_FETCH
+import com.shounakmulay.telephony.utils.Constants.GET_STATUS_REQUEST_CODE
+import com.shounakmulay.telephony.utils.Constants.ILLEGAL_ARGUMENT
+import com.shounakmulay.telephony.utils.Constants.LISTEN_STATUS
+import com.shounakmulay.telephony.utils.Constants.MESSAGE_BODY
+import com.shounakmulay.telephony.utils.Constants.PERMISSION_DENIED
+import com.shounakmulay.telephony.utils.Constants.PERMISSION_DENIED_MESSAGE
+import com.shounakmulay.telephony.utils.Constants.PERMISSION_REQUEST_CODE
+import com.shounakmulay.telephony.utils.Constants.PHONE_NUMBER
+import com.shounakmulay.telephony.utils.Constants.PROJECTION
+import com.shounakmulay.telephony.utils.Constants.SELECTION
+import com.shounakmulay.telephony.utils.Constants.SELECTION_ARGS
+import com.shounakmulay.telephony.utils.Constants.SETUP_HANDLE
+import com.shounakmulay.telephony.utils.Constants.SHARED_PREFERENCES_NAME
+import com.shounakmulay.telephony.utils.Constants.SHARED_PREFS_DISABLE_BACKGROUND_EXE
+import com.shounakmulay.telephony.utils.Constants.SMS_BACKGROUND_REQUEST_CODE
+import com.shounakmulay.telephony.utils.Constants.SMS_DELIVERED
+import com.shounakmulay.telephony.utils.Constants.SMS_QUERY_REQUEST_CODE
+import com.shounakmulay.telephony.utils.Constants.SMS_SEND_REQUEST_CODE
+import com.shounakmulay.telephony.utils.Constants.SMS_SENT
+import com.shounakmulay.telephony.utils.Constants.SORT_ORDER
+import com.shounakmulay.telephony.utils.Constants.WRONG_METHOD_TYPE
+import com.shounakmulay.telephony.utils.ContentUri
+import com.shounakmulay.telephony.utils.SmsAction
+import io.flutter.plugin.common.MethodCall
+import io.flutter.plugin.common.MethodChannel
+import io.flutter.plugin.common.PluginRegistry
+
+
+class SmsMethodCallHandler(
+ private val context: Context,
+ private val smsController: SmsController,
+ private val permissionsController: PermissionsController
+) : PluginRegistry.RequestPermissionsResultListener,
+ MethodChannel.MethodCallHandler,
+ BroadcastReceiver() {
+
+ private lateinit var result: MethodChannel.Result
+ private lateinit var action: SmsAction
+ private lateinit var foregroundChannel: MethodChannel
+ private lateinit var activity: Activity
+
+ private var projection: List? = null
+ private var selection: String? = null
+ private var selectionArgs: List? = null
+ private var sortOrder: String? = null
+
+ private lateinit var messageBody: String
+ private lateinit var address: String
+ private var listenStatus: Boolean = false
+
+ private var setupHandle: Long = -1
+ private var backgroundHandle: Long = -1
+
+ private lateinit var phoneNumber: String
+
+ private var requestCode: Int = -1
+
+ override fun onMethodCall(call: MethodCall, result: MethodChannel.Result) {
+ this.result = result
+
+ action = SmsAction.fromMethod(call.method)
+
+ if (action == SmsAction.NO_SUCH_METHOD) {
+ result.notImplemented()
+ return
+ }
+
+ when (action.toActionType()) {
+ ActionType.GET_SMS -> {
+ projection = call.argument(PROJECTION)
+ selection = call.argument(SELECTION)
+ selectionArgs = call.argument(SELECTION_ARGS)
+ sortOrder = call.argument(SORT_ORDER)
+
+ handleMethod(action, SMS_QUERY_REQUEST_CODE)
+ }
+ ActionType.SEND_SMS -> {
+ if (call.hasArgument(MESSAGE_BODY)
+ && call.hasArgument(ADDRESS)) {
+ val messageBody = call.argument(MESSAGE_BODY)
+ val address = call.argument(ADDRESS)
+ if (messageBody.isNullOrBlank() || address.isNullOrBlank()) {
+ result.error(ILLEGAL_ARGUMENT, Constants.MESSAGE_OR_ADDRESS_CANNOT_BE_NULL, null)
+ return
+ }
+
+ this.messageBody = messageBody
+ this.address = address
+
+ listenStatus = call.argument(LISTEN_STATUS) ?: false
+ }
+ handleMethod(action, SMS_SEND_REQUEST_CODE)
+ }
+ ActionType.BACKGROUND -> {
+ if (call.hasArgument(SETUP_HANDLE)
+ && call.hasArgument(BACKGROUND_HANDLE)) {
+ val setupHandle = call.argument(SETUP_HANDLE)
+ val backgroundHandle = call.argument(BACKGROUND_HANDLE)
+ if (setupHandle == null || backgroundHandle == null) {
+ result.error(ILLEGAL_ARGUMENT, "Setup handle or background handle missing", null)
+ return
+ }
+
+ this.setupHandle = setupHandle
+ this.backgroundHandle = backgroundHandle
+ }
+ handleMethod(action, SMS_BACKGROUND_REQUEST_CODE)
+ }
+ ActionType.GET -> handleMethod(action, GET_STATUS_REQUEST_CODE)
+ ActionType.PERMISSION -> handleMethod(action, PERMISSION_REQUEST_CODE)
+ ActionType.CALL -> {
+ if (call.hasArgument(PHONE_NUMBER)) {
+ val phoneNumber = call.argument(PHONE_NUMBER)
+
+ if (!phoneNumber.isNullOrBlank()) {
+ this.phoneNumber = phoneNumber
+ }
+
+ handleMethod(action, CALL_REQUEST_CODE)
+ }
+ }
+ }
+ }
+
+ /**
+ * Called by [handleMethod] after checking the permissions.
+ *
+ * #####
+ *
+ * If permission was not previously granted, [handleMethod] will request the user for permission
+ *
+ * Once user grants the permission this method will be executed.
+ *
+ * #####
+ */
+ private fun execute(smsAction: SmsAction) {
+ try {
+ when (smsAction.toActionType()) {
+ ActionType.GET_SMS -> handleGetSmsActions(smsAction)
+ ActionType.SEND_SMS -> handleSendSmsActions(smsAction)
+ ActionType.BACKGROUND -> handleBackgroundActions(smsAction)
+ ActionType.GET -> handleGetActions(smsAction)
+ ActionType.PERMISSION -> result.success(true)
+ ActionType.CALL -> handleCallActions(smsAction)
+ }
+ } catch (e: IllegalArgumentException) {
+ result.error(ILLEGAL_ARGUMENT, WRONG_METHOD_TYPE, null)
+ } catch (e: RuntimeException) {
+ result.error(FAILED_FETCH, e.message, null)
+ }
+ }
+
+ private fun handleGetSmsActions(smsAction: SmsAction) {
+ if (projection == null) {
+ projection = if (smsAction == SmsAction.GET_CONVERSATIONS) DEFAULT_CONVERSATION_PROJECTION else DEFAULT_SMS_PROJECTION
+ }
+ val contentUri = when (smsAction) {
+ SmsAction.GET_INBOX -> ContentUri.INBOX
+ SmsAction.GET_SENT -> ContentUri.SENT
+ SmsAction.GET_DRAFT -> ContentUri.DRAFT
+ SmsAction.GET_CONVERSATIONS -> ContentUri.CONVERSATIONS
+ else -> throw IllegalArgumentException()
+ }
+ val messages = smsController.getMessages(contentUri, projection!!, selection, selectionArgs, sortOrder)
+ result.success(messages)
+ }
+
+ private fun handleSendSmsActions(smsAction: SmsAction) {
+ if (listenStatus) {
+ val intentFilter = IntentFilter().apply {
+ addAction(Constants.ACTION_SMS_SENT)
+ addAction(Constants.ACTION_SMS_DELIVERED)
+ }
+ context.applicationContext.registerReceiver(this, intentFilter)
+ }
+ when (smsAction) {
+ SmsAction.SEND_SMS -> smsController.sendSms(address, messageBody, listenStatus)
+ SmsAction.SEND_MULTIPART_SMS -> smsController.sendMultipartSms(address, messageBody, listenStatus)
+ SmsAction.SEND_SMS_INTENT -> smsController.sendSmsIntent(address, messageBody)
+ else -> throw IllegalArgumentException()
+ }
+ result.success(null)
+ }
+
+ private fun handleBackgroundActions(smsAction: SmsAction) {
+ when (smsAction) {
+ SmsAction.START_BACKGROUND_SERVICE -> {
+ val preferences = context.getSharedPreferences(SHARED_PREFERENCES_NAME, Context.MODE_PRIVATE)
+ preferences.edit().putBoolean(SHARED_PREFS_DISABLE_BACKGROUND_EXE, false).apply()
+ IncomingSmsHandler.setBackgroundSetupHandle(context, setupHandle)
+ IncomingSmsHandler.setBackgroundMessageHandle(context, backgroundHandle)
+ }
+ SmsAction.BACKGROUND_SERVICE_INITIALIZED -> {
+ IncomingSmsHandler.onChannelInitialized(context.applicationContext)
+ }
+ SmsAction.DISABLE_BACKGROUND_SERVICE -> {
+ val preferences = context.getSharedPreferences(SHARED_PREFERENCES_NAME, Context.MODE_PRIVATE)
+ preferences.edit().putBoolean(SHARED_PREFS_DISABLE_BACKGROUND_EXE, true).apply()
+ }
+ else -> throw IllegalArgumentException()
+ }
+ }
+
+ @SuppressLint("MissingPermission")
+ private fun handleGetActions(smsAction: SmsAction) {
+ smsController.apply {
+ val value: Any = when (smsAction) {
+ SmsAction.IS_SMS_CAPABLE -> isSmsCapable()
+ SmsAction.GET_CELLULAR_DATA_STATE -> getCellularDataState()
+ SmsAction.GET_CALL_STATE -> getCallState()
+ SmsAction.GET_DATA_ACTIVITY -> getDataActivity()
+ SmsAction.GET_NETWORK_OPERATOR -> getNetworkOperator()
+ SmsAction.GET_NETWORK_OPERATOR_NAME -> getNetworkOperatorName()
+ SmsAction.GET_DATA_NETWORK_TYPE -> getDataNetworkType()
+ SmsAction.GET_PHONE_TYPE -> getPhoneType()
+ SmsAction.GET_SIM_OPERATOR -> getSimOperator()
+ SmsAction.GET_SIM_OPERATOR_NAME -> getSimOperatorName()
+ SmsAction.GET_SIM_STATE -> getSimState()
+ SmsAction.IS_NETWORK_ROAMING -> isNetworkRoaming()
+ SmsAction.GET_SIGNAL_STRENGTH -> {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
+ getSignalStrength()
+ ?: result.error("SERVICE_STATE_NULL", "Error getting service state", null)
+
+ } else {
+ result.error("INCORRECT_SDK_VERSION", "getServiceState() can only be called on Android Q and above", null)
+ }
+ }
+ SmsAction.GET_SERVICE_STATE -> {
+ if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+ getServiceState()
+ ?: result.error("SERVICE_STATE_NULL", "Error getting service state", null)
+ } else {
+ result.error("INCORRECT_SDK_VERSION", "getServiceState() can only be called on Android O and above", null)
+ }
+ }
+ else -> throw IllegalArgumentException()
+ }
+ result.success(value)
+ }
+ }
+
+ @SuppressLint("MissingPermission")
+ private fun handleCallActions(smsAction: SmsAction) {
+ when (smsAction) {
+ SmsAction.OPEN_DIALER -> smsController.openDialer(phoneNumber)
+ SmsAction.DIAL_PHONE_NUMBER -> smsController.dialPhoneNumber(phoneNumber)
+ else -> throw IllegalArgumentException()
+ }
+ }
+
+
+ /**
+ * Calls the [execute] method after checking if the necessary permissions are granted.
+ *
+ * If not granted then it will request the permission from the user.
+ */
+ private fun handleMethod(smsAction: SmsAction, requestCode: Int) {
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.M || checkOrRequestPermission(smsAction, requestCode)) {
+ execute(smsAction)
+ }
+ }
+
+ /**
+ * Check and request if necessary for all the SMS permissions listed in the manifest
+ */
+ @RequiresApi(Build.VERSION_CODES.M)
+ fun checkOrRequestPermission(smsAction: SmsAction, requestCode: Int): Boolean {
+ this.action = smsAction
+ this.requestCode = requestCode
+ when (smsAction) {
+ SmsAction.GET_INBOX,
+ SmsAction.GET_SENT,
+ SmsAction.GET_DRAFT,
+ SmsAction.GET_CONVERSATIONS,
+ SmsAction.SEND_SMS,
+ SmsAction.SEND_MULTIPART_SMS,
+ SmsAction.SEND_SMS_INTENT,
+ SmsAction.START_BACKGROUND_SERVICE,
+ SmsAction.BACKGROUND_SERVICE_INITIALIZED,
+ SmsAction.DISABLE_BACKGROUND_SERVICE,
+ SmsAction.REQUEST_SMS_PERMISSIONS -> {
+ val permissions = permissionsController.getSmsPermissions()
+ return checkOrRequestPermission(permissions, requestCode)
+ }
+ SmsAction.GET_DATA_NETWORK_TYPE,
+ SmsAction.OPEN_DIALER,
+ SmsAction.DIAL_PHONE_NUMBER,
+ SmsAction.REQUEST_PHONE_PERMISSIONS -> {
+ val permissions = permissionsController.getPhonePermissions()
+ return checkOrRequestPermission(permissions, requestCode)
+ }
+ SmsAction.GET_SERVICE_STATE -> {
+ val permissions = permissionsController.getServiceStatePermissions()
+ return checkOrRequestPermission(permissions, requestCode)
+ }
+ SmsAction.REQUEST_PHONE_AND_SMS_PERMISSIONS -> {
+ val permissions = listOf(permissionsController.getSmsPermissions(), permissionsController.getPhonePermissions()).flatten()
+ return checkOrRequestPermission(permissions, requestCode)
+ }
+ SmsAction.IS_SMS_CAPABLE,
+ SmsAction.GET_CELLULAR_DATA_STATE,
+ SmsAction.GET_CALL_STATE,
+ SmsAction.GET_DATA_ACTIVITY,
+ SmsAction.GET_NETWORK_OPERATOR,
+ SmsAction.GET_NETWORK_OPERATOR_NAME,
+ SmsAction.GET_PHONE_TYPE,
+ SmsAction.GET_SIM_OPERATOR,
+ SmsAction.GET_SIM_OPERATOR_NAME,
+ SmsAction.GET_SIM_STATE,
+ SmsAction.IS_NETWORK_ROAMING,
+ SmsAction.GET_SIGNAL_STRENGTH,
+ SmsAction.NO_SUCH_METHOD -> return true
+ }
+ }
+
+ fun setActivity(activity: Activity) {
+ this.activity = activity
+ }
+
+ @RequiresApi(Build.VERSION_CODES.M)
+ private fun checkOrRequestPermission(permissions: List, requestCode: Int): Boolean {
+ permissionsController.apply {
+
+ if (!::activity.isInitialized) {
+ return hasRequiredPermissions(permissions)
+ }
+
+ if (!hasRequiredPermissions(permissions)) {
+ requestPermissions(activity, permissions, requestCode)
+ return false
+ }
+ return true
+ }
+ }
+
+ override fun onRequestPermissionsResult(requestCode: Int, permissions: Array, grantResults: IntArray): Boolean {
+
+ permissionsController.isRequestingPermission = false
+
+ val deniedPermissions = mutableListOf()
+ if (requestCode != this.requestCode && !this::action.isInitialized) {
+ return false
+ }
+
+ val allPermissionGranted = grantResults.foldIndexed(true) { i, acc, result ->
+ if (result == PackageManager.PERMISSION_DENIED) {
+ permissions.let { deniedPermissions.add(it[i]) }
+ }
+ return@foldIndexed acc && result == PackageManager.PERMISSION_GRANTED
+ }
+
+ return if (allPermissionGranted) {
+ execute(action)
+ true
+ } else {
+ onPermissionDenied(deniedPermissions)
+ false
+ }
+ }
+
+ private fun onPermissionDenied(deniedPermissions: List) {
+ result.error(PERMISSION_DENIED, PERMISSION_DENIED_MESSAGE, deniedPermissions)
+ }
+
+ fun setForegroundChannel(channel: MethodChannel) {
+ foregroundChannel = channel
+ }
+
+ override fun onReceive(ctx: Context?, intent: Intent?) {
+ if (intent != null) {
+ when (intent.action) {
+ Constants.ACTION_SMS_SENT -> foregroundChannel.invokeMethod(SMS_SENT, null)
+ Constants.ACTION_SMS_DELIVERED -> {
+ foregroundChannel.invokeMethod(SMS_DELIVERED, null)
+ context.unregisterReceiver(this)
+ }
+ }
+ }
+ }
+}
diff --git a/android/src/main/kotlin/com/shounakmulay/telephony/utils/Constants.kt b/android/src/main/kotlin/com/shounakmulay/telephony/utils/Constants.kt
new file mode 100644
index 0000000..df79791
--- /dev/null
+++ b/android/src/main/kotlin/com/shounakmulay/telephony/utils/Constants.kt
@@ -0,0 +1,84 @@
+package com.shounakmulay.telephony.utils
+
+import android.Manifest
+import android.provider.Telephony
+
+object Constants {
+
+ // Channels
+ const val CHANNEL_SMS = "plugins.shounakmulay.com/foreground_sms_channel"
+ const val CHANNEL_SMS_BACKGROUND = "plugins.shounakmulay.com/background_sms_channel"
+
+ // Intent Actions
+ const val ACTION_SMS_SENT = "plugins.shounakmulay.intent.ACTION_SMS_SENT"
+ const val ACTION_SMS_DELIVERED = "plugins.shounakmulay.intent.ACTION_SMS_DELIVERED"
+
+
+
+ // Permissions
+ val SMS_PERMISSIONS = listOf(Manifest.permission.READ_SMS, Manifest.permission.SEND_SMS, Manifest.permission.RECEIVE_SMS)
+ val PHONE_PERMISSIONS = listOf(Manifest.permission.READ_PHONE_STATE, Manifest.permission.CALL_PHONE)
+ val SERVICE_STATE_PERMISSIONS = listOf(Manifest.permission.ACCESS_COARSE_LOCATION, Manifest.permission.READ_PHONE_STATE)
+
+ // Request Codes
+ const val SMS_QUERY_REQUEST_CODE = 1
+ const val SMS_SEND_REQUEST_CODE = 2
+ const val SMS_SENT_BROADCAST_REQUEST_CODE = 21
+ const val SMS_DELIVERED_BROADCAST_REQUEST_CODE = 22
+ const val SMS_BACKGROUND_REQUEST_CODE = 31
+ const val GET_STATUS_REQUEST_CODE = 41
+ const val PERMISSION_REQUEST_CODE = 51
+ const val CALL_REQUEST_CODE = 61
+
+ // Methods
+ const val ON_MESSAGE = "onMessage"
+ const val HANDLE_BACKGROUND_MESSAGE = "handleBackgroundMessage"
+ const val SMS_SENT = "smsSent"
+ const val SMS_DELIVERED = "smsDelivered"
+
+ // Invoke Method Arguments
+ const val HANDLE = "handle"
+ const val MESSAGE = "message"
+
+ // Method Call Arguments
+ const val PROJECTION = "projection"
+ const val SELECTION = "selection"
+ const val SELECTION_ARGS = "selection_args"
+ const val SORT_ORDER = "sort_order"
+ const val MESSAGE_BODY = "message_body"
+ const val ADDRESS = "address"
+ const val LISTEN_STATUS = "listen_status"
+ const val SERVICE_CENTER_ADDRESS = "service_center"
+
+ const val TIMESTAMP = "timestamp"
+ const val ORIGINATING_ADDRESS = "originating_address"
+ const val STATUS = "status"
+
+ const val SETUP_HANDLE = "setupHandle"
+ const val BACKGROUND_HANDLE = "backgroundHandle"
+
+ const val PHONE_NUMBER = "phoneNumber"
+
+ // Projections
+ val DEFAULT_SMS_PROJECTION = listOf(Telephony.Sms._ID, Telephony.Sms.ADDRESS, Telephony.Sms.BODY, Telephony.Sms.DATE)
+ val DEFAULT_CONVERSATION_PROJECTION = listOf(Telephony.Sms.Conversations.THREAD_ID ,Telephony.Sms.Conversations.SNIPPET, Telephony.Sms.Conversations.MESSAGE_COUNT)
+
+
+ // Strings
+ const val PERMISSION_DENIED = "permission_denied"
+ const val PERMISSION_DENIED_MESSAGE = "Permission Request Denied By User."
+ const val FAILED_FETCH = "failed_to_fetch_sms"
+ const val ILLEGAL_ARGUMENT = "illegal_argument"
+ const val WRONG_METHOD_TYPE = "Incorrect method called on channel."
+ const val MESSAGE_OR_ADDRESS_CANNOT_BE_NULL = "Message body or Address cannot be null or blank."
+
+ const val SMS_TO = "smsto:"
+ const val SMS_BODY = "sms_body"
+
+ // Shared Preferences
+ const val SHARED_PREFERENCES_NAME = "com.shounakmulay.android_telephony_plugin"
+ const val SHARED_PREFS_BACKGROUND_SETUP_HANDLE = "background_setup_handle"
+ const val SHARED_PREFS_BACKGROUND_MESSAGE_HANDLE = "background_message_handle"
+ const val SHARED_PREFS_DISABLE_BACKGROUND_EXE = "disable_background"
+
+}
\ No newline at end of file
diff --git a/android/src/main/kotlin/com/shounakmulay/telephony/utils/SmsEnums.kt b/android/src/main/kotlin/com/shounakmulay/telephony/utils/SmsEnums.kt
new file mode 100644
index 0000000..a1d29ce
--- /dev/null
+++ b/android/src/main/kotlin/com/shounakmulay/telephony/utils/SmsEnums.kt
@@ -0,0 +1,94 @@
+package com.shounakmulay.telephony.utils
+
+import android.net.Uri
+import android.provider.Telephony
+
+enum class SmsAction(private val methodName: String) {
+ GET_INBOX("getAllInboxSms"),
+ GET_SENT("getAllSentSms"),
+ GET_DRAFT("getAllDraftSms"),
+ GET_CONVERSATIONS("getAllConversations"),
+ SEND_SMS("sendSms"),
+ SEND_MULTIPART_SMS("sendMultipartSms"),
+ SEND_SMS_INTENT("sendSmsIntent"),
+ START_BACKGROUND_SERVICE("startBackgroundService"),
+ DISABLE_BACKGROUND_SERVICE("disableBackgroundService"),
+ BACKGROUND_SERVICE_INITIALIZED("backgroundServiceInitialized"),
+ IS_SMS_CAPABLE("isSmsCapable"),
+ GET_CELLULAR_DATA_STATE("getCellularDataState"),
+ GET_CALL_STATE("getCallState"),
+ GET_DATA_ACTIVITY("getDataActivity"),
+ GET_NETWORK_OPERATOR("getNetworkOperator"),
+ GET_NETWORK_OPERATOR_NAME("getNetworkOperatorName"),
+ GET_DATA_NETWORK_TYPE("getDataNetworkType"),
+ GET_PHONE_TYPE("getPhoneType"),
+ GET_SIM_OPERATOR("getSimOperator"),
+ GET_SIM_OPERATOR_NAME("getSimOperatorName"),
+ GET_SIM_STATE("getSimState"),
+ GET_SERVICE_STATE("getServiceState"),
+ GET_SIGNAL_STRENGTH("getSignalStrength"),
+ IS_NETWORK_ROAMING("isNetworkRoaming"),
+ REQUEST_SMS_PERMISSIONS("requestSmsPermissions"),
+ REQUEST_PHONE_PERMISSIONS("requestPhonePermissions"),
+ REQUEST_PHONE_AND_SMS_PERMISSIONS("requestPhoneAndSmsPermissions"),
+ OPEN_DIALER("openDialer"),
+ DIAL_PHONE_NUMBER("dialPhoneNumber"),
+ NO_SUCH_METHOD("noSuchMethod");
+
+ companion object {
+ fun fromMethod(method: String): SmsAction {
+ for (action in values()) {
+ if (action.methodName == method) {
+ return action
+ }
+ }
+ return NO_SUCH_METHOD
+ }
+ }
+
+ fun toActionType(): ActionType {
+ return when (this) {
+ GET_INBOX,
+ GET_SENT,
+ GET_DRAFT,
+ GET_CONVERSATIONS -> ActionType.GET_SMS
+ SEND_SMS,
+ SEND_MULTIPART_SMS,
+ SEND_SMS_INTENT,
+ NO_SUCH_METHOD -> ActionType.SEND_SMS
+ START_BACKGROUND_SERVICE,
+ DISABLE_BACKGROUND_SERVICE,
+ BACKGROUND_SERVICE_INITIALIZED -> ActionType.BACKGROUND
+ IS_SMS_CAPABLE,
+ GET_CELLULAR_DATA_STATE,
+ GET_CALL_STATE,
+ GET_DATA_ACTIVITY,
+ GET_NETWORK_OPERATOR,
+ GET_NETWORK_OPERATOR_NAME,
+ GET_DATA_NETWORK_TYPE,
+ GET_PHONE_TYPE,
+ GET_SIM_OPERATOR,
+ GET_SIM_OPERATOR_NAME,
+ GET_SIM_STATE,
+ GET_SERVICE_STATE,
+ GET_SIGNAL_STRENGTH,
+ IS_NETWORK_ROAMING -> ActionType.GET
+ REQUEST_SMS_PERMISSIONS,
+ REQUEST_PHONE_PERMISSIONS,
+ REQUEST_PHONE_AND_SMS_PERMISSIONS -> ActionType.PERMISSION
+ OPEN_DIALER,
+ DIAL_PHONE_NUMBER -> ActionType.CALL
+ }
+ }
+}
+
+enum class ActionType {
+ GET_SMS, SEND_SMS, BACKGROUND, GET, PERMISSION, CALL
+}
+
+enum class ContentUri(val uri: Uri) {
+ INBOX(Telephony.Sms.Inbox.CONTENT_URI),
+ SENT(Telephony.Sms.Sent.CONTENT_URI),
+ DRAFT(Telephony.Sms.Draft.CONTENT_URI),
+ CONVERSATIONS(Telephony.Sms.Conversations.CONTENT_URI);
+}
\ No newline at end of file
diff --git a/coverage/lcov.info b/coverage/lcov.info
new file mode 100644
index 0000000..4ccea8c
--- /dev/null
+++ b/coverage/lcov.info
@@ -0,0 +1,328 @@
+SF:lib\filter.dart
+DA:17,1
+DA:19,1
+DA:20,2
+DA:22,1
+DA:23,1
+DA:26,1
+DA:28,1
+DA:31,1
+DA:32,3
+DA:33,3
+DA:36,1
+DA:38,1
+DA:40,1
+DA:42,1
+DA:50,1
+DA:52,1
+DA:53,2
+DA:55,1
+DA:57,1
+DA:60,1
+DA:62,1
+DA:65,1
+DA:67,3
+DA:68,3
+DA:71,1
+DA:72,1
+DA:74,1
+DA:75,1
+DA:83,1
+DA:85,1
+DA:91,1
+DA:92,1
+DA:95,1
+DA:96,1
+DA:99,0
+DA:100,0
+DA:103,1
+DA:104,1
+DA:107,1
+DA:108,1
+DA:111,1
+DA:112,1
+DA:115,1
+DA:116,1
+DA:119,0
+DA:120,0
+DA:121,0
+DA:124,1
+DA:125,1
+DA:126,1
+DA:129,1
+DA:130,2
+DA:139,2
+DA:141,1
+DA:143,1
+DA:145,1
+DA:147,1
+DA:148,4
+DA:149,2
+DA:151,4
+DA:158,2
+DA:160,1
+DA:162,1
+DA:164,1
+DA:166,1
+DA:167,4
+DA:168,2
+DA:170,4
+DA:179,1
+DA:181,1
+DA:185,6
+LF:71
+LH:66
+end_of_record
+SF:lib\constants.dart
+DA:78,0
+DA:86,0
+DA:101,1
+DA:102,1
+DA:108,0
+DA:116,1
+DA:117,1
+DA:133,2
+DA:134,2
+DA:135,2
+DA:136,2
+DA:137,2
+DA:138,2
+DA:139,2
+DA:140,2
+DA:143,10
+DA:145,12
+DA:147,8
+DA:149,12
+DA:151,2
+DA:152,2
+DA:153,2
+DA:154,2
+DA:155,2
+DA:156,2
+DA:157,2
+DA:158,2
+DA:159,2
+DA:160,2
+DA:161,2
+DA:162,2
+DA:163,2
+DA:164,2
+DA:165,2
+DA:166,2
+DA:167,2
+DA:168,2
+DA:169,2
+DA:170,2
+DA:171,2
+DA:172,2
+DA:175,10
+DA:177,2
+DA:178,2
+DA:179,2
+DA:180,2
+DA:181,2
+DA:182,2
+DA:183,2
+DA:184,2
+DA:185,2
+DA:186,2
+DA:187,2
+DA:188,2
+DA:189,2
+DA:192,10
+DA:194,12
+DA:196,6
+DA:199,1
+DA:201,1
+DA:212,6
+LF:61
+LH:58
+end_of_record
+SF:lib\telephony.dart
+DA:15,0
+DA:18,0
+DA:20,0
+DA:21,0
+DA:23,0
+DA:25,0
+DA:27,0
+DA:28,0
+DA:30,0
+DA:31,0
+DA:33,0
+DA:37,0
+DA:48,0
+DA:50,2
+DA:55,0
+DA:58,0
+DA:61,0
+DA:64,0
+DA:68,0
+DA:70,0
+DA:77,0
+DA:80,0
+DA:82,0
+DA:84,0
+DA:87,0
+DA:94,0
+DA:96,0
+DA:97,0
+DA:98,0
+DA:104,0
+DA:105,0
+DA:106,0
+DA:107,0
+DA:109,0
+DA:110,0
+DA:112,0
+DA:113,0
+DA:118,1
+DA:122,3
+DA:123,1
+DA:126,3
+DA:129,3
+DA:130,1
+DA:133,1
+DA:137,3
+DA:138,1
+DA:141,3
+DA:144,3
+DA:145,1
+DA:148,1
+DA:152,3
+DA:153,1
+DA:156,3
+DA:159,3
+DA:160,1
+DA:163,1
+DA:165,3
+DA:166,1
+DA:169,3
+DA:172,3
+DA:173,1
+DA:176,1
+DA:178,1
+DA:181,5
+DA:185,2
+DA:186,2
+DA:189,1
+DA:190,5
+DA:196,2
+DA:202,6
+DA:205,1
+DA:208,2
+DA:214,4
+DA:217,1
+DA:221,1
+DA:225,2
+DA:228,1
+DA:229,2
+DA:231,1
+DA:233,3
+DA:234,2
+DA:237,1
+DA:241,1
+DA:243,3
+DA:244,1
+DA:247,1
+DA:249,3
+DA:250,1
+DA:253,1
+DA:254,2
+DA:256,1
+DA:257,2
+DA:259,1
+DA:261,3
+DA:262,1
+DA:265,1
+DA:266,3
+DA:267,1
+DA:270,1
+DA:271,2
+DA:273,1
+DA:274,2
+DA:276,1
+DA:277,3
+DA:278,1
+DA:281,1
+DA:282,2
+DA:284,1
+DA:286,3
+DA:288,3
+DA:289,1
+DA:292,1
+DA:294,3
+DA:295,1
+DA:298,1
+DA:299,2
+DA:301,1
+DA:302,2
+DA:304,1
+DA:305,2
+DA:322,1
+DA:324,1
+DA:325,2
+DA:326,2
+DA:327,1
+DA:328,1
+DA:329,2
+DA:331,1
+DA:332,1
+DA:334,1
+DA:335,1
+DA:337,1
+DA:338,2
+DA:340,0
+DA:341,0
+DA:343,0
+DA:344,0
+DA:346,0
+DA:347,0
+DA:349,0
+DA:350,0
+DA:351,0
+DA:352,0
+DA:354,0
+DA:355,0
+DA:357,0
+DA:358,0
+DA:362,0
+DA:366,0
+DA:367,0
+DA:369,0
+DA:370,0
+DA:372,0
+DA:373,0
+DA:375,0
+DA:376,0
+DA:382,1
+DA:384,3
+DA:385,3
+DA:386,3
+DA:387,3
+DA:388,3
+DA:389,3
+DA:390,3
+DA:391,3
+DA:392,3
+DA:393,3
+DA:394,3
+DA:395,3
+DA:404,1
+DA:407,1
+DA:408,2
+DA:409,2
+DA:410,1
+DA:411,1
+DA:412,1
+DA:414,1
+DA:415,2
+DA:417,1
+DA:418,2
+DA:424,1
+DA:426,3
+DA:427,3
+DA:428,3
+LF:184
+LH:125
+end_of_record
diff --git a/example/README.md b/example/README.md
new file mode 100644
index 0000000..aeb082d
--- /dev/null
+++ b/example/README.md
@@ -0,0 +1,17 @@
+
+# telephony_example
+
+Demonstrates how to use the telephony plugin.
+
+## Getting Started
+
+This project is a starting point for a Flutter application.
+
+A few resources to get you started if this is your first Flutter project:
+
+- [Lab: Write your first Flutter app](https://flutter.dev/docs/get-started/codelab)
+- [Cookbook: Useful Flutter samples](https://flutter.dev/docs/cookbook)
+
+For help getting started with Flutter, view our
+[online documentation](https://flutter.dev/docs), which offers tutorials,
+samples, guidance on mobile development, and a full API reference.
diff --git a/example/android/app/build.gradle b/example/android/app/build.gradle
new file mode 100644
index 0000000..33553a8
--- /dev/null
+++ b/example/android/app/build.gradle
@@ -0,0 +1,63 @@
+def localProperties = new Properties()
+def localPropertiesFile = rootProject.file('local.properties')
+if (localPropertiesFile.exists()) {
+ localPropertiesFile.withReader('UTF-8') { reader ->
+ localProperties.load(reader)
+ }
+}
+
+def flutterRoot = localProperties.getProperty('flutter.sdk')
+if (flutterRoot == null) {
+ throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.")
+}
+
+def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
+if (flutterVersionCode == null) {
+ flutterVersionCode = '1'
+}
+
+def flutterVersionName = localProperties.getProperty('flutter.versionName')
+if (flutterVersionName == null) {
+ flutterVersionName = '1.0'
+}
+
+apply plugin: 'com.android.application'
+apply plugin: 'kotlin-android'
+apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
+
+android {
+ compileSdkVersion 31
+
+ sourceSets {
+ main.java.srcDirs += 'src/main/kotlin'
+ }
+
+ lintOptions {
+ disable 'InvalidPackage'
+ }
+
+ defaultConfig {
+ // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
+ minSdkVersion 23
+ applicationId "com.shounakmulay.telephony_example"
+ targetSdkVersion 31
+ versionCode flutterVersionCode.toInteger()
+ versionName flutterVersionName
+ }
+
+ buildTypes {
+ release {
+ // TODO: Add your own signing config for the release build.
+ // Signing with the debug keys for now, so `flutter run --release` works.
+ signingConfig signingConfigs.debug
+ }
+ }
+}
+
+flutter {
+ source '../..'
+}
+
+dependencies {
+ implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
+}
diff --git a/example/android/app/src/debug/AndroidManifest.xml b/example/android/app/src/debug/AndroidManifest.xml
new file mode 100644
index 0000000..01828ec
--- /dev/null
+++ b/example/android/app/src/debug/AndroidManifest.xml
@@ -0,0 +1,7 @@
+
+
+
+
diff --git a/example/android/app/src/main/AndroidManifest.xml b/example/android/app/src/main/AndroidManifest.xml
new file mode 100644
index 0000000..4dd2e2b
--- /dev/null
+++ b/example/android/app/src/main/AndroidManifest.xml
@@ -0,0 +1,60 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/example/android/app/src/main/kotlin/com/shounakmulay/telephony_example/MainActivity.kt b/example/android/app/src/main/kotlin/com/shounakmulay/telephony_example/MainActivity.kt
new file mode 100644
index 0000000..86a83bb
--- /dev/null
+++ b/example/android/app/src/main/kotlin/com/shounakmulay/telephony_example/MainActivity.kt
@@ -0,0 +1,14 @@
+package com.shounakmulay.telephony_example
+
+import android.content.IntentFilter
+import android.os.Bundle
+import android.os.PersistableBundle
+import com.shounakmulay.telephony.sms.IncomingSmsReceiver
+import io.flutter.app.FlutterApplication
+import io.flutter.embedding.android.FlutterActivity
+
+class MainActivity :FlutterActivity() {}
+
+class MyApp: FlutterApplication() {
+
+}
diff --git a/example/android/app/src/main/res/drawable/launch_background.xml b/example/android/app/src/main/res/drawable/launch_background.xml
new file mode 100644
index 0000000..304732f
--- /dev/null
+++ b/example/android/app/src/main/res/drawable/launch_background.xml
@@ -0,0 +1,12 @@
+
+
+
+
+
+
+
+
diff --git a/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png
new file mode 100644
index 0000000..db77bb4
Binary files /dev/null and b/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ
diff --git a/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png
new file mode 100644
index 0000000..17987b7
Binary files /dev/null and b/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ
diff --git a/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png
new file mode 100644
index 0000000..09d4391
Binary files /dev/null and b/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ
diff --git a/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png
new file mode 100644
index 0000000..d5f1c8d
Binary files /dev/null and b/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ
diff --git a/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png
new file mode 100644
index 0000000..4d6372e
Binary files /dev/null and b/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ
diff --git a/example/android/app/src/main/res/values/styles.xml b/example/android/app/src/main/res/values/styles.xml
new file mode 100644
index 0000000..1f83a33
--- /dev/null
+++ b/example/android/app/src/main/res/values/styles.xml
@@ -0,0 +1,18 @@
+
+
+
+
+
+
+
diff --git a/example/android/app/src/profile/AndroidManifest.xml b/example/android/app/src/profile/AndroidManifest.xml
new file mode 100644
index 0000000..01828ec
--- /dev/null
+++ b/example/android/app/src/profile/AndroidManifest.xml
@@ -0,0 +1,7 @@
+
+
+
+
diff --git a/example/android/build.gradle b/example/android/build.gradle
new file mode 100644
index 0000000..ed255ee
--- /dev/null
+++ b/example/android/build.gradle
@@ -0,0 +1,31 @@
+buildscript {
+ ext.kotlin_version = '1.6.21'
+ repositories {
+ google()
+ jcenter()
+ }
+
+ dependencies {
+ classpath 'com.android.tools.build:gradle:4.1.3'
+ classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
+ }
+}
+
+allprojects {
+ repositories {
+ google()
+ jcenter()
+ }
+}
+
+rootProject.buildDir = '../build'
+subprojects {
+ project.buildDir = "${rootProject.buildDir}/${project.name}"
+}
+subprojects {
+ project.evaluationDependsOn(':app')
+}
+
+task clean(type: Delete) {
+ delete rootProject.buildDir
+}
diff --git a/example/android/gradle.properties b/example/android/gradle.properties
new file mode 100644
index 0000000..a673820
--- /dev/null
+++ b/example/android/gradle.properties
@@ -0,0 +1,4 @@
+org.gradle.jvmargs=-Xmx1536M
+android.useAndroidX=true
+android.enableJetifier=true
+android.enableR8=true
diff --git a/example/android/gradle/wrapper/gradle-wrapper.properties b/example/android/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 0000000..e190cd4
--- /dev/null
+++ b/example/android/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,6 @@
+#Sat Nov 07 10:44:31 IST 2020
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-6.7-bin.zip
diff --git a/example/android/settings.gradle b/example/android/settings.gradle
new file mode 100644
index 0000000..d3b6a40
--- /dev/null
+++ b/example/android/settings.gradle
@@ -0,0 +1,15 @@
+// Copyright 2014 The Flutter Authors. All rights reserved.
+// Use of this source code is governed by a BSD-style license that can be
+// found in the LICENSE file.
+
+include ':app'
+
+def localPropertiesFile = new File(rootProject.projectDir, "local.properties")
+def properties = new Properties()
+
+assert localPropertiesFile.exists()
+localPropertiesFile.withReader("UTF-8") { reader -> properties.load(reader) }
+
+def flutterSdkPath = properties.getProperty("flutter.sdk")
+assert flutterSdkPath != null, "flutter.sdk not set in local.properties"
+apply from: "$flutterSdkPath/packages/flutter_tools/gradle/app_plugin_loader.gradle"
diff --git a/example/lib/main.dart b/example/lib/main.dart
new file mode 100644
index 0000000..404cd18
--- /dev/null
+++ b/example/lib/main.dart
@@ -0,0 +1,77 @@
+import 'package:flutter/material.dart';
+import 'dart:async';
+import 'package:telephony/telephony.dart';
+
+onBackgroundMessage(SmsMessage message) {
+ debugPrint("onBackgroundMessage called");
+}
+
+void main() {
+ runApp(MyApp());
+}
+
+class MyApp extends StatefulWidget {
+ @override
+ _MyAppState createState() => _MyAppState();
+}
+
+class _MyAppState extends State {
+ String _message = "";
+ final telephony = Telephony.instance;
+
+ @override
+ void initState() {
+ super.initState();
+ initPlatformState();
+ }
+
+ onMessage(SmsMessage message) async {
+ setState(() {
+ _message = message.body ?? "Error reading message body.";
+ });
+ }
+
+ onSendStatus(SendStatus status) {
+ setState(() {
+ _message = status == SendStatus.SENT ? "sent" : "delivered";
+ });
+ }
+
+ // Platform messages are asynchronous, so we initialize in an async method.
+ Future initPlatformState() async {
+ // Platform messages may fail, so we use a try/catch PlatformException.
+ // If the widget was removed from the tree while the asynchronous platform
+ // message was in flight, we want to discard the reply rather than calling
+ // setState to update our non-existent appearance.
+
+ final bool? result = await telephony.requestPhoneAndSmsPermissions;
+
+ if (result != null && result) {
+ telephony.listenIncomingSms(
+ onNewMessage: onMessage, onBackgroundMessage: onBackgroundMessage);
+ }
+
+ if (!mounted) return;
+ }
+
+ @override
+ Widget build(BuildContext context) {
+ return MaterialApp(
+ home: Scaffold(
+ appBar: AppBar(
+ title: const Text('Plugin example app'),
+ ),
+ body: Column(
+ mainAxisAlignment: MainAxisAlignment.center,
+ children: [
+ Center(child: Text("Latest received SMS: $_message")),
+ TextButton(
+ onPressed: () async {
+ await telephony.openDialer("123413453");
+ },
+ child: Text('Open Dialer'))
+ ],
+ ),
+ ));
+ }
+}
diff --git a/example/pubspec.yaml b/example/pubspec.yaml
new file mode 100644
index 0000000..def93c5
--- /dev/null
+++ b/example/pubspec.yaml
@@ -0,0 +1,75 @@
+name: telephony_example
+description: Demonstrates how to use the telephony plugin.
+
+# The following line prevents the package from being accidentally published to
+# pub.dev using `pub publish`. This is preferred for private packages.
+publish_to: 'none' # Remove this line if you wish to publish to pub.dev
+
+environment:
+ sdk: ">=2.12.0 <3.0.0"
+ flutter: ">=1.10.0"
+
+dependencies:
+ flutter:
+ sdk: flutter
+
+ # When depending on this package from a real application you should use:
+ # telephony: ^x.y.z
+
+ telephony:
+ # When depending on this package from a real application you should use:
+ # telephony: ^x.y.z
+ # See https://dart.dev/tools/pub/dependencies#version-constraints
+ # The example app is bundled with the plugin so we use a path dependency on
+ # the parent directory to use the current plugin's version.
+ path: ../
+
+ # The following adds the Cupertino Icons font to your application.
+ # Use with the CupertinoIcons class for iOS style icons.
+ cupertino_icons: ^0.1.3
+
+dev_dependencies:
+ flutter_test:
+ sdk: flutter
+
+# For information on the generic Dart part of this file, see the
+# following page: https://dart.dev/tools/pub/pubspec
+
+# The following section is specific to Flutter.
+flutter:
+
+ # The following line ensures that the Material Icons font is
+ # included with your application, so that you can use the icons in
+ # the material Icons class.
+ uses-material-design: true
+
+ # To add assets to your application, add an assets section, like this:
+ # assets:
+ # - images/a_dot_burr.jpeg
+ # - images/a_dot_ham.jpeg
+
+ # An image asset can refer to one or more resolution-specific "variants", see
+ # https://flutter.dev/assets-and-images/#resolution-aware.
+
+ # For details regarding adding assets from package dependencies, see
+ # https://flutter.dev/assets-and-images/#from-packages
+
+ # To add custom fonts to your application, add a fonts section here,
+ # in this "flutter" section. Each entry in this list should have a
+ # "family" key with the font family name, and a "fonts" key with a
+ # list giving the asset and other descriptors for the font. For
+ # example:
+ # fonts:
+ # - family: Schyler
+ # fonts:
+ # - asset: fonts/Schyler-Regular.ttf
+ # - asset: fonts/Schyler-Italic.ttf
+ # style: italic
+ # - family: Trajan Pro
+ # fonts:
+ # - asset: fonts/TrajanPro.ttf
+ # - asset: fonts/TrajanPro_Bold.ttf
+ # weight: 700
+ #
+ # For details regarding fonts from package dependencies,
+ # see https://flutter.dev/custom-fonts/#from-packages
diff --git a/example/test/widget_test.dart b/example/test/widget_test.dart
new file mode 100644
index 0000000..e6815da
--- /dev/null
+++ b/example/test/widget_test.dart
@@ -0,0 +1,26 @@
+// This is a basic Flutter widget test.
+//
+// To perform an interaction with a widget in your test, use the WidgetTester
+// utility that Flutter provides. For example, you can send tap and scroll
+// gestures. You can also use WidgetTester to find child widgets in the widget
+// tree, read text, and verify that the values of widget properties are correct.
+
+import 'package:flutter/material.dart';
+import 'package:flutter_test/flutter_test.dart';
+import 'package:telephony_example/main.dart';
+
+void main() {
+ testWidgets('Verify Platform version', (WidgetTester tester) async {
+ // Build our app and trigger a frame.
+ await tester.pumpWidget(MyApp());
+
+ // Verify that platform version is retrieved.
+ expect(
+ find.byWidgetPredicate(
+ (Widget widget) =>
+ widget is Text && widget.data!.startsWith('Running on:'),
+ ),
+ findsOneWidget,
+ );
+ });
+}
diff --git a/ios/Classes/SwiftTelephonyPlugin.swift b/ios/Classes/SwiftTelephonyPlugin.swift
new file mode 100644
index 0000000..63f3be0
--- /dev/null
+++ b/ios/Classes/SwiftTelephonyPlugin.swift
@@ -0,0 +1,14 @@
+import Flutter
+import UIKit
+
+public class SwiftTelephonyPlugin: NSObject, FlutterPlugin {
+ public static func register(with registrar: FlutterPluginRegistrar) {
+ let channel = FlutterMethodChannel(name: "telephony", binaryMessenger: registrar.messenger())
+ let instance = SwiftTelephonyPlugin()
+ registrar.addMethodCallDelegate(instance, channel: channel)
+ }
+
+ public func handle(_ call: FlutterMethodCall, result: @escaping FlutterResult) {
+ result("iOS " + UIDevice.current.systemVersion)
+ }
+}
diff --git a/ios/Classes/TelephonyPlugin.h b/ios/Classes/TelephonyPlugin.h
new file mode 100644
index 0000000..a787a7f
--- /dev/null
+++ b/ios/Classes/TelephonyPlugin.h
@@ -0,0 +1,4 @@
+#import
+
+@interface TelephonyPlugin : NSObject
+@end
diff --git a/ios/Classes/TelephonyPlugin.m b/ios/Classes/TelephonyPlugin.m
new file mode 100644
index 0000000..911ace8
--- /dev/null
+++ b/ios/Classes/TelephonyPlugin.m
@@ -0,0 +1,15 @@
+#import "TelephonyPlugin.h"
+#if __has_include()
+#import
+#else
+// Support project import fallback if the generated compatibility header
+// is not copied when this plugin is created as a library.
+// https://forums.swift.org/t/swift-static-libraries-dont-copy-generated-objective-c-header/19816
+#import "telephony-Swift.h"
+#endif
+
+@implementation TelephonyPlugin
++ (void)registerWithRegistrar:(NSObject*)registrar {
+ [SwiftTelephonyPlugin registerWithRegistrar:registrar];
+}
+@end
diff --git a/ios/telephony.podspec b/ios/telephony.podspec
new file mode 100644
index 0000000..5908274
--- /dev/null
+++ b/ios/telephony.podspec
@@ -0,0 +1,23 @@
+#
+# To learn more about a Podspec see http://guides.cocoapods.org/syntax/podspec.html.
+# Run `pod lib lint telephony.podspec' to validate before publishing.
+#
+Pod::Spec.new do |s|
+ s.name = 'telephony'
+ s.version = '0.0.1'
+ s.summary = 'A new flutter plugin project.'
+ s.description = <<-DESC
+A new flutter plugin project.
+ DESC
+ s.homepage = 'http://example.com'
+ s.license = { :file => '../LICENSE' }
+ s.author = { 'Your Company' => 'email@example.com' }
+ s.source = { :path => '.' }
+ s.source_files = 'Classes/**/*'
+ s.dependency 'Flutter'
+ s.platform = :ios, '8.0'
+
+ # Flutter.framework does not contain a i386 slice. Only x86_64 simulators are supported.
+ s.pod_target_xcconfig = { 'DEFINES_MODULE' => 'YES', 'VALID_ARCHS[sdk=iphonesimulator*]' => 'x86_64' }
+ s.swift_version = '5.0'
+end
diff --git a/lib/constants.dart b/lib/constants.dart
new file mode 100644
index 0000000..31992eb
--- /dev/null
+++ b/lib/constants.dart
@@ -0,0 +1,255 @@
+part of 'telephony.dart';
+
+const _FOREGROUND_CHANNEL = 'plugins.shounakmulay.com/foreground_sms_channel';
+const _BACKGROUND_CHANNEL = 'plugins.shounakmulay.com/background_sms_channel';
+
+const HANDLE_BACKGROUND_MESSAGE = "handleBackgroundMessage";
+const BACKGROUND_SERVICE_INITIALIZED = "backgroundServiceInitialized";
+const GET_ALL_INBOX_SMS = "getAllInboxSms";
+const GET_ALL_SENT_SMS = "getAllSentSms";
+const GET_ALL_DRAFT_SMS = "getAllDraftSms";
+const GET_ALL_CONVERSATIONS = "getAllConversations";
+const SEND_SMS = "sendSms";
+const SEND_MULTIPART_SMS = "sendMultipartSms";
+const SEND_SMS_INTENT = "sendSmsIntent";
+const IS_SMS_CAPABLE = "isSmsCapable";
+const GET_CELLULAR_DATA_STATE = "getCellularDataState";
+const GET_CALL_STATE = "getCallState";
+const GET_DATA_ACTIVITY = "getDataActivity";
+const GET_NETWORK_OPERATOR = "getNetworkOperator";
+const GET_NETWORK_OPERATOR_NAME = "getNetworkOperatorName";
+const GET_DATA_NETWORK_TYPE = "getDataNetworkType";
+const GET_PHONE_TYPE = "getPhoneType";
+const GET_SIM_OPERATOR = "getSimOperator";
+const GET_SIM_OPERATOR_NAME = "getSimOperatorName";
+const GET_SIM_STATE = "getSimState";
+const IS_NETWORK_ROAMING = "isNetworkRoaming";
+const GET_SIGNAL_STRENGTH = "getSignalStrength";
+const GET_SERVICE_STATE = "getServiceState";
+const REQUEST_SMS_PERMISSION = "requestSmsPermissions";
+const REQUEST_PHONE_PERMISSION = "requestPhonePermissions";
+const REQUEST_PHONE_AND_SMS_PERMISSION = "requestPhoneAndSmsPermissions";
+const OPEN_DIALER = "openDialer";
+const DIAL_PHONE_NUMBER = "dialPhoneNumber";
+
+const ON_MESSAGE = "onMessage";
+const SMS_SENT = "smsSent";
+const SMS_DELIVERED = "smsDelivered";
+
+///
+/// Possible parameters that can be fetched during a SMS query operation.
+class _SmsProjections {
+// static const String COUNT = "_count";
+ static const String ID = "_id";
+ static const String ORIGINATING_ADDRESS = "originating_address";
+ static const String ADDRESS = "address";
+ static const String MESSAGE_BODY = "message_body";
+ static const String BODY = "body";
+ static const String SERVICE_CENTER_ADDRESS = "service_center";
+
+// static const String CREATOR = "creator";
+ static const String TIMESTAMP = "timestamp";
+ static const String DATE = "date";
+ static const String DATE_SENT = "date_sent";
+
+// static const String ERROR_CODE = "error_code";
+// static const String LOCKED = "locked";
+// static const int MESSAGE_TYPE_ALL = 0;
+// static const int MESSAGE_TYPE_DRAFT = 3;
+// static const int MESSAGE_TYPE_FAILED = 5;
+// static const int MESSAGE_TYPE_INBOX = 1;
+// static const int MESSAGE_TYPE_OUTBOX = 4;
+// static const int MESSAGE_TYPE_QUEUED = 6;
+// static const int MESSAGE_TYPE_SENT = 2;
+// static const String PERSON = "person";
+// static const String PROTOCOL = "protocol";
+ static const String READ = "read";
+
+// static const String REPLY_PATH_PRESENT = "reply_path_present";
+ static const String SEEN = "seen";
+
+// static const String SERVICE_CENTER = "service_center";
+ static const String STATUS = "status";
+
+// static const int STATUS_COMPLETE = 0;
+// static const int STATUS_FAILED = 64;
+// static const int STATUS_NONE = -1;
+// static const int STATUS_PENDING = 32;
+ static const String SUBJECT = "subject";
+ static const String SUBSCRIPTION_ID = "sub_id";
+ static const String THREAD_ID = "thread_id";
+ static const String TYPE = "type";
+}
+
+///
+/// Possible parameters that can be fetched during a Conversation query operation.
+class _ConversationProjections {
+ static const String SNIPPET = "snippet";
+ static const String THREAD_ID = "thread_id";
+ static const String MSG_COUNT = "msg_count";
+}
+
+abstract class _TelephonyColumn {
+ const _TelephonyColumn();
+
+ String get _name;
+}
+
+/// Represents all the possible parameters for a SMS
+class SmsColumn extends _TelephonyColumn {
+ final String _columnName;
+
+ const SmsColumn._(this._columnName);
+
+ static const ID = SmsColumn._(_SmsProjections.ID);
+ static const ADDRESS = SmsColumn._(_SmsProjections.ADDRESS);
+ static const SERVICE_CENTER_ADDRESS =
+ SmsColumn._(_SmsProjections.SERVICE_CENTER_ADDRESS);
+ static const BODY = SmsColumn._(_SmsProjections.BODY);
+ static const DATE = SmsColumn._(_SmsProjections.DATE);
+ static const DATE_SENT = SmsColumn._(_SmsProjections.DATE_SENT);
+ static const READ = SmsColumn._(_SmsProjections.READ);
+ static const SEEN = SmsColumn._(_SmsProjections.SEEN);
+ static const STATUS = SmsColumn._(_SmsProjections.STATUS);
+ static const SUBJECT = SmsColumn._(_SmsProjections.SUBJECT);
+ static const SUBSCRIPTION_ID = SmsColumn._(_SmsProjections.SUBSCRIPTION_ID);
+ static const THREAD_ID = SmsColumn._(_SmsProjections.THREAD_ID);
+ static const TYPE = SmsColumn._(_SmsProjections.TYPE);
+
+ @override
+ String get _name => _columnName;
+}
+
+/// Represents all the possible parameters for a Conversation
+class ConversationColumn extends _TelephonyColumn {
+ final String _columnName;
+
+ const ConversationColumn._(this._columnName);
+
+ static const SNIPPET = ConversationColumn._(_ConversationProjections.SNIPPET);
+ static const THREAD_ID =
+ ConversationColumn._(_ConversationProjections.THREAD_ID);
+ static const MSG_COUNT =
+ ConversationColumn._(_ConversationProjections.MSG_COUNT);
+
+ @override
+ String get _name => _columnName;
+}
+
+const DEFAULT_SMS_COLUMNS = [
+ SmsColumn.ID,
+ SmsColumn.ADDRESS,
+ SmsColumn.BODY,
+ SmsColumn.DATE
+];
+
+const INCOMING_SMS_COLUMNS = [
+ SmsColumn._(_SmsProjections.ORIGINATING_ADDRESS),
+ SmsColumn._(_SmsProjections.MESSAGE_BODY),
+ SmsColumn._(_SmsProjections.TIMESTAMP),
+ SmsColumn._(_SmsProjections.SERVICE_CENTER_ADDRESS),
+ SmsColumn.STATUS
+];
+
+const DEFAULT_CONVERSATION_COLUMNS = [
+ ConversationColumn.SNIPPET,
+ ConversationColumn.THREAD_ID,
+ ConversationColumn.MSG_COUNT
+];
+
+/// Represents types of SMS.
+enum SmsType {
+ MESSAGE_TYPE_ALL,
+ MESSAGE_TYPE_INBOX,
+ MESSAGE_TYPE_SENT,
+ MESSAGE_TYPE_DRAFT,
+ MESSAGE_TYPE_OUTBOX,
+ MESSAGE_TYPE_FAILED,
+ MESSAGE_TYPE_QUEUED
+}
+
+/// Represents states of SMS.
+enum SmsStatus { STATUS_COMPLETE, STATUS_FAILED, STATUS_NONE, STATUS_PENDING }
+
+/// Represents data connection state.
+enum DataState { DISCONNECTED, CONNECTING, CONNECTED, SUSPENDED, UNKNOWN }
+
+/// Represents state of cellular calls.
+enum CallState { IDLE, RINGING, OFFHOOK, UNKNOWN }
+
+/// Represents state of cellular network data activity.
+enum DataActivity { NONE, IN, OUT, INOUT, DORMANT, UNKNOWN }
+
+/// Represents types of networks for a device.
+enum NetworkType {
+ UNKNOWN,
+ GPRS,
+ EDGE,
+ UMTS,
+ CDMA,
+ EVDO_0,
+ EVDO_A,
+ TYPE_1xRTT,
+ HSDPA,
+ HSUPA,
+ HSPA,
+ IDEN,
+ EVDO_B,
+ LTE,
+ EHRPD,
+ HSPAP,
+ GSM,
+ TD_SCDMA,
+ IWLAN,
+ LTE_CA,
+ NR,
+}
+
+/// Represents types of cellular technology supported by a device.
+enum PhoneType { NONE, GSM, CDMA, SIP, UNKNOWN }
+
+/// Represents state of SIM.
+enum SimState {
+ UNKNOWN,
+ ABSENT,
+ PIN_REQUIRED,
+ PUK_REQUIRED,
+ NETWORK_LOCKED,
+ READY,
+ NOT_READY,
+ PERM_DISABLED,
+ CARD_IO_ERROR,
+ CARD_RESTRICTED,
+ LOADED,
+ PRESENT
+}
+
+/// Represents state of cellular service.
+enum ServiceState {
+ IN_SERVICE,
+ OUT_OF_SERVICE,
+ EMERGENCY_ONLY,
+ POWER_OFF,
+ UNKNOWN
+}
+
+/// Represents the quality of cellular signal.
+enum SignalStrength { NONE_OR_UNKNOWN, POOR, MODERATE, GOOD, GREAT }
+
+/// Represents sort order for [OrderBy].
+enum Sort { ASC, DESC }
+
+extension Value on Sort {
+ String get value {
+ switch (this) {
+ case Sort.ASC:
+ return "ASC";
+ case Sort.DESC:
+ default:
+ return "DESC";
+ }
+ }
+}
+
+/// Represents the status of a sms message sent from the device.
+enum SendStatus { SENT, DELIVERED }
diff --git a/lib/filter.dart b/lib/filter.dart
new file mode 100644
index 0000000..5a1a96f
--- /dev/null
+++ b/lib/filter.dart
@@ -0,0 +1,200 @@
+part of 'telephony.dart';
+
+abstract class Filter {
+ T and(K column);
+
+ T or(K column);
+
+ String get selection;
+
+ List get selectionArgs;
+}
+
+/// Filter to be applied to a SMS query operation.
+///
+/// Works like a SQL WHERE clause.
+///
+/// Public constructor:
+///
+/// SmsFilter.where();
+class SmsFilter implements Filter {
+ final String _filter;
+ final List _filterArgs;
+
+ SmsFilter._(this._filter, this._filterArgs);
+
+ static SmsFilterStatement where(SmsColumn column) =>
+ SmsFilterStatement._(column._columnName);
+
+ /// Joins two filter statements by the AND operator.
+ SmsFilterStatement and(SmsColumn column) {
+ return _addCombineOperator(column, " AND");
+ }
+
+ /// Joins to filter statements by the OR operator.
+ @override
+ SmsFilterStatement or(SmsColumn column) {
+ return _addCombineOperator(column, " OR");
+ }
+
+ SmsFilterStatement _addCombineOperator(SmsColumn column, String operator) {
+ return SmsFilterStatement._withPreviousFilter("$_filter $operator",
+ column._name, List.from(_filterArgs, growable: true));
+ }
+
+ /// ## Do not call this method. This method is visible only for testing.
+ @visibleForTesting
+ @override
+ String get selection => _filter;
+
+ /// ## Do not call this method. This method is visible only for testing.
+ @visibleForTesting
+ @override
+ List get selectionArgs => _filterArgs;
+}
+
+class ConversationFilter
+ extends Filter {
+ final String _filter;
+ final List _filterArgs;
+
+ ConversationFilter._(this._filter, this._filterArgs);
+
+ static ConversationFilterStatement where(ConversationColumn column) =>
+ ConversationFilterStatement._(column._columnName);
+
+ /// Joins two filter statements by the AND operator.
+ @override
+ ConversationFilterStatement and(ConversationColumn column) {
+ return _addCombineOperator(column, " AND");
+ }
+
+ /// Joins to filter statements by the OR operator.
+ @override
+ ConversationFilterStatement or(ConversationColumn column) {
+ return _addCombineOperator(column, " OR");
+ }
+
+ ConversationFilterStatement _addCombineOperator(
+ ConversationColumn column, String operator) {
+ return ConversationFilterStatement._withPreviousFilter("$_filter $operator",
+ column._name, List.from(_filterArgs, growable: true));
+ }
+
+ @override
+ String get selection => _filter;
+
+ @override
+ List get selectionArgs => _filterArgs;
+}
+
+abstract class FilterStatement {
+ String _column;
+ String _previousFilter = "";
+ List _previousFilterArgs = [];
+
+ FilterStatement._(this._column);
+
+ FilterStatement._withPreviousFilter(
+ String previousFilter, String column, List previousFilterArgs)
+ : _previousFilter = previousFilter,
+ _column = column,
+ _previousFilterArgs = previousFilterArgs;
+
+ /// Checks equality between the column value and [equalTo] value
+ T equals(String equalTo) {
+ return _createFilter(equalTo, "=");
+ }
+
+ /// Checks whether the value of the column is greater than [value]
+ T greaterThan(String value) {
+ return _createFilter(value, ">");
+ }
+
+ /// Checks whether the value of the column is less than [value]
+ T lessThan(String value) {
+ return _createFilter(value, "<");
+ }
+
+ /// Checks whether the value of the column is greater than or equal to [value]
+ T greaterThanOrEqualTo(String value) {
+ return _createFilter(value, ">=");
+ }
+
+ /// Checks whether the value of the column is less than or equal to [value]
+ T lessThanOrEqualTo(String value) {
+ return _createFilter(value, "<=");
+ }
+
+ /// Checks for inequality between the column value and [value]
+ T notEqualTo(String value) {
+ return _createFilter(value, "!=");
+ }
+
+ /// Checks whether the column value is LIKE the provided string [value]
+ T like(String value) {
+ return _createFilter(value, "LIKE");
+ }
+
+ /// Checks whether the column value is in the provided list of [values]
+ T inValues(List values) {
+ final String filterValues = values.join(",");
+ return _createFilter("($filterValues)", "IN");
+ }
+
+ /// Checks whether the column value lies BETWEEN [from] and [to].
+ T between(String from, String to) {
+ final String filterValue = "$from AND $to";
+ return _createFilter(filterValue, "BETWEEN");
+ }
+
+ /// Applies the NOT operator
+ K get not {
+ _previousFilter += " NOT";
+ return this as K;
+ }
+
+ T _createFilter(String value, String operator);
+}
+
+class SmsFilterStatement
+ extends FilterStatement {
+ SmsFilterStatement._(String column) : super._(column);
+
+ SmsFilterStatement._withPreviousFilter(
+ String previousFilter, String column, List previousFilterArgs)
+ : super._withPreviousFilter(previousFilter, column, previousFilterArgs);
+
+ @override
+ SmsFilter _createFilter(String value, String operator) {
+ return SmsFilter._("$_previousFilter $_column $operator ?",
+ _previousFilterArgs..add(value));
+ }
+}
+
+class ConversationFilterStatement
+ extends FilterStatement {
+ ConversationFilterStatement._(String column) : super._(column);
+
+ ConversationFilterStatement._withPreviousFilter(
+ String previousFilter, String column, List previousFilterArgs)
+ : super._withPreviousFilter(previousFilter, column, previousFilterArgs);
+
+ @override
+ ConversationFilter _createFilter(String value, String operator) {
+ return ConversationFilter._("$_previousFilter $_column $operator ?",
+ _previousFilterArgs..add(value));
+ }
+}
+
+class OrderBy {
+ final _TelephonyColumn _column;
+ Sort _sort = Sort.DESC;
+
+ /// Orders the query results by the provided column and [sort] value.
+ OrderBy(this._column, {Sort sort = Sort.DESC}) {
+ _sort = sort;
+ }
+
+ String get _value => "${_column._name} ${_sort.value}";
+}
diff --git a/lib/telephony.dart b/lib/telephony.dart
new file mode 100644
index 0000000..d15eed6
--- /dev/null
+++ b/lib/telephony.dart
@@ -0,0 +1,702 @@
+import 'dart:async';
+import 'dart:ui';
+
+import 'package:flutter/services.dart';
+import 'package:flutter/widgets.dart';
+import 'package:platform/platform.dart';
+
+part 'constants.dart';
+
+part 'filter.dart';
+
+typedef MessageHandler(SmsMessage message);
+typedef SmsSendStatusListener(SendStatus status);
+
+void _flutterSmsSetupBackgroundChannel(
+ {MethodChannel backgroundChannel =
+ const MethodChannel(_BACKGROUND_CHANNEL)}) async {
+ WidgetsFlutterBinding.ensureInitialized();
+
+ backgroundChannel.setMethodCallHandler((call) async {
+ if (call.method == HANDLE_BACKGROUND_MESSAGE) {
+ final CallbackHandle handle =
+ CallbackHandle.fromRawHandle(call.arguments['handle']);
+ final Function handlerFunction =
+ PluginUtilities.getCallbackFromHandle(handle)!;
+ try {
+ await handlerFunction(SmsMessage.fromMap(
+ call.arguments['message'], INCOMING_SMS_COLUMNS));
+ } catch (e) {
+ print('Unable to handle incoming background message.');
+ print(e);
+ }
+ return Future.value();
+ }
+ });
+
+ backgroundChannel.invokeMethod(BACKGROUND_SERVICE_INITIALIZED);
+}
+
+///
+/// A Flutter plugin to use telephony features such as
+/// - Send SMS Messages
+/// - Query SMS Messages
+/// - Listen for incoming SMS
+/// - Retrieve various network parameters
+///
+///
+/// This plugin tries to replicate some of the functionality provided by Android's Telephony class.
+///
+///
+class Telephony {
+ final MethodChannel _foregroundChannel;
+ final Platform _platform;
+
+ late MessageHandler _onNewMessage;
+ late MessageHandler _onBackgroundMessages;
+ late SmsSendStatusListener _statusListener;
+
+ ///
+ /// Gets a singleton instance of the [Telephony] class.
+ ///
+ static Telephony get instance => _instance;
+
+ ///
+ /// Gets a singleton instance of the [Telephony] class to be used in background execution context.
+ ///
+ static Telephony get backgroundInstance => _backgroundInstance;
+
+ /// ## Do not call this method. This method is visible only for testing.
+ @visibleForTesting
+ Telephony.private(MethodChannel methodChannel, Platform platform)
+ : _foregroundChannel = methodChannel,
+ _platform = platform;
+
+ Telephony._newInstance(MethodChannel methodChannel, LocalPlatform platform)
+ : _foregroundChannel = methodChannel,
+ _platform = platform {
+ _foregroundChannel.setMethodCallHandler(handler);
+ }
+
+ static final Telephony _instance = Telephony._newInstance(
+ const MethodChannel(_FOREGROUND_CHANNEL), const LocalPlatform());
+ static final Telephony _backgroundInstance = Telephony._newInstance(
+ const MethodChannel(_FOREGROUND_CHANNEL), const LocalPlatform());
+
+ ///
+ /// Listens to incoming SMS.
+ ///
+ /// ### Requires RECEIVE_SMS permission.
+ ///
+ /// Parameters:
+ ///
+ /// - [onNewMessage] : Called on every new message received when app is in foreground.
+ /// - [onBackgroundMessage] (optional) : Called on every new message received when app is in background.
+ /// - [listenInBackground] (optional) : Defaults to true. Set to false to only listen to messages in foreground. [listenInBackground] is
+ /// ignored if [onBackgroundMessage] is not set.
+ ///
+ ///
+ void listenIncomingSms(
+ {required MessageHandler onNewMessage,
+ MessageHandler? onBackgroundMessage,
+ bool listenInBackground = true}) {
+ assert(_platform.isAndroid == true, "Can only be called on Android.");
+ assert(
+ listenInBackground
+ ? onBackgroundMessage != null
+ : onBackgroundMessage == null,
+ listenInBackground
+ ? "`onBackgroundMessage` cannot be null when `listenInBackground` is true. Set `listenInBackground` to false if you don't need background processing."
+ : "You have set `listenInBackground` to false. `onBackgroundMessage` can only be set when `listenInBackground` is true");
+
+ _onNewMessage = onNewMessage;
+
+ if (listenInBackground && onBackgroundMessage != null) {
+ _onBackgroundMessages = onBackgroundMessage;
+ final CallbackHandle backgroundSetupHandle =
+ PluginUtilities.getCallbackHandle(_flutterSmsSetupBackgroundChannel)!;
+ final CallbackHandle? backgroundMessageHandle =
+ PluginUtilities.getCallbackHandle(_onBackgroundMessages);
+
+ if (backgroundMessageHandle == null) {
+ throw ArgumentError(
+ '''Failed to setup background message handler! `onBackgroundMessage`
+ should be a TOP-LEVEL OR STATIC FUNCTION and should NOT be tied to a
+ class or an anonymous function.''',
+ );
+ }
+
+ _foregroundChannel.invokeMethod(
+ 'startBackgroundService',
+ {
+ 'setupHandle': backgroundSetupHandle.toRawHandle(),
+ 'backgroundHandle': backgroundMessageHandle.toRawHandle()
+ },
+ );
+ } else {
+ _foregroundChannel.invokeMethod('disableBackgroundService');
+ }
+ }
+
+ /// ## Do not call this method. This method is visible only for testing.
+ @visibleForTesting
+ Future handler(MethodCall call) async {
+ switch (call.method) {
+ case ON_MESSAGE:
+ final message = call.arguments["message"];
+ return _onNewMessage(SmsMessage.fromMap(message, INCOMING_SMS_COLUMNS));
+ case SMS_SENT:
+ return _statusListener(SendStatus.SENT);
+ case SMS_DELIVERED:
+ return _statusListener(SendStatus.DELIVERED);
+ }
+ }
+
+ ///
+ /// Query SMS Inbox.
+ ///
+ /// ### Requires READ_SMS permission.
+ ///
+ /// Parameters:
+ ///
+ /// - [columns] (optional) : List of [SmsColumn] to be returned by this query. Defaults to [ SmsColumn.ID, SmsColumn.ADDRESS, SmsColumn.BODY, SmsColumn.DATE ]
+ /// - [filter] (optional) : [SmsFilter] to filter the results of this query. Works like SQL WHERE clause.
+ /// - [sortOrder] (optional): List of [OrderBy]. Orders the results of this query by the provided columns and order.
+ ///
+ /// Returns:
+ ///
+ /// [Future>]
+ Future> getInboxSms(
+ {List columns = DEFAULT_SMS_COLUMNS,
+ SmsFilter? filter,
+ List? sortOrder}) async {
+ assert(_platform.isAndroid == true, "Can only be called on Android.");
+ final args = _getArguments(columns, filter, sortOrder);
+
+ final messages =
+ await _foregroundChannel.invokeMethod(GET_ALL_INBOX_SMS, args);
+
+ return messages
+ ?.map((message) => SmsMessage.fromMap(message, columns))
+ .toList(growable: false) ??
+ List.empty();
+ }
+
+ ///
+ /// Query SMS Outbox / Sent messages.
+ ///
+ /// ### Requires READ_SMS permission.
+ ///
+ /// Parameters:
+ ///
+ /// - [columns] (optional) : List of [SmsColumn] to be returned by this query. Defaults to [ SmsColumn.ID, SmsColumn.ADDRESS, SmsColumn.BODY, SmsColumn.DATE ]
+ /// - [filter] (optional) : [SmsFilter] to filter the results of this query. Works like SQL WHERE clause.
+ /// - [sortOrder] (optional): List of [OrderBy]. Orders the results of this query by the provided columns and order.
+ ///
+ /// Returns:
+ ///
+ /// [Future>]
+ Future> getSentSms(
+ {List columns = DEFAULT_SMS_COLUMNS,
+ SmsFilter? filter,
+ List? sortOrder}) async {
+ assert(_platform.isAndroid == true, "Can only be called on Android.");
+ final args = _getArguments(columns, filter, sortOrder);
+
+ final messages =
+ await _foregroundChannel.invokeMethod(GET_ALL_SENT_SMS, args);
+
+ return messages
+ ?.map((message) => SmsMessage.fromMap(message, columns))
+ .toList(growable: false) ??
+ List.empty();
+ }
+
+ ///
+ /// Query SMS Drafts.
+ ///
+ /// ### Requires READ_SMS permission.
+ ///
+ /// Parameters:
+ ///
+ /// - [columns] (optional) : List of [SmsColumn] to be returned by this query. Defaults to [ SmsColumn.ID, SmsColumn.ADDRESS, SmsColumn.BODY, SmsColumn.DATE ]
+ /// - [filter] (optional) : [SmsFilter] to filter the results of this query. Works like SQL WHERE clause.
+ /// - [sortOrder] (optional): List of [OrderBy]. Orders the results of this query by the provided columns and order.
+ ///
+ /// Returns:
+ ///
+ /// [Future>]
+ Future> getDraftSms(
+ {List columns = DEFAULT_SMS_COLUMNS,
+ SmsFilter? filter,
+ List? sortOrder}) async {
+ assert(_platform.isAndroid == true, "Can only be called on Android.");
+ final args = _getArguments(columns, filter, sortOrder);
+
+ final messages =
+ await _foregroundChannel.invokeMethod(GET_ALL_DRAFT_SMS, args);
+
+ return messages
+ ?.map((message) => SmsMessage.fromMap(message, columns))
+ .toList(growable: false) ??
+ List.empty();
+ }
+
+ ///
+ /// Query SMS Inbox.
+ ///
+ /// ### Requires READ_SMS permission.
+ ///
+ /// Parameters:
+ ///
+ /// - [filter] (optional) : [ConversationFilter] to filter the results of this query. Works like SQL WHERE clause.
+ /// - [sortOrder] (optional): List of [OrderBy]. Orders the results of this query by the provided columns and order.
+ ///
+ /// Returns:
+ ///
+ /// [Future>]
+ Future> getConversations(
+ {ConversationFilter? filter, List? sortOrder}) async {
+ assert(_platform.isAndroid == true, "Can only be called on Android.");
+ final args = _getArguments(DEFAULT_CONVERSATION_COLUMNS, filter, sortOrder);
+
+ final conversations = await _foregroundChannel.invokeMethod(
+ GET_ALL_CONVERSATIONS, args);
+
+ return conversations
+ ?.map((conversation) => SmsConversation.fromMap(conversation))
+ .toList(growable: false) ??
+ List.empty();
+ }
+
+ Map _getArguments(List<_TelephonyColumn> columns,
+ Filter? filter, List? sortOrder) {
+ final Map args = {};
+
+ args["projection"] = columns.map((c) => c._name).toList();
+
+ if (filter != null) {
+ args["selection"] = filter.selection;
+ args["selection_args"] = filter.selectionArgs;
+ }
+
+ if (sortOrder != null && sortOrder.isNotEmpty) {
+ args["sort_order"] = sortOrder.map((o) => o._value).join(",");
+ }
+
+ return args;
+ }
+
+ ///
+ /// Send an SMS directly from your application. Uses Android's SmsManager to send SMS.
+ ///
+ /// ### Requires SEND_SMS permission.
+ ///
+ /// Parameters:
+ ///
+ /// - [to] : Address to send the SMS to.
+ /// - [message] : Message to be sent. If message body is longer than standard SMS length limits set appropriate
+ /// value for [isMultipart]
+ /// - [statusListener] (optional) : Listen to the status of the sent SMS. Values can be one of [SmsStatus]
+ /// - [isMultipart] (optional) : If message body is longer than standard SMS limit of 160 characters, set this flag to
+ /// send the SMS in multiple parts.
+ Future sendSms({
+ required String to,
+ required String message,
+ SmsSendStatusListener? statusListener,
+ bool isMultipart = false,
+ }) async {
+ assert(_platform.isAndroid == true, "Can only be called on Android.");
+ bool listenStatus = false;
+ if (statusListener != null) {
+ _statusListener = statusListener;
+ listenStatus = true;
+ }
+ final Map args = {
+ "address": to,
+ "message_body": message,
+ "listen_status": listenStatus
+ };
+ final String method = isMultipart ? SEND_MULTIPART_SMS : SEND_SMS;
+ await _foregroundChannel.invokeMethod(method, args);
+ }
+
+ ///
+ /// Open Android's default SMS application with the provided message and address.
+ ///
+ /// ### Requires SEND_SMS permission.
+ ///
+ /// Parameters:
+ ///
+ /// - [to] : Address to send the SMS to.
+ /// - [message] : Message to be sent.
+ ///
+ Future sendSmsByDefaultApp({
+ required String to,
+ required String message,
+ }) async {
+ final Map args = {
+ "address": to,
+ "message_body": message,
+ };
+ await _foregroundChannel.invokeMethod(SEND_SMS_INTENT, args);
+ }
+
+ ///
+ /// Checks if the device has necessary features to send and receive SMS.
+ ///
+ /// Uses TelephonyManager class on Android.
+ ///
+ Future get isSmsCapable =>
+ _foregroundChannel.invokeMethod(IS_SMS_CAPABLE);
+
+ ///
+ /// Returns a constant indicating the current data connection state (cellular).
+ ///
+ /// Returns:
+ ///
+ /// [Future]
+ Future get cellularDataState async {
+ final int? dataState =
+ await _foregroundChannel.invokeMethod(GET_CELLULAR_DATA_STATE);
+ if (dataState == null || dataState == -1) {
+ return DataState.UNKNOWN;
+ } else {
+ return DataState.values[dataState];
+ }
+ }
+
+ ///
+ /// Returns a constant that represents the current state of all phone calls.
+ ///
+ /// Returns:
+ ///
+ /// [Future]
+ Future get callState async {
+ final int? state =
+ await _foregroundChannel.invokeMethod(GET_CALL_STATE);
+ if (state != null) {
+ return CallState.values[state];
+ } else {
+ return CallState.UNKNOWN;
+ }
+ }
+
+ ///
+ /// Returns a constant that represents the current state of all phone calls.
+ ///
+ /// Returns:
+ ///
+ /// [Future]
+ Future get dataActivity async {
+ final int? activity =
+ await _foregroundChannel.invokeMethod(GET_DATA_ACTIVITY);
+ if (activity != null) {
+ return DataActivity.values[activity];
+ } else {
+ return DataActivity.UNKNOWN;
+ }
+ }
+
+ ///
+ /// Returns the numeric name (MCC+MNC) of current registered operator.
+ ///
+ /// Availability: Only when user is registered to a network.
+ ///
+ /// Result may be unreliable on CDMA networks (use phoneType to determine if on a CDMA network).
+ ///
+ Future get networkOperator =>
+ _foregroundChannel.invokeMethod(GET_NETWORK_OPERATOR);
+
+ ///
+ /// Returns the alphabetic name of current registered operator.
+ ///
+ /// Availability: Only when user is registered to a network.
+ ///
+ /// Result may be unreliable on CDMA networks (use phoneType to determine if on a CDMA network).
+ ///
+ Future get networkOperatorName =>
+ _foregroundChannel.invokeMethod(GET_NETWORK_OPERATOR_NAME);
+
+ ///
+ /// Returns a constant indicating the radio technology (network type) currently in use on the device for data transmission.
+ ///
+ /// ### Requires READ_PHONE_STATE permission.
+ ///
+ Future get dataNetworkType async {
+ final int? type =
+ await _foregroundChannel.invokeMethod(GET_DATA_NETWORK_TYPE);
+ if (type != null) {
+ return NetworkType.values[type];
+ } else {
+ return NetworkType.UNKNOWN;
+ }
+ }
+
+ ///
+ /// Returns a constant indicating the device phone type. This indicates the type of radio used to transmit voice calls.
+ ///
+ Future get phoneType async {
+ final int? type =
+ await _foregroundChannel.invokeMethod(GET_PHONE_TYPE);
+ if (type != null) {
+ return PhoneType.values[type];
+ } else {
+ return PhoneType.UNKNOWN;
+ }
+ }
+
+ ///
+ /// Returns the MCC+MNC (mobile country code + mobile network code) of the provider of the SIM. 5 or 6 decimal digits.
+ ///
+ /// Availability: SimState must be SIM\_STATE\_READY
+ Future get simOperator =>
+ _foregroundChannel.invokeMethod(GET_SIM_OPERATOR);
+
+ ///
+ /// Returns the Service Provider Name (SPN).
+ ///
+ /// Availability: SimState must be SIM_STATE_READY
+ Future get simOperatorName =>
+ _foregroundChannel.invokeMethod(GET_SIM_OPERATOR_NAME);
+
+ ///
+ /// Returns a constant indicating the state of the default SIM card.
+ ///
+ /// Returns:
+ ///
+ /// [Future]
+ Future get simState async {
+ final int? state =
+ await _foregroundChannel.invokeMethod(GET_SIM_STATE);
+ if (state != null) {
+ return SimState.values[state];
+ } else {
+ return SimState.UNKNOWN;
+ }
+ }
+
+ ///
+ /// Returns true if the device is considered roaming on the current network, for GSM purposes.
+ ///
+ /// Availability: Only when user registered to a network.
+ Future get isNetworkRoaming =>
+ _foregroundChannel.invokeMethod(IS_NETWORK_ROAMING);
+
+ ///
+ /// Returns a List of SignalStrength or an empty List if there are no valid measurements.
+ ///
+ /// ### Requires Android build version 29 --> Android Q
+ ///
+ /// Returns:
+ ///
+ /// [Future>]
+ Future> get signalStrengths async {
+ final List? strengths =
+ await _foregroundChannel.invokeMethod(GET_SIGNAL_STRENGTH);
+ return (strengths ?? [])
+ .map((s) => SignalStrength.values[s])
+ .toList(growable: false);
+ }
+
+ ///
+ /// Returns current voice service state.
+ ///
+ /// ### Requires Android build version 26 --> Android O
+ /// ### Requires permissions ACCESS_COARSE_LOCATION and READ_PHONE_STATE
+ ///
+ /// Returns:
+ ///
+ /// [Future]
+ Future get serviceState async {
+ final int? state =
+ await _foregroundChannel.invokeMethod(GET_SERVICE_STATE);
+ if (state != null) {
+ return ServiceState.values[state];
+ } else {
+ return ServiceState.UNKNOWN;
+ }
+ }
+
+ ///
+ /// Request the user for all the sms permissions listed in the app's AndroidManifest.xml
+ ///
+ Future get requestSmsPermissions =>
+ _foregroundChannel.invokeMethod(REQUEST_SMS_PERMISSION);
+
+ ///
+ /// Request the user for all the phone permissions listed in the app's AndroidManifest.xml
+ ///
+ Future get requestPhonePermissions =>
+ _foregroundChannel.invokeMethod(REQUEST_PHONE_PERMISSION);
+
+ ///
+ /// Request the user for all the phone and sms permissions listed in the app's AndroidManifest.xml
+ ///
+ Future get requestPhoneAndSmsPermissions =>
+ _foregroundChannel.invokeMethod(REQUEST_PHONE_AND_SMS_PERMISSION);
+
+ ///
+ /// Opens the default dialer with the given phone number.
+ ///
+ Future openDialer(String phoneNumber) async {
+ assert(phoneNumber.isNotEmpty, "phoneNumber cannot be empty");
+ final Map args = {"phoneNumber": phoneNumber};
+ await _foregroundChannel.invokeMethod(OPEN_DIALER, args);
+ }
+
+ ///
+ /// Starts a phone all with the given phone number.
+ ///
+ /// ### Requires permission CALL_PHONE
+ ///
+ Future dialPhoneNumber(String phoneNumber) async {
+ assert(phoneNumber.isNotEmpty, "phoneNumber cannot be null or empty");
+ final Map args = {"phoneNumber": phoneNumber};
+ await _foregroundChannel.invokeMethod(DIAL_PHONE_NUMBER, args);
+ }
+}
+
+///
+/// Represents a message returned by one of the query functions such as
+/// [getInboxSms], [getSentSms], [getDraftSms]
+class SmsMessage {
+ int? id;
+ String? address;
+ String? body;
+ int? date;
+ int? dateSent;
+ bool? read;
+ bool? seen;
+ String? subject;
+ int? subscriptionId;
+ int? threadId;
+ SmsType? type;
+ SmsStatus? status;
+ String? serviceCenterAddress;
+
+ /// ## Do not call this method. This method is visible only for testing.
+ @visibleForTesting
+ SmsMessage.fromMap(Map rawMessage, List columns) {
+ final message = Map.castFrom(rawMessage);
+ for (var column in columns) {
+ debugPrint('Column is ${column._columnName}');
+ final value = message[column._columnName];
+ switch (column._columnName) {
+ case _SmsProjections.ID:
+ this.id = int.tryParse(value);
+ break;
+ case _SmsProjections.ORIGINATING_ADDRESS:
+ case _SmsProjections.ADDRESS:
+ this.address = value;
+ break;
+ case _SmsProjections.MESSAGE_BODY:
+ case _SmsProjections.BODY:
+ this.body = value;
+ break;
+ case _SmsProjections.DATE:
+ case _SmsProjections.TIMESTAMP:
+ this.date = int.tryParse(value);
+ break;
+ case _SmsProjections.DATE_SENT:
+ this.dateSent = int.tryParse(value);
+ break;
+ case _SmsProjections.READ:
+ this.read = int.tryParse(value) == 0 ? false : true;
+ break;
+ case _SmsProjections.SEEN:
+ this.seen = int.tryParse(value) == 0 ? false : true;
+ break;
+ case _SmsProjections.STATUS:
+ switch (int.tryParse(value)) {
+ case 0:
+ this.status = SmsStatus.STATUS_COMPLETE;
+ break;
+ case 32:
+ this.status = SmsStatus.STATUS_PENDING;
+ break;
+ case 64:
+ this.status = SmsStatus.STATUS_FAILED;
+ break;
+ case -1:
+ default:
+ this.status = SmsStatus.STATUS_NONE;
+ break;
+ }
+ break;
+ case _SmsProjections.SUBJECT:
+ this.subject = value;
+ break;
+ case _SmsProjections.SUBSCRIPTION_ID:
+ this.subscriptionId = int.tryParse(value);
+ break;
+ case _SmsProjections.THREAD_ID:
+ this.threadId = int.tryParse(value);
+ break;
+ case _SmsProjections.TYPE:
+ var smsTypeIndex = int.tryParse(value);
+ this.type =
+ smsTypeIndex != null ? SmsType.values[smsTypeIndex] : null;
+ break;
+ case _SmsProjections.SERVICE_CENTER_ADDRESS:
+ this.serviceCenterAddress = value;
+ break;
+ }
+ }
+ }
+
+ /// ## Do not call this method. This method is visible only for testing.
+ @visibleForTesting
+ bool equals(SmsMessage other) {
+ return this.id == other.id &&
+ this.address == other.address &&
+ this.body == other.body &&
+ this.date == other.date &&
+ this.dateSent == other.dateSent &&
+ this.read == other.read &&
+ this.seen == other.seen &&
+ this.subject == other.subject &&
+ this.subscriptionId == other.subscriptionId &&
+ this.threadId == other.threadId &&
+ this.type == other.type &&
+ this.status == other.status;
+ }
+}
+
+///
+/// Represents a conversation returned by the query conversation functions
+/// [getConversations]
+class SmsConversation {
+ String? snippet;
+ int? threadId;
+ int? messageCount;
+
+ /// ## Do not call this method. This method is visible only for testing.
+ @visibleForTesting
+ SmsConversation.fromMap(Map rawConversation) {
+ final conversation =
+ Map.castFrom(rawConversation);
+ for (var column in DEFAULT_CONVERSATION_COLUMNS) {
+ final String? value = conversation[column._columnName];
+ switch (column._columnName) {
+ case _ConversationProjections.SNIPPET:
+ this.snippet = value;
+ break;
+ case _ConversationProjections.THREAD_ID:
+ this.threadId = int.tryParse(value!);
+ break;
+ case _ConversationProjections.MSG_COUNT:
+ this.messageCount = int.tryParse(value!);
+ break;
+ }
+ }
+ }
+
+ /// ## Do not call this method. This method is visible only for testing.
+ @visibleForTesting
+ bool equals(SmsConversation other) {
+ return this.threadId == other.threadId &&
+ this.snippet == other.snippet &&
+ this.messageCount == other.messageCount;
+ }
+}
diff --git a/pubspec.yaml b/pubspec.yaml
new file mode 100644
index 0000000..06540cf
--- /dev/null
+++ b/pubspec.yaml
@@ -0,0 +1,66 @@
+name: telephony
+description: A Flutter plugin to use telephony features such as fetch network
+ info, start phone calls, send and receive SMS, and listen for incoming SMS.
+version: 0.2.0
+homepage: https://telephony.shounakmulay.dev
+repository: https://github.com/shounakmulay/Telephony
+
+environment:
+ sdk: '>=2.15.1 <3.0.0'
+ flutter: ">=1.10.0"
+
+dependencies:
+ flutter:
+ sdk: flutter
+ platform: ^3.1.0
+
+dev_dependencies:
+ flutter_test:
+ sdk: flutter
+ build_runner: ^2.1.10
+ collection: ^1.16.0
+ mockito: ^5.2.0
+# For information on the generic Dart part of this file, see the
+# following page: https://dart.dev/tools/pub/pubspec
+# The following section is specific to Flutter.
+flutter:
+ # This section identifies this Flutter project as a plugin project.
+ # The 'pluginClass' and Android 'package' identifiers should not ordinarily
+ # be modified. They are used by the tooling to maintain consistency when
+ # adding or updating assets for this project.
+ plugin:
+ platforms:
+ android:
+ package: com.shounakmulay.telephony
+ pluginClass: TelephonyPlugin
+ ios:
+ pluginClass: TelephonyPlugin
+ # To add assets to your plugin package, add an assets section, like this:
+ # assets:
+ # - images/a_dot_burr.jpeg
+ # - images/a_dot_ham.jpeg
+ #
+ # For details regarding assets in packages, see
+ # https://flutter.dev/assets-and-images/#from-packages
+ #
+ # An image asset can refer to one or more resolution-specific "variants", see
+ # https://flutter.dev/assets-and-images/#resolution-aware.
+ # To add custom fonts to your plugin package, add a fonts section here,
+ # in this "flutter" section. Each entry in this list should have a
+ # "family" key with the font family name, and a "fonts" key with a
+ # list giving the asset and other descriptors for the font. For
+ # example:
+ # fonts:
+ # - family: Schyler
+ # fonts:
+ # - asset: fonts/Schyler-Regular.ttf
+ # - asset: fonts/Schyler-Italic.ttf
+ # style: italic
+ # - family: Trajan Pro
+ # fonts:
+ # - asset: fonts/TrajanPro.ttf
+ # - asset: fonts/TrajanPro_Bold.ttf
+ # weight: 700
+ #
+ # For details regarding fonts in packages, see
+ # https://flutter.dev/custom-fonts/#from-packages
diff --git a/test/listener_test.dart b/test/listener_test.dart
new file mode 100644
index 0000000..0298d60
--- /dev/null
+++ b/test/listener_test.dart
@@ -0,0 +1,66 @@
+import "package:flutter/services.dart";
+import "package:flutter_test/flutter_test.dart";
+import "package:platform/platform.dart";
+import "package:telephony/telephony.dart";
+import 'mocks/messages.dart';
+
+main() {
+ TestWidgetsFlutterBinding.ensureInitialized();
+
+ late MethodChannel methodChannel;
+ late Telephony telephony;
+ final List log = [];
+ SmsSendStatusListener listener;
+
+ setUp(() {
+ methodChannel = MethodChannel("testChannel");
+ telephony = Telephony.private(
+ methodChannel, FakePlatform(operatingSystem: "android"));
+ methodChannel.setMockMethodCallHandler((call) {
+ log.add(call);
+ return telephony.handler(call);
+ });
+ });
+
+ tearDown(() {
+ methodChannel.setMockMethodCallHandler(null);
+ log.clear();
+ });
+
+ group("should listen to", () {
+ test("sms sent status", () async {
+ listener = (status) {
+ expect(status, SendStatus.SENT);
+ };
+
+ final args = {
+ "address": "0000000000",
+ "message_body": "Test message",
+ "listen_status": true
+ };
+
+ telephony.sendSms(
+ to: "0000000000", message: "Test message", statusListener: listener);
+
+ expect(log, [isMethodCall(SEND_SMS, arguments: args)]);
+
+ // called by native side
+ methodChannel.invokeMethod(SMS_SENT);
+
+ expect(log.length, 2);
+ expect(log.last, isMethodCall(SMS_SENT, arguments: null));
+ });
+
+ test("incoming sms", () async {
+ telephony.listenIncomingSms(
+ onNewMessage: (message) {
+ expect(message.body, mockIncomingMessage["message_body"]);
+ expect(message.address, mockIncomingMessage["originating_address"]);
+ expect(message.status, SmsStatus.STATUS_COMPLETE);
+ },
+ listenInBackground: false);
+
+ methodChannel.invokeMethod(ON_MESSAGE, {"message": mockIncomingMessage});
+ });
+ });
+}
diff --git a/test/mocks/messages.dart b/test/mocks/messages.dart
new file mode 100644
index 0000000..8016e13
--- /dev/null
+++ b/test/mocks/messages.dart
@@ -0,0 +1,50 @@
+import 'dart:collection';
+
+final mockMessages = [
+ LinkedHashMap.from({
+ "_id": "1",
+ "address": "123456",
+ "body": "message body",
+ "date": "1595056125597",
+ "thread_id": "3"
+ }),
+ LinkedHashMap.from({
+ "_id": "12",
+ "address": "0000000000",
+ "body": "text message",
+ "date": "1595056125663",
+ "thread_id": "6"
+ })
+];
+
+final mockMessageWithSmsType = LinkedHashMap.from({
+ "_id": "1",
+ "address": "123456",
+ "body": "message body",
+ "date": "1595056125597",
+ "thread_id": "3",
+ "type": "1"
+ });
+
+ final mockMessageWithInvalidSmsType = LinkedHashMap.from({
+ "_id": "1",
+ "address": "123456",
+ "body": "message body",
+ "date": "1595056125597",
+ "thread_id": "3",
+ "type": "type"
+ });
+
+final mockConversations = [
+ LinkedHashMap.from(
+ {"snippet": "message snippet", "thread_id": "2", "msg_count": "32"}),
+ LinkedHashMap.from(
+ {"snippet": "snippet", "thread_id": "5", "msg_count": "20"})
+];
+
+const mockIncomingMessage = {
+ "originating_address": "123456789",
+ "message_body": "incoming sms",
+ "timestamp": "123422135",
+ "status": "0"
+};
diff --git a/test/telephony_test.dart b/test/telephony_test.dart
new file mode 100644
index 0000000..3c10dce
--- /dev/null
+++ b/test/telephony_test.dart
@@ -0,0 +1,522 @@
+import 'dart:collection';
+
+import "package:flutter/services.dart";
+import "package:flutter_test/flutter_test.dart";
+import 'package:mockito/annotations.dart';
+import "package:mockito/mockito.dart";
+import "package:platform/platform.dart";
+import "package:telephony/telephony.dart";
+import 'package:collection/collection.dart';
+
+import 'mocks/messages.dart';
+import 'telephony_test.mocks.dart';
+
+@GenerateMocks([MethodChannel])
+main() {
+ TestWidgetsFlutterBinding.ensureInitialized();
+
+ MockMethodChannel methodChannel = MockMethodChannel();
+ late Telephony telephony;
+
+ setUp(() {
+ methodChannel = MockMethodChannel();
+ telephony = Telephony.private(
+ methodChannel, FakePlatform(operatingSystem: "android"));
+ });
+
+ tearDown(() {
+ verifyNoMoreInteractions(methodChannel);
+ });
+
+ group('telephony', () {
+ group("should request", () {
+ test("sms permissions", () async {
+ when(methodChannel.invokeMethod(REQUEST_SMS_PERMISSION))
+ .thenAnswer((_) => Future.value(true));
+ final permissionGranted = await telephony.requestSmsPermissions;
+ verify(methodChannel.invokeMethod(REQUEST_SMS_PERMISSION))
+ .called(1);
+ expect(permissionGranted, true);
+ });
+
+ test("phone permissions", () async {
+ when(methodChannel.invokeMethod(REQUEST_PHONE_PERMISSION))
+ .thenAnswer((_) => Future.value(true));
+ final permissionGranted = await telephony.requestPhonePermissions;
+ verify(methodChannel.invokeMethod(REQUEST_PHONE_PERMISSION))
+ .called(1);
+ expect(permissionGranted, true);
+ });
+
+ test("phone and sms permissions", () async {
+ when(methodChannel.invokeMethod(REQUEST_PHONE_AND_SMS_PERMISSION))
+ .thenAnswer((_) => Future.value(true));
+ final permissionGranted = await telephony.requestPhoneAndSmsPermissions;
+ verify(methodChannel
+ .invokeMethod(REQUEST_PHONE_AND_SMS_PERMISSION))
+ .called(1);
+ expect(permissionGranted, true);
+ });
+ });
+
+ group("should get", () {
+ test("service state", () async {
+ when(methodChannel.invokeMethod(GET_SERVICE_STATE))
+ .thenAnswer((_) => Future.value(1));
+ final state = await telephony.serviceState;
+ verify(methodChannel.invokeMethod(GET_SERVICE_STATE)).called(1);
+ expect(state, ServiceState.OUT_OF_SERVICE);
+ });
+
+ test("signal strengths", () async {
+ when(methodChannel.invokeMethod(GET_SIGNAL_STRENGTH))
+ .thenAnswer((_) => Future.value([0, 1]));
+ final strengths = await telephony.signalStrengths;
+ verify(methodChannel.invokeMethod(GET_SIGNAL_STRENGTH)).called(1);
+ expect(
+ strengths, [SignalStrength.NONE_OR_UNKNOWN, SignalStrength.POOR]);
+ });
+
+ test("is network roaming", () async {
+ when(methodChannel.invokeMethod(IS_NETWORK_ROAMING))
+ .thenAnswer((_) => Future.value(false));
+ final result = await telephony.isNetworkRoaming;
+ verify(methodChannel.invokeMethod(IS_NETWORK_ROAMING)).called(1);
+ expect(result, false);
+ });
+
+ test("sim state", () async {
+ when(methodChannel.invokeMethod(GET_SIM_STATE))
+ .thenAnswer((_) => Future.value(5));
+ final state = await telephony.simState;
+ verify(methodChannel.invokeMethod(GET_SIM_STATE)).called(1);
+ expect(state, SimState.READY);
+ });
+
+ test("sim operator name", () async {
+ when(methodChannel.invokeMethod(GET_SIM_OPERATOR_NAME))
+ .thenAnswer((_) => Future.value("operatorName"));
+ final name = await telephony.simOperatorName;
+ verify(methodChannel.invokeMethod(GET_SIM_OPERATOR_NAME)).called(1);
+ expect(name, "operatorName");
+ });
+
+ test("sim operator", () async {
+ when(methodChannel.invokeMethod(GET_SIM_OPERATOR))
+ .thenAnswer((_) => Future.value("operator"));
+ final name = await telephony.simOperator;
+ verify(methodChannel.invokeMethod(GET_SIM_OPERATOR)).called(1);
+ expect(name, "operator");
+ });
+
+ test("phone type", () async {
+ when(methodChannel.invokeMethod(GET_PHONE_TYPE))
+ .thenAnswer((_) => Future.value(2));
+ final type = await telephony.phoneType;
+ verify(methodChannel.invokeMethod(GET_PHONE_TYPE)).called(1);
+ expect(type, PhoneType.CDMA);
+ });
+
+ test("data network type", () async {
+ when(methodChannel.invokeMethod(GET_DATA_NETWORK_TYPE))
+ .thenAnswer((_) => Future.value(0));
+ final type = await telephony.dataNetworkType;
+ verify(methodChannel.invokeMethod(GET_DATA_NETWORK_TYPE)).called(1);
+ expect(type, NetworkType.UNKNOWN);
+ });
+
+ test("network operator name", () async {
+ when(methodChannel.invokeMethod(GET_NETWORK_OPERATOR_NAME))
+ .thenAnswer((_) => Future.value("operator name"));
+ final name = await telephony.networkOperatorName;
+ verify(methodChannel.invokeMethod(GET_NETWORK_OPERATOR_NAME)).called(1);
+ expect(name, "operator name");
+ });
+
+ test("network operator", () async {
+ when(methodChannel.invokeMethod(GET_NETWORK_OPERATOR))
+ .thenAnswer((_) => Future.value("operator"));
+ final operator = await telephony.networkOperator;
+ verify(methodChannel.invokeMethod(GET_NETWORK_OPERATOR)).called(1);
+ expect(operator, "operator");
+ });
+
+ test("data activity", () async {
+ when(methodChannel.invokeMethod(GET_DATA_ACTIVITY))
+ .thenAnswer((_) => Future.value(1));
+ final activity = await telephony.dataActivity;
+ verify(methodChannel.invokeMethod(GET_DATA_ACTIVITY)).called(1);
+ expect(activity, DataActivity.IN);
+ });
+
+ test("call state", () async {
+ when(methodChannel.invokeMethod(GET_CALL_STATE))
+ .thenAnswer((_) => Future.value(2));
+ final state = await telephony.callState;
+ verify(methodChannel.invokeMethod(GET_CALL_STATE)).called(1);
+ expect(state, CallState.OFFHOOK);
+ });
+
+ test("cellular data state", () async {
+ when(methodChannel.invokeMethod(GET_CELLULAR_DATA_STATE))
+ .thenAnswer((_) => Future.value(0));
+ final state = await telephony.cellularDataState;
+ verify(methodChannel.invokeMethod(GET_CELLULAR_DATA_STATE)).called(1);
+ expect(state, DataState.DISCONNECTED);
+ });
+
+ test("is sms capable", () async {
+ when(methodChannel.invokeMethod(IS_SMS_CAPABLE))
+ .thenAnswer((_) => Future.value(true));
+ final result = await telephony.isSmsCapable;
+ verify(methodChannel.invokeMethod(IS_SMS_CAPABLE)).called(1);
+ expect(result, true);
+ });
+ });
+
+ group("should send", () {
+ test("sms", () async {
+ final String address = "0000000000";
+ final String body = "Test message";
+ when(methodChannel.invokeMethod(SEND_SMS, {
+ "address": address,
+ "message_body": body,
+ "listen_status": false
+ })).thenAnswer((realInvocation) => Future.value());
+ telephony.sendSms(to: address, message: body);
+ verify(methodChannel.invokeMethod(SEND_SMS, {
+ "address": address,
+ "message_body": body,
+ "listen_status": false
+ })).called(1);
+ });
+
+ test("multipart message", () async {
+ final args = {
+ "address": "123456",
+ "message_body": "some long message",
+ "listen_status": false
+ };
+ when(methodChannel.invokeMethod(SEND_MULTIPART_SMS, args))
+ .thenAnswer((realInvocation) => Future.value());
+ telephony.sendSms(
+ to: "123456", message: "some long message", isMultipart: true);
+
+ verifyNever(methodChannel.invokeMethod(SEND_SMS, args));
+
+ verify(methodChannel.invokeMethod(SEND_MULTIPART_SMS, args)).called(1);
+ });
+
+ test("sms by default app", () async {
+ final String address = "123456";
+ final String body = "message";
+ when(methodChannel.invokeMethod(
+ SEND_SMS_INTENT, {"address": address, "message_body": body}))
+ .thenAnswer((realInvocation) => Future.value());
+ telephony.sendSmsByDefaultApp(to: address, message: body);
+
+ verify(methodChannel.invokeMethod(
+ SEND_SMS_INTENT, {"address": address, "message_body": body}))
+ .called(1);
+ });
+ });
+
+ group("smsMessage fromMap should", () {
+ test("correctly parse SmsType", () {
+ final columns = DEFAULT_SMS_COLUMNS.toList();
+ columns.add(SmsColumn.TYPE);
+ final message = mockMessageWithSmsType;
+ final sms = SmsMessage.fromMap(message, columns);
+
+ expect(sms.type, equals(SmsType.MESSAGE_TYPE_INBOX));
+ });
+
+ test("correctly parse SmsType when tryParse returns null", () {
+ final columns = DEFAULT_SMS_COLUMNS.toList();
+ columns.add(SmsColumn.TYPE);
+ final message = mockMessageWithInvalidSmsType;
+ final sms = SmsMessage.fromMap(message, columns);
+
+ expect(sms.type, equals(null));
+ });
+ });
+
+ group("should query", () {
+ test("inbox", () async {
+ final args = {
+ "projection": ["_id", "address", "body", "date"],
+ };
+
+ when(methodChannel.invokeMethod>(
+ GET_ALL_INBOX_SMS, args))
+ .thenAnswer((_) => Future.value(mockMessages));
+
+ final inbox = await telephony.getInboxSms();
+
+ verify(methodChannel.invokeMethod(GET_ALL_INBOX_SMS, args)).called(1);
+
+ expect(
+ inbox[0].equals(
+ SmsMessage.fromMap(mockMessages[0], DEFAULT_SMS_COLUMNS)),
+ isTrue);
+ expect(
+ inbox[1].equals(
+ SmsMessage.fromMap(mockMessages[1], DEFAULT_SMS_COLUMNS)),
+ isTrue);
+ });
+
+ test("inbox with filters", () async {
+ final columns = [SmsColumn.ID, SmsColumn.ADDRESS];
+ final SmsFilter filter = SmsFilter.where(SmsColumn.ID)
+ .equals("3")
+ .and(SmsColumn.ADDRESS)
+ .like("mess");
+ final sortOrder = [OrderBy(SmsColumn.ID, sort: Sort.ASC)];
+
+ final args = {
+ "projection": ["_id", "address"],
+ "selection": " _id = ? AND address LIKE ?",
+ "selection_args": ["3", "mess"],
+ "sort_order": "_id ASC"
+ };
+
+ when(methodChannel.invokeMethod>(
+ GET_ALL_INBOX_SMS, args))
+ .thenAnswer((_) => Future.value(mockMessages));
+
+ final inbox = await telephony.getInboxSms(
+ columns: columns, filter: filter, sortOrder: sortOrder);
+ verify(await methodChannel.invokeMethod(GET_ALL_INBOX_SMS, args))
+ .called(1);
+ expect(inbox[0].equals(SmsMessage.fromMap(mockMessages[0], columns)),
+ isTrue);
+ expect(inbox[1].equals(SmsMessage.fromMap(mockMessages[1], columns)),
+ isTrue);
+ });
+
+ test("sent", () async {
+ final args = {
+ "projection": ["_id", "address", "body", "date"],
+ };
+
+ when(methodChannel.invokeMethod>(
+ GET_ALL_SENT_SMS, args))
+ .thenAnswer((_) => Future.value(mockMessages));
+
+ final sent = await telephony.getSentSms();
+
+ verify(methodChannel.invokeMethod(GET_ALL_SENT_SMS, args)).called(1);
+
+ expect(
+ sent[0].equals(
+ SmsMessage.fromMap(mockMessages[0], DEFAULT_SMS_COLUMNS)),
+ isTrue);
+ expect(
+ sent[1].equals(
+ SmsMessage.fromMap(mockMessages[1], DEFAULT_SMS_COLUMNS)),
+ isTrue);
+ });
+
+ test("sent with filters", () async {
+ final columns = [SmsColumn.ID, SmsColumn.ADDRESS];
+ final SmsFilter filter = SmsFilter.where(SmsColumn.ID)
+ .equals("4")
+ .and(SmsColumn.DATE)
+ .greaterThan("12");
+ final sortOrder = [OrderBy(SmsColumn.ID, sort: Sort.ASC)];
+
+ final args = {
+ "projection": ["_id", "address"],
+ "selection": " _id = ? AND date > ?",
+ "selection_args": ["4", "12"],
+ "sort_order": "_id ASC"
+ };
+
+ when(methodChannel.invokeMethod>(
+ GET_ALL_SENT_SMS, args))
+ .thenAnswer((_) => Future.value(mockMessages));
+
+ final sent = await telephony.getSentSms(
+ columns: columns, filter: filter, sortOrder: sortOrder);
+ verify(await methodChannel.invokeMethod(GET_ALL_SENT_SMS, args))
+ .called(1);
+ expect(sent[0].equals(SmsMessage.fromMap(mockMessages[0], columns)),
+ isTrue);
+ expect(sent[1].equals(SmsMessage.fromMap(mockMessages[1], columns)),
+ isTrue);
+ });
+
+ test("draft", () async {
+ final args = {
+ "projection": ["_id", "address", "body", "date"],
+ };
+
+ when(methodChannel.invokeMethod>(
+ GET_ALL_DRAFT_SMS, args))
+ .thenAnswer((_) => Future.value(mockMessages));
+
+ final drafts = await telephony.getDraftSms();
+
+ verify(methodChannel.invokeMethod(GET_ALL_DRAFT_SMS, args)).called(1);
+
+ expect(
+ drafts[0].equals(
+ SmsMessage.fromMap(mockMessages[0], DEFAULT_SMS_COLUMNS)),
+ isTrue);
+ expect(
+ drafts[1].equals(
+ SmsMessage.fromMap(mockMessages[1], DEFAULT_SMS_COLUMNS)),
+ isTrue);
+ });
+
+ test("draft with filters", () async {
+ final columns = [SmsColumn.ID, SmsColumn.ADDRESS];
+ final SmsFilter filter = SmsFilter.where(SmsColumn.ID)
+ .equals("4")
+ .and(SmsColumn.DATE)
+ .greaterThan("12");
+ final sortOrder = [OrderBy(SmsColumn.ID, sort: Sort.ASC)];
+
+ final args = {
+ "projection": ["_id", "address"],
+ "selection": " _id = ? AND date > ?",
+ "selection_args": ["4", "12"],
+ "sort_order": "_id ASC"
+ };
+
+ when(methodChannel.invokeMethod>(
+ GET_ALL_DRAFT_SMS, args))
+ .thenAnswer((_) => Future.value(mockMessages));
+
+ final drafts = await telephony.getDraftSms(
+ columns: columns, filter: filter, sortOrder: sortOrder);
+ verify(await methodChannel.invokeMethod(GET_ALL_DRAFT_SMS, args))
+ .called(1);
+ expect(drafts[0].equals(SmsMessage.fromMap(mockMessages[0], columns)),
+ isTrue);
+ expect(drafts[1].equals(SmsMessage.fromMap(mockMessages[1], columns)),
+ isTrue);
+ });
+
+ test("conversations", () async {
+ final args = {
+ "projection": ["snippet", "thread_id", "msg_count"]
+ };
+
+ when(methodChannel.invokeMethod>(
+ GET_ALL_CONVERSATIONS, args))
+ .thenAnswer((realInvocation) => Future.value(mockConversations));
+
+ final conversations = await telephony.getConversations();
+
+ verify(methodChannel.invokeMethod(GET_ALL_CONVERSATIONS, args))
+ .called(1);
+ expect(
+ conversations[0]
+ .equals(SmsConversation.fromMap(mockConversations[0])),
+ isTrue);
+ expect(
+ conversations[1]
+ .equals(SmsConversation.fromMap(mockConversations[1])),
+ isTrue);
+ });
+
+ test("conversations with filter", () async {
+ final ConversationFilter filter =
+ ConversationFilter.where(ConversationColumn.MSG_COUNT)
+ .equals("4")
+ .and(ConversationColumn.THREAD_ID)
+ .greaterThan("12");
+ final sortOrder = [
+ OrderBy(ConversationColumn.THREAD_ID, sort: Sort.ASC)
+ ];
+
+ final args = {
+ "projection": ["snippet", "thread_id", "msg_count"],
+ "selection": " msg_count = ? AND thread_id > ?",
+ "selection_args": ["4", "12"],
+ "sort_order": "thread_id ASC"
+ };
+
+ when(methodChannel.invokeMethod>(
+ GET_ALL_CONVERSATIONS, args))
+ .thenAnswer((realInvocation) => Future.value(mockConversations));
+
+ final conversations = await telephony.getConversations(
+ filter: filter, sortOrder: sortOrder);
+
+ verify(await methodChannel.invokeMethod(GET_ALL_CONVERSATIONS, args))
+ .called(1);
+ expect(
+ conversations[0]
+ .equals(SmsConversation.fromMap(mockConversations[0])),
+ isTrue);
+ expect(
+ conversations[1]
+ .equals(SmsConversation.fromMap(mockConversations[1])),
+ isTrue);
+ });
+ });
+
+ group("should generate", () {
+ test("sms filter statement", () async {
+ final SmsFilter statement = SmsFilter.where(SmsColumn.ADDRESS)
+ .greaterThan("1")
+ .and(SmsColumn.ID)
+ .greaterThanOrEqualTo("2")
+ .or(SmsColumn.DATE)
+ .between("3", "4")
+ .or(SmsColumn.TYPE)
+ .not
+ .like("5");
+
+ expect(
+ statement.selection,
+ equals(
+ " address > ? AND _id >= ? OR date BETWEEN ? OR NOT type LIKE ?"));
+ expect(
+ ListEquality()
+ .equals(statement.selectionArgs, ["1", "2", "3 AND 4", "5"]),
+ isTrue);
+ });
+
+ test("conversation filter statement", () async {
+ final ConversationFilter statement =
+ ConversationFilter.where(ConversationColumn.THREAD_ID)
+ .lessThanOrEqualTo("1")
+ .or(ConversationColumn.MSG_COUNT)
+ .notEqualTo("6")
+ .and(ConversationColumn.SNIPPET)
+ .not
+ .notEqualTo("7");
+
+ expect(statement.selection,
+ " thread_id <= ? OR msg_count != ? AND NOT snippet != ?");
+ expect(ListEquality().equals(statement.selectionArgs, ["1", "6", "7"]),
+ isTrue);
+ });
+ });
+
+ group("should initiate call", () {
+ test("via default phone app", () async {
+ final args = {"phoneNumber": "123456789"};
+ when(methodChannel.invokeMethod(OPEN_DIALER, args))
+ .thenAnswer((realInvocation) async {});
+
+ await telephony.openDialer("123456789");
+
+ verify(methodChannel.invokeMethod(OPEN_DIALER, args)).called(1);
+ });
+
+ test('directly', () async {
+ final args = {"phoneNumber": "123456789"};
+ when(methodChannel.invokeMethod(DIAL_PHONE_NUMBER, args))
+ .thenAnswer((realInvocation) async {});
+
+ await telephony.dialPhoneNumber("123456789");
+
+ verify(methodChannel.invokeMethod(DIAL_PHONE_NUMBER, args)).called(1);
+ });
+ });
+ });
+}
diff --git a/test/telephony_test.mocks.dart b/test/telephony_test.mocks.dart
new file mode 100644
index 0000000..431e423
--- /dev/null
+++ b/test/telephony_test.mocks.dart
@@ -0,0 +1,65 @@
+// Mocks generated by Mockito 5.2.0 from annotations
+// in telephony/test/telephony_test.dart.
+// Do not manually edit this file.
+
+import 'dart:async' as _i5;
+
+import 'package:flutter/src/services/binary_messenger.dart' as _i3;
+import 'package:flutter/src/services/message_codec.dart' as _i2;
+import 'package:flutter/src/services/platform_channel.dart' as _i4;
+import 'package:mockito/mockito.dart' as _i1;
+
+// ignore_for_file: type=lint
+// ignore_for_file: avoid_redundant_argument_values
+// ignore_for_file: avoid_setters_without_getters
+// ignore_for_file: comment_references
+// ignore_for_file: implementation_imports
+// ignore_for_file: invalid_use_of_visible_for_testing_member
+// ignore_for_file: prefer_const_constructors
+// ignore_for_file: unnecessary_parenthesis
+// ignore_for_file: camel_case_types
+
+class _FakeMethodCodec_0 extends _i1.Fake implements _i2.MethodCodec {}
+
+class _FakeBinaryMessenger_1 extends _i1.Fake implements _i3.BinaryMessenger {}
+
+/// A class which mocks [MethodChannel].
+///
+/// See the documentation for Mockito's code generation for more information.
+class MockMethodChannel extends _i1.Mock implements _i4.MethodChannel {
+ MockMethodChannel() {
+ _i1.throwOnMissingStub(this);
+ }
+
+ @override
+ String get name =>
+ (super.noSuchMethod(Invocation.getter(#name), returnValue: '') as String);
+ @override
+ _i2.MethodCodec get codec => (super.noSuchMethod(Invocation.getter(#codec),
+ returnValue: _FakeMethodCodec_0()) as _i2.MethodCodec);
+ @override
+ _i3.BinaryMessenger get binaryMessenger =>
+ (super.noSuchMethod(Invocation.getter(#binaryMessenger),
+ returnValue: _FakeBinaryMessenger_1()) as _i3.BinaryMessenger);
+ @override
+ _i5.Future invokeMethod(String? method, [dynamic arguments]) =>
+ (super.noSuchMethod(Invocation.method(#invokeMethod, [method, arguments]),
+ returnValue: Future.value()) as _i5.Future);
+ @override
+ _i5.Future?> invokeListMethod(String? method,
+ [dynamic arguments]) =>
+ (super.noSuchMethod(
+ Invocation.method(#invokeListMethod, [method, arguments]),
+ returnValue: Future?>.value()) as _i5.Future?>);
+ @override
+ _i5.Future