first commit

master
王亚玲 2 years ago
commit a7f9c11a4e

@ -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

@ -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.

@ -0,0 +1,326 @@
<p align="center">
<a href="https://pub.dev/packages/telephony" alt="Pub">
<img src="https://img.shields.io/pub/v/telephony" /></a>
<a href="https://github.com/shounakmulay/Telephony/releases" alt="Release">
<img src="https://img.shields.io/github/v/release/shounakmulay/telephony" /></a>
<a href="https://github.com/shounakmulay/Telephony/actions/workflows/Telephony_CI.yml?query=branch%3Adevelop" alt="Build">
<img src="https://github.com/shounakmulay/telephony/actions/workflows/Telephony_CI.yml/badge.svg?branch=develop" /></a>
</p>
# 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
<uses-permission android:name="android.permission.SEND_SMS"/>
```
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
<uses-permission android:name="android.permission.READ_SMS"/>
```
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<SmsMessage> 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
<uses-permission android:name="android.permission.READ_SMS"/>
```
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<SmsConversation> 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
<manifest>
<uses-permission android:name="android.permission.RECEIVE_SMS"/>
<application>
...
...
<receiver android:name="com.shounakmulay.telephony.sms.IncomingSmsReceiver"
android:permission="android.permission.BROADCAST_SMS" android:exported="true">
<intent-filter>
<action android:name="android.provider.Telephony.SMS_RECEIVED"/>
</intent-filter>
</receiver>
</application>
</manifest>
```
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<MyApp> {
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<MyApp> {
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

@ -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'
}

@ -0,0 +1,3 @@
org.gradle.jvmargs=-Xmx1536M
android.useAndroidX=true
android.enableJetifier=true

@ -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

@ -0,0 +1 @@
rootProject.name = 'telephony'

@ -0,0 +1,5 @@
<manifest package="com.shounakmulay.telephony"
xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.ACCESS_COARSE_LOCATION" />
</manifest>

@ -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<String>): 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<String>, requestCode: Int) {
if (!isRequestingPermission) {
isRequestingPermission = true
activity.requestPermissions(permissions.toTypedArray(), requestCode)
}
}
fun getSmsPermissions(): List<String> {
val permissions = getListedPermissions()
return permissions.filter { permission -> SMS_PERMISSIONS.contains(permission) }
}
fun getPhonePermissions(): List<String> {
val permissions = getListedPermissions()
return permissions.filter { permission -> PHONE_PERMISSIONS.contains(permission) }
}
fun getServiceStatePermissions(): List<String> {
val permissions = getListedPermissions()
return permissions.filter { permission -> SERVICE_STATE_PERMISSIONS.contains(permission) }
}
private fun getListedPermissions(): Array<out String> {
context.apply {
val info = packageManager.getPackageInfo(packageName, PackageManager.GET_PERMISSIONS)
return info.requestedPermissions ?: arrayOf()
}
}
}

@ -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)
}
}

@ -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
}

@ -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<SmsMessage>) {
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<String, Any>()
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<String, Any?>) {
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<String, Any?> {
val smsMap = HashMap<String, Any?>()
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<HashMap<String, Any?>>())
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<String, Any?>
) {
if (!this::backgroundChannel.isInitialized) {
throw RuntimeException(
"setBackgroundChannel was not called before messages came in, exiting."
)
}
val args: MutableMap<String, Any?> = 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<ActivityManager.RunningAppProcessInfo>
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!")
)
}
}
}

@ -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<String>,
selection: String?,
selectionArgs: List<String>?,
sortOrder: String?
): List<HashMap<String, String?>> {
val messages = mutableListOf<HashMap<String, String?>>()
val cursor = context.contentResolver.query(
contentUri.uri,
projection.toTypedArray(),
selection,
selectionArgs?.toTypedArray(),
sortOrder
)
while (cursor != null && cursor.moveToNext()) {
val dataObject = HashMap<String, String?>(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<PendingIntent>, ArrayList<PendingIntent>> {
val sentPendingIntents = arrayListOf<PendingIntent>()
val deliveredPendingIntents = arrayListOf<PendingIntent>()
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<PendingIntent, PendingIntent> {
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<Int>? {
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
}
}
}

@ -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<String>? = null
private var selection: String? = null
private var selectionArgs: List<String>? = 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<String>(MESSAGE_BODY)
val address = call.argument<String>(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<Long>(SETUP_HANDLE)
val backgroundHandle = call.argument<Long>(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<String>(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<String>, 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<out String>, grantResults: IntArray): Boolean {
permissionsController.isRequestingPermission = false
val deniedPermissions = mutableListOf<String>()
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<String>) {
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)
}
}
}
}
}

@ -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"
}

@ -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);
}

@ -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

@ -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.

@ -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"
}

@ -0,0 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.shounakmulay.telephony_example">
<!-- Flutter needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>

