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>
|
After Width: | Height: | Size: 544 B |
After Width: | Height: | Size: 442 B |
After Width: | Height: | Size: 721 B |
After Width: | Height: | Size: 1.0 KiB |
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…
Reference in new issue