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