@ -0,0 +1,60 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.shounakmulay.telephony_example">
<!-- io.flutter.app.FlutterApplication is an android.app.Application that
calls FlutterMain.startInitialization(this); in its onCreate method.
In most cases you can leave this as-is, but you if you want to provide
additional functionality it is fine to subclass or reimplement
FlutterApplication and put your custom class here. -->
<uses-permission android:name="android.permission.READ_SMS"/>
<uses-permission android:name="android.permission.RECEIVE_SMS"/>
<uses-permission android:name="android.permission.SEND_SMS"/>
<uses-permission android:name="android.permission.READ_PHONE_STATE"/>
<uses-permission android:name="android.permission.CALL_PHONE"/>
<application
android:name=".MyApp"
android:label="telephony_example"
android:icon="@mipmap/ic_launcher">
<activity
android:name=".MainActivity"
android:launchMode="singleTop"
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize"
android:exported="true">
<!-- Specifies an Android theme to apply to this Activity as soon as
the Android process has started. This theme is visible to the user
while the Flutter UI initializes. After that, this theme continues
to determine the Window background behind the Flutter UI. -->
<meta-data
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme"
/>
<!-- Displays an Android View that continues showing the launch screen
Drawable until Flutter paints its first frame, then this splash
screen fades out. A splash screen is useful to avoid any visual
gap between the end of Android's launch screen and the painting of
Flutter's first frame. -->
<meta-data
android:name="io.flutter.embedding.android.SplashScreenDrawable"
android:resource="@drawable/launch_background"
/>
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
<meta-data
android:name="flutterEmbedding"
android:value="2" />
<receiver android:name="com.shounakmulay.telephony.sms.IncomingSmsReceiver"
android:permission="android.permission.BROADCAST_SMS" android:exported="true">
<intent-filter>
<action android:name="android.provider.Telephony.SMS_RECEIVED"/>
</intent-filter>
</receiver>
</application>
</manifest>

@ -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() {
}

@ -0,0 +1,12 @@
<?xml version="1.0" encoding="utf-8"?>
<!-- Modify this file to customize your launch splash screen -->
<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
<item android:drawable="@android:color/white" />
<!-- You can insert your own image assets here -->
<!-- <item>
<bitmap
android:gravity="center"
android:src="@mipmap/launch_image" />
</item> -->
</layer-list>

Binary file not shown.

After

Width:  |  Height:  |  Size: 544 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 442 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 721 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

@ -0,0 +1,18 @@
<?xml version="1.0" encoding="utf-8"?>
<resources>
<!-- Theme applied to the Android Window while the process is starting -->
<style name="LaunchTheme" parent="@android:style/Theme.Black.NoTitleBar">
<!-- Show a splash screen on the activity. Automatically removed when
Flutter draws its first frame -->
<item name="android:windowBackground">@drawable/launch_background</item>
</style>
<!-- Theme applied to the Android Window as soon as the process has started.
This theme determines the color of the Android Window while your
Flutter UI initializes, as well as behind your Flutter UI while its
running.
This Theme is only used starting with V2 of Flutter's Android embedding. -->
<style name="NormalTheme" parent="@android:style/Theme.Black.NoTitleBar">
<item name="android:windowBackground">@android:color/white</item>
</style>
</resources>

@ -0,0 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.shounakmulay.telephony_example">
<!-- Flutter needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>

@ -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
}

@ -0,0 +1,4 @@
org.gradle.jvmargs=-Xmx1536M
android.useAndroidX=true
android.enableJetifier=true
android.enableR8=true

@ -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

@ -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"

@ -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<MyApp> {
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<void> 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'))
],
),
));
}
}

@ -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

@ -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,
);
});
}

@ -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)
}
}

@ -0,0 +1,4 @@
#import <Flutter/Flutter.h>
@interface TelephonyPlugin : NSObject<FlutterPlugin>
@end

@ -0,0 +1,15 @@
#import "TelephonyPlugin.h"
#if __has_include(<telephony/telephony-Swift.h>)
#import <telephony/telephony-Swift.h>
#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<FlutterPluginRegistrar>*)registrar {
[SwiftTelephonyPlugin registerWithRegistrar:registrar];
}
@end

@ -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

@ -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 }

@ -0,0 +1,200 @@
part of 'telephony.dart';
abstract class Filter<T, K> {
T and(K column);
T or(K column);
String get selection;
List<String> 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<SmsFilterStatement, SmsColumn> {
final String _filter;
final List<String> _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<String> get selectionArgs => _filterArgs;
}
class ConversationFilter
extends Filter<ConversationFilterStatement, ConversationColumn> {
final String _filter;
final List<String> _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<String> get selectionArgs => _filterArgs;
}
abstract class FilterStatement<T extends Filter, K> {
String _column;
String _previousFilter = "";
List<String> _previousFilterArgs = [];
FilterStatement._(this._column);
FilterStatement._withPreviousFilter(
String previousFilter, String column, List<String> 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<String> 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<SmsFilter, SmsFilterStatement> {
SmsFilterStatement._(String column) : super._(column);
SmsFilterStatement._withPreviousFilter(
String previousFilter, String column, List<String> 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<ConversationFilter, ConversationFilterStatement> {
ConversationFilterStatement._(String column) : super._(column);
ConversationFilterStatement._withPreviousFilter(
String previousFilter, String column, List<String> 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}";
}

@ -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<void>.value();
}
});
backgroundChannel.invokeMethod<void>(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<bool>(
'startBackgroundService',
<String, dynamic>{
'setupHandle': backgroundSetupHandle.toRawHandle(),
'backgroundHandle': backgroundMessageHandle.toRawHandle()
},
);
} else {
_foregroundChannel.invokeMethod('disableBackgroundService');
}
}
/// ## Do not call this method. This method is visible only for testing.
@visibleForTesting
Future<dynamic> 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<List<SmsMessage>>]
Future<List<SmsMessage>> getInboxSms(
{List<SmsColumn> columns = DEFAULT_SMS_COLUMNS,
SmsFilter? filter,
List<OrderBy>? sortOrder}) async {
assert(_platform.isAndroid == true, "Can only be called on Android.");
final args = _getArguments(columns, filter, sortOrder);
final messages =
await _foregroundChannel.invokeMethod<List?>(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<List<SmsMessage>>]
Future<List<SmsMessage>> getSentSms(
{List<SmsColumn> columns = DEFAULT_SMS_COLUMNS,
SmsFilter? filter,
List<OrderBy>? sortOrder}) async {
assert(_platform.isAndroid == true, "Can only be called on Android.");
final args = _getArguments(columns, filter, sortOrder);
final messages =
await _foregroundChannel.invokeMethod<List?>(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<List<SmsMessage>>]
Future<List<SmsMessage>> getDraftSms(
{List<SmsColumn> columns = DEFAULT_SMS_COLUMNS,
SmsFilter? filter,
List<OrderBy>? sortOrder}) async {
assert(_platform.isAndroid == true, "Can only be called on Android.");
final args = _getArguments(columns, filter, sortOrder);
final messages =
await _foregroundChannel.invokeMethod<List?>(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<List<SmsConversation>>]
Future<List<SmsConversation>> getConversations(
{ConversationFilter? filter, List<OrderBy>? 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<List?>(
GET_ALL_CONVERSATIONS, args);
return conversations
?.map((conversation) => SmsConversation.fromMap(conversation))
.toList(growable: false) ??
List.empty();
}
Map<String, dynamic> _getArguments(List<_TelephonyColumn> columns,
Filter? filter, List<OrderBy>? sortOrder) {
final Map<String, dynamic> 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<void> 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<String, dynamic> 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<void> sendSmsByDefaultApp({
required String to,
required String message,
}) async {
final Map<String, dynamic> 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<bool?> get isSmsCapable =>
_foregroundChannel.invokeMethod<bool>(IS_SMS_CAPABLE);
///
/// Returns a constant indicating the current data connection state (cellular).
///
/// Returns:
///
/// [Future<DataState>]
Future<DataState> get cellularDataState async {
final int? dataState =
await _foregroundChannel.invokeMethod<int>(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<CallState>]
Future<CallState> get callState async {
final int? state =
await _foregroundChannel.invokeMethod<int>(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<CallState>]
Future<DataActivity> get dataActivity async {
final int? activity =
await _foregroundChannel.invokeMethod<int>(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<String?> get networkOperator =>
_foregroundChannel.invokeMethod<String>(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<String?> get networkOperatorName =>
_foregroundChannel.invokeMethod<String>(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<NetworkType> get dataNetworkType async {
final int? type =
await _foregroundChannel.invokeMethod<int>(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<PhoneType> get phoneType async {
final int? type =
await _foregroundChannel.invokeMethod<int>(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<String?> get simOperator =>
_foregroundChannel.invokeMethod<String>(GET_SIM_OPERATOR);
///
/// Returns the Service Provider Name (SPN).
///
/// Availability: SimState must be SIM_STATE_READY
Future<String?> get simOperatorName =>
_foregroundChannel.invokeMethod<String>(GET_SIM_OPERATOR_NAME);
///
/// Returns a constant indicating the state of the default SIM card.
///
/// Returns:
///
/// [Future<SimState>]
Future<SimState> get simState async {
final int? state =
await _foregroundChannel.invokeMethod<int>(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<bool?> get isNetworkRoaming =>
_foregroundChannel.invokeMethod<bool>(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<List<SignalStrength>>]
Future<List<SignalStrength>> get signalStrengths async {
final List<dynamic>? 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<ServiceState>]
Future<ServiceState> get serviceState async {
final int? state =
await _foregroundChannel.invokeMethod<int>(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<bool?> get requestSmsPermissions =>
_foregroundChannel.invokeMethod<bool>(REQUEST_SMS_PERMISSION);
///
/// Request the user for all the phone permissions listed in the app's AndroidManifest.xml
///
Future<bool?> get requestPhonePermissions =>
_foregroundChannel.invokeMethod<bool>(REQUEST_PHONE_PERMISSION);
///
/// Request the user for all the phone and sms permissions listed in the app's AndroidManifest.xml
///
Future<bool?> get requestPhoneAndSmsPermissions =>
_foregroundChannel.invokeMethod<bool>(REQUEST_PHONE_AND_SMS_PERMISSION);
///
/// Opens the default dialer with the given phone number.
///
Future<void> openDialer(String phoneNumber) async {
assert(phoneNumber.isNotEmpty, "phoneNumber cannot be empty");
final Map<String, dynamic> args = {"phoneNumber": phoneNumber};
await _foregroundChannel.invokeMethod(OPEN_DIALER, args);
}
///
/// Starts a phone all with the given phone number.
///
/// ### Requires permission CALL_PHONE
///
Future<void> dialPhoneNumber(String phoneNumber) async {
assert(phoneNumber.isNotEmpty, "phoneNumber cannot be null or empty");
final Map<String, dynamic> 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<SmsColumn> columns) {
final message = Map.castFrom<dynamic, dynamic, String, dynamic>(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<dynamic, dynamic, String, dynamic>(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;
}
}

@ -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

@ -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<MethodCall> log = <MethodCall>[];
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});
});
});
}

@ -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"
};

@ -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<bool>.value(true));
final permissionGranted = await telephony.requestSmsPermissions;
verify(methodChannel.invokeMethod<bool>(REQUEST_SMS_PERMISSION))
.called(1);
expect(permissionGranted, true);
});
test("phone permissions", () async {
when(methodChannel.invokeMethod(REQUEST_PHONE_PERMISSION))
.thenAnswer((_) => Future<bool>.value(true));
final permissionGranted = await telephony.requestPhonePermissions;
verify(methodChannel.invokeMethod<bool>(REQUEST_PHONE_PERMISSION))
.called(1);
expect(permissionGranted, true);
});
test("phone and sms permissions", () async {
when(methodChannel.invokeMethod(REQUEST_PHONE_AND_SMS_PERMISSION))
.thenAnswer((_) => Future<bool>.value(true));
final permissionGranted = await telephony.requestPhoneAndSmsPermissions;
verify(methodChannel
.invokeMethod<bool>(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<int>.value(1));
final state = await telephony.serviceState;
verify(methodChannel.invokeMethod<int>(GET_SERVICE_STATE)).called(1);
expect(state, ServiceState.OUT_OF_SERVICE);
});
test("signal strengths", () async {
when(methodChannel.invokeMethod(GET_SIGNAL_STRENGTH))
.thenAnswer((_) => Future<List>.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<bool>(IS_NETWORK_ROAMING))
.thenAnswer((_) => Future<bool>.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<int>.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<String>.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<String>.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<int>.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<int>.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<String>.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<String>.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<int>.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<int>.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<int>.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<bool>.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<void>.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<void>.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<void>.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<List<LinkedHashMap>>(
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<List<LinkedHashMap>>(
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<List<LinkedHashMap>>(
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<List<LinkedHashMap>>(
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<List<LinkedHashMap>>(
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<List<LinkedHashMap>>(
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<List<LinkedHashMap>>(
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<List<LinkedHashMap>>(
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);
});
});
});
}

@ -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<T?> invokeMethod<T>(String? method, [dynamic arguments]) =>
(super.noSuchMethod(Invocation.method(#invokeMethod, [method, arguments]),
returnValue: Future<T?>.value()) as _i5.Future<T?>);
@override
_i5.Future<List<T>?> invokeListMethod<T>(String? method,
[dynamic arguments]) =>
(super.noSuchMethod(
Invocation.method(#invokeListMethod, [method, arguments]),
returnValue: Future<List<T>?>.value()) as _i5.Future<List<T>?>);
@override
_i5.Future<Map<K, V>?> invokeMapMethod<K, V>(String? method,
[dynamic arguments]) =>
(super.noSuchMethod(
Invocation.method(#invokeMapMethod, [method, arguments]),
returnValue: Future<Map<K, V>?>.value()) as _i5.Future<Map<K, V>?>);
@override
void setMethodCallHandler(
_i5.Future<dynamic> Function(_i2.MethodCall)? handler) =>
super.noSuchMethod(Invocation.method(#setMethodCallHandler, [handler]),
returnValueForMissingStub: null);
}
Loading…
Cancel
Save