Merge branch 'dev' into Protocol_00
# Conflicts: # dialer/android/app/src/main/AndroidManifest.xml # dialer/android/app/src/main/kotlin/com/icing/dialer/activities/MainActivity.kt # dialer/android/app/src/main/kotlin/com/icing/dialer/services/MyInCallService.kt # dialer/lib/features/call/call_page.dart # dialer/lib/features/contacts/widgets/contact_modal.dart # dialer/lib/features/history/history_page.dart # dialer/lib/features/home/home_page.dart # dialer/lib/main.dart # dialer/lib/services/call_service.dart
This commit is contained in:
commit
da60b32dc7
@ -1,17 +1,19 @@
|
|||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.icing.dialer">
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.icing.dialer">
|
||||||
<uses-feature android:name="android.hardware.telephony" android:required="true" />
|
<uses-feature android:name="android.hardware.telephony" android:required="true" />
|
||||||
|
|
||||||
<uses-permission android:name="android.permission.READ_CONTACTS" />
|
<uses-permission android:name="android.permission.READ_CONTACTS"/>
|
||||||
<uses-permission android:name="android.permission.WRITE_CONTACTS" />
|
<uses-permission android:name="android.permission.WRITE_CONTACTS"/>
|
||||||
<uses-permission android:name="android.permission.CALL_PHONE" />
|
<uses-permission android:name="android.permission.CALL_PHONE" />
|
||||||
<uses-permission android:name="android.permission.SEND_SMS" />
|
<uses-permission android:name="android.permission.SEND_SMS" />
|
||||||
<uses-permission android:name="android.permission.READ_BLOCKED_NUMBERS" />
|
<uses-permission android:name="android.permission.READ_BLOCKED_NUMBERS" />
|
||||||
<uses-permission android:name="android.permission.WRITE_BLOCKED_NUMBERS" />
|
<uses-permission android:name="android.permission.WRITE_BLOCKED_NUMBERS" />
|
||||||
<uses-permission android:name="android.permission.READ_CALL_LOG" />
|
<uses-permission android:name="android.permission.READ_CALL_LOG"/>
|
||||||
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
|
<uses-permission android:name="android.permission.READ_PHONE_STATE"/>
|
||||||
<uses-permission android:name="android.permission.MANAGE_OWN_CALLS" />
|
<uses-permission android:name="android.permission.MANAGE_OWN_CALLS" />
|
||||||
<uses-permission android:name="android.permission.ANSWER_PHONE_CALLS" />
|
<uses-permission android:name="android.permission.ANSWER_PHONE_CALLS" />
|
||||||
<uses-permission android:name="android.permission.PROCESS_OUTGOING_CALLS" />
|
<uses-permission android:name="android.permission.PROCESS_OUTGOING_CALLS" />
|
||||||
|
<uses-permission android:name="android.permission.POST_NOTIFICATIONS" />
|
||||||
|
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
|
||||||
|
|
||||||
<application
|
<application
|
||||||
android:label="Icing Dialer"
|
android:label="Icing Dialer"
|
||||||
@ -27,12 +29,17 @@
|
|||||||
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
|
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
|
||||||
android:hardwareAccelerated="true"
|
android:hardwareAccelerated="true"
|
||||||
android:windowSoftInputMode="adjustResize">
|
android:windowSoftInputMode="adjustResize">
|
||||||
|
<!-- 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
|
<meta-data
|
||||||
android:name="io.flutter.embedding.android.NormalTheme"
|
android:name="io.flutter.embedding.android.NormalTheme"
|
||||||
android:resource="@style/NormalTheme" />
|
android:resource="@style/NormalTheme"
|
||||||
|
/>
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN" />
|
<action android:name="android.intent.action.MAIN"/>
|
||||||
<category android:name="android.intent.category.LAUNCHER" />
|
<category android:name="android.intent.category.LAUNCHER"/>
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
<!-- Dialer intent filters (required for default dialer eligibility) -->
|
<!-- Dialer intent filters (required for default dialer eligibility) -->
|
||||||
<intent-filter>
|
<intent-filter>
|
||||||
@ -75,17 +82,22 @@
|
|||||||
<action android:name="android.telecom.ConnectionService" />
|
<action android:name="android.telecom.ConnectionService" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</service> -->
|
</service> -->
|
||||||
|
|
||||||
<!-- Don't delete the meta-data below.
|
<!-- Don't delete the meta-data below.
|
||||||
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
|
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
|
||||||
<meta-data
|
<meta-data
|
||||||
android:name="flutterEmbedding"
|
android:name="flutterEmbedding"
|
||||||
android:value="2" />
|
android:value="2" />
|
||||||
</application>
|
</application>
|
||||||
|
<!-- Required to query activities that can process text, see:
|
||||||
|
https://developer.android.com/training/package-visibility and
|
||||||
|
https://developer.android.com/reference/android/content/Intent#ACTION_PROCESS_TEXT.
|
||||||
|
|
||||||
|
In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. -->
|
||||||
<queries>
|
<queries>
|
||||||
<intent>
|
<intent>
|
||||||
<action android:name="android.intent.action.PROCESS_TEXT" />
|
<action android:name="android.intent.action.PROCESS_TEXT"/>
|
||||||
<data android:mimeType="text/plain" />
|
<data android:mimeType="text/plain"/>
|
||||||
</intent>
|
</intent>
|
||||||
</queries>
|
</queries>
|
||||||
</manifest>
|
</manifest>
|
||||||
|
@ -6,7 +6,6 @@ import android.content.Context
|
|||||||
import android.content.Intent
|
import android.content.Intent
|
||||||
import android.content.pm.PackageManager
|
import android.content.pm.PackageManager
|
||||||
import android.database.Cursor
|
import android.database.Cursor
|
||||||
import android.net.Uri
|
|
||||||
import android.os.Build
|
import android.os.Build
|
||||||
import android.os.Bundle
|
import android.os.Bundle
|
||||||
import android.provider.CallLog
|
import android.provider.CallLog
|
||||||
@ -26,24 +25,76 @@ class MainActivity : FlutterActivity() {
|
|||||||
private val CALL_CHANNEL = "call_service"
|
private val CALL_CHANNEL = "call_service"
|
||||||
private val TAG = "MainActivity"
|
private val TAG = "MainActivity"
|
||||||
private val REQUEST_CODE_SET_DEFAULT_DIALER = 1001
|
private val REQUEST_CODE_SET_DEFAULT_DIALER = 1001
|
||||||
|
private val REQUEST_CODE_CALL_LOG_PERMISSION = 1002
|
||||||
|
private var pendingIncomingCall: Pair<String?, Boolean>? = null
|
||||||
|
private var wasPhoneLocked: Boolean = false
|
||||||
|
|
||||||
override fun onCreate(savedInstanceState: Bundle?) {
|
override fun onCreate(savedInstanceState: Bundle?) {
|
||||||
super.onCreate(savedInstanceState)
|
super.onCreate(savedInstanceState)
|
||||||
Log.d(TAG, "onCreate started")
|
Log.d(TAG, "onCreate started")
|
||||||
Log.d(TAG, "Waiting for Flutter to signal permissions")
|
wasPhoneLocked = intent.getBooleanExtra("wasPhoneLocked", false)
|
||||||
|
Log.d(TAG, "Was phone locked at start: $wasPhoneLocked")
|
||||||
|
updateLockScreenFlags(intent)
|
||||||
|
handleIncomingCallIntent(intent)
|
||||||
|
}
|
||||||
|
|
||||||
|
override fun onNewIntent(intent: Intent) {
|
||||||
|
super.onNewIntent(intent)
|
||||||
|
setIntent(intent)
|
||||||
|
wasPhoneLocked = intent.getBooleanExtra("wasPhoneLocked", false)
|
||||||
|
Log.d(TAG, "onNewIntent, wasPhoneLocked: $wasPhoneLocked")
|
||||||
|
updateLockScreenFlags(intent)
|
||||||
|
handleIncomingCallIntent(intent)
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun updateLockScreenFlags(intent: Intent?) {
|
||||||
|
val isIncomingCall = intent?.getBooleanExtra("isIncomingCall", false) ?: false
|
||||||
|
if (isIncomingCall && wasPhoneLocked) {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
|
||||||
|
setShowWhenLocked(true)
|
||||||
|
setTurnScreenOn(true)
|
||||||
|
} else {
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
window.addFlags(
|
||||||
|
android.view.WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED or
|
||||||
|
android.view.WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Log.d(TAG, "Enabled showWhenLocked and turnScreenOn for incoming call")
|
||||||
|
} else {
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
|
||||||
|
setShowWhenLocked(false)
|
||||||
|
setTurnScreenOn(false)
|
||||||
|
} else {
|
||||||
|
@Suppress("DEPRECATION")
|
||||||
|
window.clearFlags(
|
||||||
|
android.view.WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED or
|
||||||
|
android.view.WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON
|
||||||
|
)
|
||||||
|
}
|
||||||
|
Log.d(TAG, "Disabled showWhenLocked and turnScreenOn for normal usage")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
|
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
|
||||||
super.configureFlutterEngine(flutterEngine)
|
super.configureFlutterEngine(flutterEngine)
|
||||||
Log.d(TAG, "Configuring Flutter engine")
|
Log.d(TAG, "Configuring Flutter engine")
|
||||||
|
|
||||||
MyInCallService.channel = MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CALL_CHANNEL)
|
MyInCallService.channel = MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CALL_CHANNEL)
|
||||||
|
|
||||||
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CALL_CHANNEL)
|
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CALL_CHANNEL)
|
||||||
.setMethodCallHandler { call, result ->
|
.setMethodCallHandler { call, result ->
|
||||||
when (call.method) {
|
when (call.method) {
|
||||||
"permissionsGranted" -> {
|
"permissionsGranted" -> {
|
||||||
Log.d(TAG, "Received permissionsGranted from Flutter")
|
Log.d(TAG, "Received permissionsGranted from Flutter")
|
||||||
checkAndRequestDefaultDialer()
|
pendingIncomingCall?.let { (phoneNumber, showScreen) ->
|
||||||
|
if (showScreen) {
|
||||||
|
MyInCallService.channel?.invokeMethod("incomingCallFromNotification", mapOf(
|
||||||
|
"phoneNumber" to phoneNumber,
|
||||||
|
"wasPhoneLocked" to wasPhoneLocked
|
||||||
|
))
|
||||||
|
pendingIncomingCall = null
|
||||||
|
}
|
||||||
|
}
|
||||||
result.success(true)
|
result.success(true)
|
||||||
}
|
}
|
||||||
"makeGsmCall" -> {
|
"makeGsmCall" -> {
|
||||||
@ -60,43 +111,144 @@ class MainActivity : FlutterActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
"hangUpCall" -> {
|
"hangUpCall" -> {
|
||||||
val success = CallService.hangUpCall(this)
|
val success = MyInCallService.currentCall?.let {
|
||||||
|
it.disconnect()
|
||||||
|
Log.d(TAG, "Call disconnected")
|
||||||
|
MyInCallService.channel?.invokeMethod("callEnded", mapOf(
|
||||||
|
"callId" to it.details.handle.toString(),
|
||||||
|
"wasPhoneLocked" to wasPhoneLocked
|
||||||
|
))
|
||||||
|
true
|
||||||
|
} ?: false
|
||||||
if (success) {
|
if (success) {
|
||||||
result.success(mapOf("status" to "ended"))
|
result.success(mapOf("status" to "ended"))
|
||||||
|
if (wasPhoneLocked) {
|
||||||
|
Log.d(TAG, "Finishing and removing task after hangup, phone was locked")
|
||||||
|
finishAndRemoveTask()
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
result.error("HANGUP_FAILED", "Failed to end call", null)
|
Log.w(TAG, "No active call to hang up")
|
||||||
|
result.error("HANGUP_FAILED", "No active call to hang up", null)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
"answerCall" -> {
|
"answerCall" -> {
|
||||||
val success = MyInCallService.currentCall?.let {
|
val success = MyInCallService.currentCall?.let {
|
||||||
it.answer(0) // 0 for default video state (audio-only)
|
it.answer(0)
|
||||||
Log.d(TAG, "Answered call")
|
Log.d(TAG, "Answered call")
|
||||||
true
|
true
|
||||||
} ?: false
|
} ?: false
|
||||||
if (success) {
|
if (success) {
|
||||||
result.success(mapOf("status" to "answered"))
|
result.success(mapOf("status" to "answered"))
|
||||||
} else {
|
} else {
|
||||||
|
Log.w(TAG, "No active call to answer")
|
||||||
result.error("ANSWER_FAILED", "No active call to answer", null)
|
result.error("ANSWER_FAILED", "No active call to answer", null)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
"callEndedFromFlutter" -> {
|
||||||
|
Log.d(TAG, "Call ended from Flutter, wasPhoneLocked: $wasPhoneLocked")
|
||||||
|
if (wasPhoneLocked) {
|
||||||
|
finishAndRemoveTask()
|
||||||
|
Log.d(TAG, "Finishing and removing task after call ended, phone was locked")
|
||||||
|
}
|
||||||
|
result.success(true)
|
||||||
|
}
|
||||||
|
"getCallState" -> {
|
||||||
|
val stateStr = when (MyInCallService.currentCall?.state) {
|
||||||
|
android.telecom.Call.STATE_ACTIVE -> "active"
|
||||||
|
android.telecom.Call.STATE_RINGING -> "ringing"
|
||||||
|
android.telecom.Call.STATE_DIALING -> "dialing"
|
||||||
|
android.telecom.Call.STATE_DISCONNECTED -> "disconnected"
|
||||||
|
android.telecom.Call.STATE_DISCONNECTING -> "disconnecting"
|
||||||
|
else -> "unknown"
|
||||||
|
}
|
||||||
|
Log.d(TAG, "getCallState called, returning: $stateStr")
|
||||||
|
result.success(stateStr)
|
||||||
|
}
|
||||||
|
"muteCall" -> {
|
||||||
|
val mute = call.argument<Boolean>("mute") ?: false
|
||||||
|
val success = MyInCallService.currentCall?.let {
|
||||||
|
MyInCallService.toggleMute(mute)
|
||||||
|
} ?: false
|
||||||
|
if (success) {
|
||||||
|
Log.d(TAG, "Mute call set to $mute")
|
||||||
|
result.success(mapOf("status" to "success"))
|
||||||
|
} else {
|
||||||
|
Log.w(TAG, "No active call or failed to mute")
|
||||||
|
result.error("MUTE_FAILED", "No active call or failed to mute", null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"speakerCall" -> {
|
||||||
|
val speaker = call.argument<Boolean>("speaker") ?: false
|
||||||
|
val success = MyInCallService.currentCall?.let {
|
||||||
|
MyInCallService.toggleSpeaker(speaker)
|
||||||
|
} ?: false
|
||||||
|
if (success) {
|
||||||
|
Log.d(TAG, "Speaker call set to $speaker")
|
||||||
|
result.success(mapOf("status" to "success"))
|
||||||
|
} else {
|
||||||
|
Log.w(TAG, "No active call or failed to set speaker")
|
||||||
|
result.error("SPEAKER_FAILED", "No active call or failed to set speaker", null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"isDefaultDialer" -> {
|
||||||
|
val isDefault = isDefaultDialer()
|
||||||
|
Log.d(TAG, "isDefaultDialer called, returning: $isDefault")
|
||||||
|
result.success(isDefault)
|
||||||
|
}
|
||||||
|
"requestDefaultDialer" -> {
|
||||||
|
checkAndRequestDefaultDialer()
|
||||||
|
result.success(true)
|
||||||
|
}
|
||||||
|
"sendDtmfTone" -> {
|
||||||
|
val digit = call.argument<String>("digit")
|
||||||
|
if (digit != null) {
|
||||||
|
val success = MyInCallService.sendDtmfTone(digit)
|
||||||
|
result.success(success)
|
||||||
|
} else {
|
||||||
|
result.error("INVALID_ARGUMENT", "Digit is null", null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"isDefaultDialer" -> {
|
||||||
|
val isDefault = isDefaultDialer()
|
||||||
|
Log.d(TAG, "isDefaultDialer called, returning: $isDefault")
|
||||||
|
result.success(isDefault)
|
||||||
|
}
|
||||||
|
"requestDefaultDialer" -> {
|
||||||
|
checkAndRequestDefaultDialer()
|
||||||
|
result.success(true)
|
||||||
|
}
|
||||||
else -> result.notImplemented()
|
else -> result.notImplemented()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, KEYSTORE_CHANNEL)
|
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, KEYSTORE_CHANNEL)
|
||||||
.setMethodCallHandler { call, result -> KeystoreHelper(call, result).handleMethodCall() }
|
.setMethodCallHandler { call, result ->
|
||||||
|
KeystoreHelper(call, result).handleMethodCall()
|
||||||
|
}
|
||||||
|
|
||||||
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CALLLOG_CHANNEL)
|
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CALLLOG_CHANNEL)
|
||||||
.setMethodCallHandler { call, result ->
|
.setMethodCallHandler { call, result ->
|
||||||
if (call.method == "getCallLogs") {
|
if (call.method == "getCallLogs") {
|
||||||
val callLogs = getCallLogs()
|
if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_CALL_LOG) == PackageManager.PERMISSION_GRANTED) {
|
||||||
result.success(callLogs)
|
val callLogs = getCallLogs()
|
||||||
|
result.success(callLogs)
|
||||||
|
} else {
|
||||||
|
requestPermissions(arrayOf(Manifest.permission.READ_CALL_LOG), REQUEST_CODE_CALL_LOG_PERMISSION)
|
||||||
|
result.error("PERMISSION_DENIED", "Call log permission not granted", null)
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
result.notImplemented()
|
result.notImplemented()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun isDefaultDialer(): Boolean {
|
||||||
|
val telecomManager = getSystemService(TELECOM_SERVICE) as TelecomManager
|
||||||
|
val currentDefault = telecomManager.defaultDialerPackage
|
||||||
|
Log.d(TAG, "Checking default dialer: current=$currentDefault, myPackage=$packageName")
|
||||||
|
return currentDefault == packageName
|
||||||
|
}
|
||||||
|
|
||||||
private fun checkAndRequestDefaultDialer() {
|
private fun checkAndRequestDefaultDialer() {
|
||||||
val telecomManager = getSystemService(TELECOM_SERVICE) as TelecomManager
|
val telecomManager = getSystemService(TELECOM_SERVICE) as TelecomManager
|
||||||
val currentDefault = telecomManager.defaultDialerPackage
|
val currentDefault = telecomManager.defaultDialerPackage
|
||||||
@ -109,8 +261,6 @@ class MainActivity : FlutterActivity() {
|
|||||||
val intent = roleManager.createRequestRoleIntent(RoleManager.ROLE_DIALER)
|
val intent = roleManager.createRequestRoleIntent(RoleManager.ROLE_DIALER)
|
||||||
startActivityForResult(intent, REQUEST_CODE_SET_DEFAULT_DIALER)
|
startActivityForResult(intent, REQUEST_CODE_SET_DEFAULT_DIALER)
|
||||||
Log.d(TAG, "Launched RoleManager intent for default dialer on API 29+")
|
Log.d(TAG, "Launched RoleManager intent for default dialer on API 29+")
|
||||||
} else {
|
|
||||||
Log.d(TAG, "RoleManager: Available=${roleManager.isRoleAvailable(RoleManager.ROLE_DIALER)}, Held=${roleManager.isRoleHeld(RoleManager.ROLE_DIALER)}")
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
val intent = Intent(TelecomManager.ACTION_CHANGE_DEFAULT_DIALER)
|
val intent = Intent(TelecomManager.ACTION_CHANGE_DEFAULT_DIALER)
|
||||||
@ -148,6 +298,18 @@ class MainActivity : FlutterActivity() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<out String>, grantResults: IntArray) {
|
||||||
|
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
|
||||||
|
if (requestCode == REQUEST_CODE_CALL_LOG_PERMISSION) {
|
||||||
|
if (grantResults.isNotEmpty() && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
|
||||||
|
Log.d(TAG, "Call log permission granted")
|
||||||
|
MyInCallService.channel?.invokeMethod("callLogPermissionGranted", null)
|
||||||
|
} else {
|
||||||
|
Log.w(TAG, "Call log permission denied")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private fun getCallLogs(): List<Map<String, Any?>> {
|
private fun getCallLogs(): List<Map<String, Any?>> {
|
||||||
val logsList = mutableListOf<Map<String, Any?>>()
|
val logsList = mutableListOf<Map<String, Any?>>()
|
||||||
val cursor: Cursor? = contentResolver.query(
|
val cursor: Cursor? = contentResolver.query(
|
||||||
@ -171,4 +333,25 @@ class MainActivity : FlutterActivity() {
|
|||||||
}
|
}
|
||||||
return logsList
|
return logsList
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private fun handleIncomingCallIntent(intent: Intent?) {
|
||||||
|
intent?.let {
|
||||||
|
if (it.getBooleanExtra("isIncomingCall", false)) {
|
||||||
|
val phoneNumber = it.getStringExtra("phoneNumber")
|
||||||
|
val showScreen = it.getBooleanExtra("showIncomingCallScreen", false)
|
||||||
|
Log.d(TAG, "Received incoming call intent for $phoneNumber, showScreen=$showScreen, wasPhoneLocked=$wasPhoneLocked")
|
||||||
|
if (showScreen) {
|
||||||
|
if (MyInCallService.channel != null) {
|
||||||
|
MyInCallService.channel?.invokeMethod("incomingCallFromNotification", mapOf(
|
||||||
|
"phoneNumber" to phoneNumber,
|
||||||
|
"wasPhoneLocked" to wasPhoneLocked
|
||||||
|
))
|
||||||
|
} else {
|
||||||
|
pendingIncomingCall = Pair(phoneNumber, true)
|
||||||
|
Log.d(TAG, "Flutter channel not ready, storing pending call: $phoneNumber")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
@ -1,8 +1,19 @@
|
|||||||
package com.icing.dialer.services
|
package com.icing.dialer.services
|
||||||
|
|
||||||
|
import android.app.KeyguardManager
|
||||||
|
import android.app.NotificationChannel
|
||||||
|
import android.app.NotificationManager
|
||||||
|
import android.app.PendingIntent
|
||||||
|
import android.content.Context
|
||||||
|
import android.content.Intent
|
||||||
|
import android.media.AudioManager
|
||||||
|
import android.os.Build
|
||||||
import android.telecom.Call
|
import android.telecom.Call
|
||||||
import android.telecom.InCallService
|
import android.telecom.InCallService
|
||||||
|
import android.telecom.CallAudioState
|
||||||
import android.util.Log
|
import android.util.Log
|
||||||
|
import androidx.core.app.NotificationCompat
|
||||||
|
import com.icing.dialer.activities.MainActivity
|
||||||
import io.flutter.plugin.common.MethodChannel
|
import io.flutter.plugin.common.MethodChannel
|
||||||
|
|
||||||
class MyInCallService : InCallService() {
|
class MyInCallService : InCallService() {
|
||||||
@ -10,6 +21,53 @@ class MyInCallService : InCallService() {
|
|||||||
var channel: MethodChannel? = null
|
var channel: MethodChannel? = null
|
||||||
var currentCall: Call? = null
|
var currentCall: Call? = null
|
||||||
private const val TAG = "MyInCallService"
|
private const val TAG = "MyInCallService"
|
||||||
|
private const val NOTIFICATION_CHANNEL_ID = "incoming_call_channel"
|
||||||
|
private const val NOTIFICATION_ID = 1
|
||||||
|
var wasPhoneLocked: Boolean = false
|
||||||
|
private var instance: MyInCallService? = null
|
||||||
|
|
||||||
|
fun toggleMute(mute: Boolean): Boolean {
|
||||||
|
return instance?.let { service ->
|
||||||
|
try {
|
||||||
|
service.setMuted(mute)
|
||||||
|
Log.d(TAG, "Requested to set call mute state to $mute")
|
||||||
|
true
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Failed to set mute state: $e")
|
||||||
|
false
|
||||||
|
}
|
||||||
|
} ?: false
|
||||||
|
}
|
||||||
|
|
||||||
|
fun toggleSpeaker(speaker: Boolean): Boolean {
|
||||||
|
return instance?.let { service ->
|
||||||
|
try {
|
||||||
|
val route = if (speaker) CallAudioState.ROUTE_SPEAKER else CallAudioState.ROUTE_EARPIECE
|
||||||
|
service.setAudioRoute(route)
|
||||||
|
Log.d(TAG, "Requested to set audio route to $route")
|
||||||
|
true
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Failed to set audio route: $e")
|
||||||
|
false
|
||||||
|
}
|
||||||
|
} ?: false
|
||||||
|
}
|
||||||
|
|
||||||
|
fun sendDtmfTone(digit: String): Boolean {
|
||||||
|
return instance?.let { service ->
|
||||||
|
try {
|
||||||
|
currentCall?.let { call ->
|
||||||
|
call.playDtmfTone(digit[0])
|
||||||
|
call.stopDtmfTone()
|
||||||
|
Log.d(TAG, "Sent DTMF tone: $digit")
|
||||||
|
true
|
||||||
|
} ?: false
|
||||||
|
} catch (e: Exception) {
|
||||||
|
Log.e(TAG, "Failed to send DTMF tone: $e")
|
||||||
|
false
|
||||||
|
}
|
||||||
|
} ?: false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private val callCallback = object : Call.Callback() {
|
private val callCallback = object : Call.Callback() {
|
||||||
@ -26,44 +84,135 @@ class MyInCallService : InCallService() {
|
|||||||
Log.d(TAG, "State changed: $stateStr for call ${call.details.handle}")
|
Log.d(TAG, "State changed: $stateStr for call ${call.details.handle}")
|
||||||
channel?.invokeMethod("callStateChanged", mapOf(
|
channel?.invokeMethod("callStateChanged", mapOf(
|
||||||
"callId" to call.details.handle.toString(),
|
"callId" to call.details.handle.toString(),
|
||||||
"state" to stateStr
|
"state" to stateStr,
|
||||||
|
"wasPhoneLocked" to wasPhoneLocked
|
||||||
))
|
))
|
||||||
if (state == Call.STATE_DISCONNECTED || state == Call.STATE_DISCONNECTING) {
|
if (state == Call.STATE_RINGING) {
|
||||||
Log.d(TAG, "Call ended: ${call.details.handle}")
|
val keyguardManager = getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager
|
||||||
channel?.invokeMethod("callEnded", mapOf("callId" to call.details.handle.toString()))
|
wasPhoneLocked = keyguardManager.isKeyguardLocked
|
||||||
|
Log.d(TAG, "Phone locked at ringing: $wasPhoneLocked")
|
||||||
|
showIncomingCallScreen(call.details.handle.toString().replace("tel:", ""))
|
||||||
|
} else if (state == Call.STATE_DISCONNECTED || state == Call.STATE_DISCONNECTING) {
|
||||||
|
Log.d(TAG, "Call ended: ${call.details.handle}, wasPhoneLocked: $wasPhoneLocked")
|
||||||
|
channel?.invokeMethod("callEnded", mapOf(
|
||||||
|
"callId" to call.details.handle.toString(),
|
||||||
|
"wasPhoneLocked" to wasPhoneLocked
|
||||||
|
))
|
||||||
currentCall = null
|
currentCall = null
|
||||||
|
cancelNotification()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCallAdded(call: Call) {
|
override fun onCallAdded(call: Call) {
|
||||||
super.onCallAdded(call)
|
super.onCallAdded(call)
|
||||||
|
instance = this
|
||||||
currentCall = call
|
currentCall = call
|
||||||
val stateStr = when (call.state) {
|
val stateStr = when (call.state) {
|
||||||
Call.STATE_DIALING -> "dialing"
|
Call.STATE_DIALING -> "dialing"
|
||||||
Call.STATE_ACTIVE -> "active"
|
Call.STATE_ACTIVE -> "active"
|
||||||
Call.STATE_RINGING -> "ringing"
|
Call.STATE_RINGING -> "ringing"
|
||||||
else -> "dialing" // Default for outgoing
|
else -> "dialing"
|
||||||
}
|
}
|
||||||
Log.d(TAG, "Call added: ${call.details.handle}, state: $stateStr")
|
Log.d(TAG, "Call added: ${call.details.handle}, state: $stateStr")
|
||||||
channel?.invokeMethod("callAdded", mapOf(
|
channel?.invokeMethod("callAdded", mapOf(
|
||||||
"callId" to call.details.handle.toString(),
|
"callId" to call.details.handle.toString(),
|
||||||
"state" to stateStr
|
"state" to stateStr
|
||||||
))
|
))
|
||||||
|
if (stateStr == "ringing") {
|
||||||
|
val keyguardManager = getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager
|
||||||
|
wasPhoneLocked = keyguardManager.isKeyguardLocked
|
||||||
|
Log.d(TAG, "Phone locked at call added: $wasPhoneLocked")
|
||||||
|
showIncomingCallScreen(call.details.handle.toString().replace("tel:", ""))
|
||||||
|
}
|
||||||
call.registerCallback(callCallback)
|
call.registerCallback(callCallback)
|
||||||
|
if (callAudioState != null) {
|
||||||
|
val audioState = callAudioState
|
||||||
|
channel?.invokeMethod("audioStateChanged", mapOf(
|
||||||
|
"route" to audioState.route,
|
||||||
|
"muted" to audioState.isMuted,
|
||||||
|
"speaker" to (audioState.route == CallAudioState.ROUTE_SPEAKER)
|
||||||
|
))
|
||||||
|
} else {
|
||||||
|
Log.w("MyInCallService", "callAudioState is null in onCallAdded")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCallRemoved(call: Call) {
|
override fun onCallRemoved(call: Call) {
|
||||||
super.onCallRemoved(call)
|
super.onCallRemoved(call)
|
||||||
Log.d(TAG, "Call removed: ${call.details.handle}")
|
Log.d(TAG, "Call removed: ${call.details.handle}, wasPhoneLocked: $wasPhoneLocked")
|
||||||
call.unregisterCallback(callCallback)
|
call.unregisterCallback(callCallback)
|
||||||
channel?.invokeMethod("callRemoved", mapOf("callId" to call.details.handle.toString()))
|
channel?.invokeMethod("callRemoved", mapOf(
|
||||||
|
"callId" to call.details.handle.toString(),
|
||||||
|
"wasPhoneLocked" to wasPhoneLocked
|
||||||
|
))
|
||||||
currentCall = null
|
currentCall = null
|
||||||
|
instance = null
|
||||||
|
cancelNotification()
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCallAudioStateChanged(state: android.telecom.CallAudioState) {
|
override fun onCallAudioStateChanged(state: CallAudioState) {
|
||||||
super.onCallAudioStateChanged(state)
|
super.onCallAudioStateChanged(state)
|
||||||
Log.d(TAG, "Audio state changed: route=${state.route}")
|
Log.d(TAG, "Audio state changed: route=${state.route}, muted=${state.isMuted}")
|
||||||
channel?.invokeMethod("audioStateChanged", mapOf("route" to state.route))
|
channel?.invokeMethod("audioStateChanged", mapOf(
|
||||||
|
"route" to state.route,
|
||||||
|
"muted" to state.isMuted,
|
||||||
|
"speaker" to (state.route == CallAudioState.ROUTE_SPEAKER)
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun showIncomingCallScreen(phoneNumber: String) {
|
||||||
|
val intent = Intent(this, MainActivity::class.java).apply {
|
||||||
|
flags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TOP or Intent.FLAG_ACTIVITY_SINGLE_TOP
|
||||||
|
putExtra("phoneNumber", phoneNumber)
|
||||||
|
putExtra("isIncomingCall", true)
|
||||||
|
putExtra("showIncomingCallScreen", true)
|
||||||
|
putExtra("wasPhoneLocked", wasPhoneLocked)
|
||||||
|
}
|
||||||
|
|
||||||
|
val keyguardManager = getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager
|
||||||
|
if (keyguardManager.isKeyguardLocked) {
|
||||||
|
startActivity(intent)
|
||||||
|
Log.d(TAG, "Launched MainActivity directly for locked screen, phoneNumber: $phoneNumber")
|
||||||
|
} else {
|
||||||
|
val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||||
|
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
|
||||||
|
val channel = NotificationChannel(
|
||||||
|
NOTIFICATION_CHANNEL_ID,
|
||||||
|
"Incoming Calls",
|
||||||
|
NotificationManager.IMPORTANCE_HIGH
|
||||||
|
).apply {
|
||||||
|
description = "Notifications for incoming calls"
|
||||||
|
enableVibration(true)
|
||||||
|
setShowBadge(true)
|
||||||
|
}
|
||||||
|
notificationManager.createNotificationChannel(channel)
|
||||||
|
}
|
||||||
|
|
||||||
|
val pendingIntent = PendingIntent.getActivity(
|
||||||
|
this, 0, intent,
|
||||||
|
PendingIntent.FLAG_UPDATE_CURRENT or (if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) PendingIntent.FLAG_IMMUTABLE else 0)
|
||||||
|
)
|
||||||
|
|
||||||
|
val notification = NotificationCompat.Builder(this, NOTIFICATION_CHANNEL_ID)
|
||||||
|
.setSmallIcon(android.R.drawable.ic_dialog_alert)
|
||||||
|
.setContentTitle("Incoming Call")
|
||||||
|
.setContentText("Call from $phoneNumber")
|
||||||
|
.setPriority(NotificationCompat.PRIORITY_HIGH)
|
||||||
|
.setFullScreenIntent(pendingIntent, true)
|
||||||
|
.setAutoCancel(true)
|
||||||
|
.setOngoing(true)
|
||||||
|
.build()
|
||||||
|
|
||||||
|
startActivity(intent)
|
||||||
|
notificationManager.notify(NOTIFICATION_ID, notification)
|
||||||
|
Log.d(TAG, "Launched MainActivity with notification for unlocked screen, phoneNumber: $phoneNumber")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private fun cancelNotification() {
|
||||||
|
val notificationManager = getSystemService(Context.NOTIFICATION_SERVICE) as NotificationManager
|
||||||
|
notificationManager.cancel(NOTIFICATION_ID)
|
||||||
|
Log.d(TAG, "Notification canceled")
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,8 +1,10 @@
|
|||||||
import 'dart:typed_data';
|
import 'dart:async';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_contacts/flutter_contacts.dart';
|
||||||
import 'package:dialer/services/call_service.dart';
|
import 'package:dialer/services/call_service.dart';
|
||||||
import 'package:dialer/services/obfuscate_service.dart';
|
import 'package:dialer/services/obfuscate_service.dart';
|
||||||
import 'package:dialer/widgets/username_color_generator.dart';
|
import 'package:dialer/widgets/username_color_generator.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
|
||||||
class CallPage extends StatefulWidget {
|
class CallPage extends StatefulWidget {
|
||||||
final String displayName;
|
final String displayName;
|
||||||
@ -24,27 +26,174 @@ class _CallPageState extends State<CallPage> {
|
|||||||
final ObfuscateService _obfuscateService = ObfuscateService();
|
final ObfuscateService _obfuscateService = ObfuscateService();
|
||||||
final CallService _callService = CallService();
|
final CallService _callService = CallService();
|
||||||
bool isMuted = false;
|
bool isMuted = false;
|
||||||
bool isSpeakerOn = false;
|
bool isSpeaker = false;
|
||||||
bool isKeypadVisible = false;
|
bool isKeypadVisible = false;
|
||||||
bool icingProtocolOk = true;
|
bool icingProtocolOk = true;
|
||||||
String _typedDigits = "";
|
String _typedDigits = "";
|
||||||
|
Timer? _callTimer;
|
||||||
|
int _callSeconds = 0;
|
||||||
|
String _callStatus = "Calling...";
|
||||||
|
StreamSubscription<String>? _callStateSubscription;
|
||||||
|
StreamSubscription<Map<String, dynamic>>? _audioStateSubscription;
|
||||||
|
|
||||||
void _addDigit(String digit) {
|
bool get isNumberUnknown => widget.displayName == widget.phoneNumber;
|
||||||
|
|
||||||
|
@override
|
||||||
|
void initState() {
|
||||||
|
super.initState();
|
||||||
|
_checkInitialCallState();
|
||||||
|
_listenToCallState();
|
||||||
|
_listenToAudioState();
|
||||||
|
_setInitialAudioState();
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
void dispose() {
|
||||||
|
_callTimer?.cancel();
|
||||||
|
_callStateSubscription?.cancel();
|
||||||
|
_audioStateSubscription?.cancel();
|
||||||
|
super.dispose();
|
||||||
|
}
|
||||||
|
|
||||||
|
void _setInitialAudioState() {
|
||||||
|
final initialAudioState = _callService.currentAudioState;
|
||||||
|
if (initialAudioState != null) {
|
||||||
|
setState(() {
|
||||||
|
isMuted = initialAudioState['muted'] ?? false;
|
||||||
|
isSpeaker = initialAudioState['speaker'] ?? false;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _checkInitialCallState() async {
|
||||||
|
try {
|
||||||
|
final state = await _callService.getCallState();
|
||||||
|
print('CallPage: Initial call state: $state');
|
||||||
|
if (mounted && state == "active") {
|
||||||
|
setState(() {
|
||||||
|
_callStatus = "00:00";
|
||||||
|
_startCallTimer();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
print('CallPage: Error checking initial state: $e');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _listenToCallState() {
|
||||||
|
_callStateSubscription = _callService.callStateStream.listen((state) {
|
||||||
|
print('CallPage: Call state changed to $state');
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
if (state == "active") {
|
||||||
|
_callStatus = "00:00";
|
||||||
|
_startCallTimer();
|
||||||
|
} else if (state == "disconnected" || state == "disconnecting") {
|
||||||
|
_callTimer?.cancel();
|
||||||
|
_callStatus = "Call Ended";
|
||||||
|
} else {
|
||||||
|
_callStatus = "Calling...";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _listenToAudioState() {
|
||||||
|
_audioStateSubscription = _callService.audioStateStream.listen((state) {
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
isMuted = state['muted'] ?? isMuted;
|
||||||
|
isSpeaker = state['speaker'] ?? isSpeaker;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _startCallTimer() {
|
||||||
|
_callTimer?.cancel();
|
||||||
|
_callTimer = Timer.periodic(const Duration(seconds: 1), (timer) {
|
||||||
|
if (mounted) {
|
||||||
|
setState(() {
|
||||||
|
_callSeconds++;
|
||||||
|
final minutes = (_callSeconds ~/ 60).toString().padLeft(2, '0');
|
||||||
|
final seconds = (_callSeconds % 60).toString().padLeft(2, '0');
|
||||||
|
_callStatus = '$minutes:$seconds';
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void _addDigit(String digit) async {
|
||||||
|
print('CallPage: Tapped digit: $digit');
|
||||||
setState(() {
|
setState(() {
|
||||||
_typedDigits += digit;
|
_typedDigits += digit;
|
||||||
});
|
});
|
||||||
|
// Send DTMF tone
|
||||||
|
const channel = MethodChannel('call_service');
|
||||||
|
try {
|
||||||
|
final success =
|
||||||
|
await channel.invokeMethod<bool>('sendDtmfTone', {'digit': digit});
|
||||||
|
if (success != true) {
|
||||||
|
print('CallPage: Failed to send DTMF tone for $digit');
|
||||||
|
if (mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text('Failed to send DTMF tone')),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
print('CallPage: Error sending DTMF tone: $e');
|
||||||
|
if (mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text('Error sending DTMF tone: $e')),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _toggleMute() {
|
void _toggleMute() async {
|
||||||
setState(() {
|
try {
|
||||||
isMuted = !isMuted;
|
print('CallPage: Toggling mute, current state: $isMuted');
|
||||||
});
|
final result = await _callService.muteCall(context, mute: !isMuted);
|
||||||
|
print('CallPage: Mute call result: $result');
|
||||||
|
if (mounted && result['status'] != 'success') {
|
||||||
|
print('CallPage: Failed to toggle mute: ${result['message']}');
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text('Failed to toggle mute: ${result['message']}')),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
print('CallPage: Error toggling mute: $e');
|
||||||
|
if (mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text('Error toggling mute: $e')),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _toggleSpeaker() {
|
Future<void> _toggleSpeaker() async {
|
||||||
setState(() {
|
try {
|
||||||
isSpeakerOn = !isSpeakerOn;
|
print('CallPage: Toggling speaker, current state: $isSpeaker');
|
||||||
});
|
final result =
|
||||||
|
await _callService.speakerCall(context, speaker: !isSpeaker);
|
||||||
|
print('CallPage: Speaker call result: $result');
|
||||||
|
if (result['status'] != 'success') {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(
|
||||||
|
content: Text('Failed to toggle speaker: ${result['message']}')),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} catch (e) {
|
||||||
|
print('CallPage: Error toggling speaker: $e');
|
||||||
|
if (mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text('Error toggling speaker: $e')),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void _toggleKeypad() {
|
void _toggleKeypad() {
|
||||||
@ -61,9 +210,33 @@ class _CallPageState extends State<CallPage> {
|
|||||||
|
|
||||||
void _hangUp() async {
|
void _hangUp() async {
|
||||||
try {
|
try {
|
||||||
await _callService.hangUpCall(context);
|
print('CallPage: Initiating hangUp');
|
||||||
|
final result = await _callService.hangUpCall(context);
|
||||||
|
print('CallPage: Hang up result: $result');
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print("Error hanging up: $e");
|
print('CallPage: Error hanging up: $e');
|
||||||
|
if (mounted) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text('Error hanging up: $e')),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _addContact() async {
|
||||||
|
if (await FlutterContacts.requestPermission()) {
|
||||||
|
final newContact = Contact()..phones = [Phone(widget.phoneNumber)];
|
||||||
|
final updatedContact =
|
||||||
|
await FlutterContacts.openExternalInsert(newContact);
|
||||||
|
if (updatedContact != null) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text('Contact added successfully!')),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text('Permission denied for contacts')),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -73,252 +246,319 @@ class _CallPageState extends State<CallPage> {
|
|||||||
final double nameFontSize = isKeypadVisible ? 24.0 : 24.0;
|
final double nameFontSize = isKeypadVisible ? 24.0 : 24.0;
|
||||||
final double statusFontSize = isKeypadVisible ? 16.0 : 16.0;
|
final double statusFontSize = isKeypadVisible ? 16.0 : 16.0;
|
||||||
|
|
||||||
return Scaffold(
|
print(
|
||||||
body: Container(
|
'CallPage: Building UI, _callStatus: $_callStatus, route: ${ModalRoute.of(context)?.settings.name ?? "unknown"}');
|
||||||
color: Colors.black,
|
return PopScope(
|
||||||
child: SafeArea(
|
canPop: _callStatus == "Call Ended",
|
||||||
child: Column(
|
onPopInvoked: (didPop) {
|
||||||
children: [
|
if (!didPop) {
|
||||||
Container(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
SnackBar(content: Text('Cannot leave during an active call')),
|
||||||
child: Column(
|
);
|
||||||
mainAxisSize: MainAxisSize.min,
|
}
|
||||||
children: [
|
},
|
||||||
SizedBox(height: 35),
|
child: Scaffold(
|
||||||
ObfuscatedAvatar(
|
body: Container(
|
||||||
imageBytes: widget.thumbnail, // Uses thumbnail if provided
|
color: Colors.black,
|
||||||
radius: avatarRadius,
|
child: SafeArea(
|
||||||
backgroundColor: generateColorFromName(widget.displayName),
|
child: Column(
|
||||||
fallbackInitial: widget.displayName,
|
children: [
|
||||||
),
|
Container(
|
||||||
const SizedBox(height: 4),
|
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||||
Row(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.center,
|
mainAxisSize: MainAxisSize.min,
|
||||||
children: [
|
children: [
|
||||||
Icon(
|
const SizedBox(height: 35),
|
||||||
icingProtocolOk ? Icons.lock : Icons.lock_open,
|
ObfuscatedAvatar(
|
||||||
color: icingProtocolOk ? Colors.green : Colors.red,
|
imageBytes: widget.thumbnail,
|
||||||
size: 16,
|
radius: avatarRadius,
|
||||||
),
|
backgroundColor:
|
||||||
const SizedBox(width: 4),
|
generateColorFromName(widget.displayName),
|
||||||
Text(
|
fallbackInitial: widget.displayName,
|
||||||
'Icing protocol: ${icingProtocolOk ? "ok" : "ko"}',
|
),
|
||||||
style: TextStyle(
|
const SizedBox(height: 4),
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Icon(
|
||||||
|
icingProtocolOk ? Icons.lock : Icons.lock_open,
|
||||||
color: icingProtocolOk ? Colors.green : Colors.red,
|
color: icingProtocolOk ? Colors.green : Colors.red,
|
||||||
fontSize: 12,
|
size: 16,
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
),
|
||||||
),
|
const SizedBox(width: 4),
|
||||||
],
|
Text(
|
||||||
),
|
'Icing protocol: ${icingProtocolOk ? "ok" : "ko"}',
|
||||||
const SizedBox(height: 4),
|
style: TextStyle(
|
||||||
Text(
|
color:
|
||||||
_obfuscateService.obfuscateData(widget.displayName),
|
icingProtocolOk ? Colors.green : Colors.red,
|
||||||
style: TextStyle(
|
fontSize: 12,
|
||||||
fontSize: nameFontSize,
|
fontWeight: FontWeight.bold,
|
||||||
color: Colors.white,
|
),
|
||||||
fontWeight: FontWeight.bold,
|
),
|
||||||
|
],
|
||||||
),
|
),
|
||||||
),
|
const SizedBox(height: 4),
|
||||||
Text(
|
Text(
|
||||||
widget.phoneNumber,
|
_obfuscateService.obfuscateData(widget.displayName),
|
||||||
style: TextStyle(fontSize: statusFontSize, color: Colors.white70),
|
style: TextStyle(
|
||||||
),
|
fontSize: nameFontSize,
|
||||||
Text(
|
color: Colors.white,
|
||||||
'Calling...',
|
fontWeight: FontWeight.bold,
|
||||||
style: TextStyle(fontSize: statusFontSize, color: Colors.white70),
|
),
|
||||||
),
|
),
|
||||||
],
|
Text(
|
||||||
|
widget.phoneNumber,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: statusFontSize,
|
||||||
|
color: Colors.white70,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
_callStatus,
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: statusFontSize,
|
||||||
|
color: Colors.white70,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
Expanded(
|
||||||
Expanded(
|
child: Column(
|
||||||
child: Column(
|
children: [
|
||||||
children: [
|
if (isKeypadVisible) ...[
|
||||||
if (isKeypadVisible) ...[
|
const Spacer(flex: 2),
|
||||||
const Spacer(flex: 2),
|
Padding(
|
||||||
Padding(
|
padding: const EdgeInsets.symmetric(horizontal: 20.0),
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 20.0),
|
child: Row(
|
||||||
child: Row(
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
children: [
|
||||||
children: [
|
Expanded(
|
||||||
Expanded(
|
child: Text(
|
||||||
child: Text(
|
_typedDigits,
|
||||||
_typedDigits,
|
maxLines: 1,
|
||||||
maxLines: 1,
|
textAlign: TextAlign.right,
|
||||||
textAlign: TextAlign.right,
|
overflow: TextOverflow.ellipsis,
|
||||||
overflow: TextOverflow.ellipsis,
|
style: const TextStyle(
|
||||||
style: const TextStyle(
|
fontSize: 24,
|
||||||
fontSize: 24,
|
color: Colors.white,
|
||||||
color: Colors.white,
|
fontWeight: FontWeight.bold,
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
IconButton(
|
|
||||||
padding: EdgeInsets.zero,
|
|
||||||
onPressed: _toggleKeypad,
|
|
||||||
icon: const Icon(Icons.close, color: Colors.white),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
),
|
|
||||||
Container(
|
|
||||||
height: MediaQuery.of(context).size.height * 0.35,
|
|
||||||
margin: const EdgeInsets.symmetric(horizontal: 20),
|
|
||||||
child: GridView.count(
|
|
||||||
shrinkWrap: true,
|
|
||||||
physics: const NeverScrollableScrollPhysics(),
|
|
||||||
crossAxisCount: 3,
|
|
||||||
childAspectRatio: 1.3,
|
|
||||||
mainAxisSpacing: 8,
|
|
||||||
crossAxisSpacing: 8,
|
|
||||||
children: List.generate(12, (index) {
|
|
||||||
String label;
|
|
||||||
if (index < 9) {
|
|
||||||
label = '${index + 1}';
|
|
||||||
} else if (index == 9) {
|
|
||||||
label = '*';
|
|
||||||
} else if (index == 10) {
|
|
||||||
label = '0';
|
|
||||||
} else {
|
|
||||||
label = '#';
|
|
||||||
}
|
|
||||||
return GestureDetector(
|
|
||||||
onTap: () => _addDigit(label),
|
|
||||||
child: Container(
|
|
||||||
decoration: const BoxDecoration(
|
|
||||||
shape: BoxShape.circle,
|
|
||||||
color: Colors.transparent,
|
|
||||||
),
|
|
||||||
child: Center(
|
|
||||||
child: Text(
|
|
||||||
label,
|
|
||||||
style: const TextStyle(fontSize: 32, color: Colors.white),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
IconButton(
|
||||||
}),
|
padding: EdgeInsets.zero,
|
||||||
|
onPressed: _toggleKeypad,
|
||||||
|
icon: const Icon(
|
||||||
|
Icons.close,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
Container(
|
||||||
const Spacer(flex: 1),
|
height: MediaQuery.of(context).size.height * 0.4,
|
||||||
] else ...[
|
margin: const EdgeInsets.symmetric(horizontal: 20),
|
||||||
const Spacer(),
|
padding: const EdgeInsets.all(8),
|
||||||
Padding(
|
child: GridView.count(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 32.0),
|
shrinkWrap: true,
|
||||||
child: Column(
|
physics: const NeverScrollableScrollPhysics(),
|
||||||
mainAxisSize: MainAxisSize.min,
|
crossAxisCount: 3,
|
||||||
children: [
|
childAspectRatio: 1.5,
|
||||||
Row(
|
mainAxisSpacing: 8,
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
crossAxisSpacing: 8,
|
||||||
children: [
|
children: List.generate(12, (index) {
|
||||||
Column(
|
String label;
|
||||||
mainAxisSize: MainAxisSize.min,
|
if (index < 9) {
|
||||||
children: [
|
label = '${index + 1}';
|
||||||
IconButton(
|
} else if (index == 9) {
|
||||||
onPressed: _toggleMute,
|
label = '*';
|
||||||
icon: Icon(
|
} else if (index == 10) {
|
||||||
isMuted ? Icons.mic_off : Icons.mic,
|
label = '0';
|
||||||
|
} else {
|
||||||
|
label = '#';
|
||||||
|
}
|
||||||
|
return GestureDetector(
|
||||||
|
onTap: () => _addDigit(label),
|
||||||
|
child: Container(
|
||||||
|
decoration: const BoxDecoration(
|
||||||
|
shape: BoxShape.circle,
|
||||||
|
color: Colors.transparent,
|
||||||
|
),
|
||||||
|
child: Center(
|
||||||
|
child: Text(
|
||||||
|
label,
|
||||||
|
style: const TextStyle(
|
||||||
|
fontSize: 32,
|
||||||
color: Colors.white,
|
color: Colors.white,
|
||||||
size: 32,
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
Text(
|
),
|
||||||
isMuted ? 'Unmute' : 'Mute',
|
|
||||||
style: const TextStyle(color: Colors.white, fontSize: 14),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
Column(
|
);
|
||||||
mainAxisSize: MainAxisSize.min,
|
}),
|
||||||
children: [
|
),
|
||||||
IconButton(
|
|
||||||
onPressed: _toggleKeypad,
|
|
||||||
icon: const Icon(Icons.dialpad, color: Colors.white, size: 32),
|
|
||||||
),
|
|
||||||
const Text(
|
|
||||||
'Keypad',
|
|
||||||
style: TextStyle(color: Colors.white, fontSize: 14),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
IconButton(
|
|
||||||
onPressed: _toggleSpeaker,
|
|
||||||
icon: Icon(
|
|
||||||
isSpeakerOn ? Icons.volume_up : Icons.volume_off,
|
|
||||||
color: isSpeakerOn ? Colors.amber : Colors.white,
|
|
||||||
size: 32,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
const Text(
|
|
||||||
'Speaker',
|
|
||||||
style: TextStyle(color: Colors.white, fontSize: 14),
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
const SizedBox(height: 20),
|
|
||||||
Row(
|
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
|
||||||
children: [
|
|
||||||
Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
IconButton(
|
|
||||||
onPressed: () {},
|
|
||||||
icon: const Icon(Icons.person_add, color: Colors.white, size: 32),
|
|
||||||
),
|
|
||||||
const Text('Add Contact',
|
|
||||||
style: TextStyle(color: Colors.white, fontSize: 14)),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
Column(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
IconButton(
|
|
||||||
onPressed: () {},
|
|
||||||
icon: const Icon(Icons.sim_card, color: Colors.white, size: 32),
|
|
||||||
),
|
|
||||||
const Text('Change SIM',
|
|
||||||
style: TextStyle(color: Colors.white, fontSize: 14)),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
),
|
||||||
),
|
const Spacer(flex: 1),
|
||||||
const Spacer(flex: 3),
|
] else ...[
|
||||||
|
const Spacer(),
|
||||||
|
Padding(
|
||||||
|
padding: const EdgeInsets.symmetric(horizontal: 32.0),
|
||||||
|
child: Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment:
|
||||||
|
MainAxisAlignment.spaceEvenly,
|
||||||
|
children: [
|
||||||
|
Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
IconButton(
|
||||||
|
onPressed: _toggleMute,
|
||||||
|
icon: Icon(
|
||||||
|
isMuted ? Icons.mic_off : Icons.mic,
|
||||||
|
color: isMuted
|
||||||
|
? Colors.amber
|
||||||
|
: Colors.white,
|
||||||
|
size: 32,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Text(
|
||||||
|
isMuted ? 'Unmute' : 'Mute',
|
||||||
|
style: const TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontSize: 14,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
IconButton(
|
||||||
|
onPressed: _toggleKeypad,
|
||||||
|
icon: const Icon(
|
||||||
|
Icons.dialpad,
|
||||||
|
color: Colors.white,
|
||||||
|
size: 32,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Text(
|
||||||
|
'Keypad',
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontSize: 14,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
IconButton(
|
||||||
|
onPressed: _toggleSpeaker,
|
||||||
|
icon: Icon(
|
||||||
|
isSpeaker
|
||||||
|
? Icons.volume_up
|
||||||
|
: Icons.volume_off,
|
||||||
|
color: isSpeaker
|
||||||
|
? Colors.amber
|
||||||
|
: Colors.white,
|
||||||
|
size: 32,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Text(
|
||||||
|
'Speaker',
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontSize: 14,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
const SizedBox(height: 20),
|
||||||
|
Row(
|
||||||
|
mainAxisAlignment:
|
||||||
|
MainAxisAlignment.spaceEvenly,
|
||||||
|
children: [
|
||||||
|
if (isNumberUnknown)
|
||||||
|
Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
IconButton(
|
||||||
|
onPressed: _addContact,
|
||||||
|
icon: const Icon(
|
||||||
|
Icons.person_add,
|
||||||
|
color: Colors.white,
|
||||||
|
size: 32,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Text(
|
||||||
|
'Add Contact',
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontSize: 14,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
Column(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
IconButton(
|
||||||
|
onPressed: () {},
|
||||||
|
icon: const Icon(
|
||||||
|
Icons.sim_card,
|
||||||
|
color: Colors.white,
|
||||||
|
size: 32,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Text(
|
||||||
|
'Change SIM',
|
||||||
|
style: TextStyle(
|
||||||
|
color: Colors.white,
|
||||||
|
fontSize: 14,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
const Spacer(flex: 3),
|
||||||
|
],
|
||||||
],
|
],
|
||||||
],
|
),
|
||||||
),
|
),
|
||||||
),
|
Padding(
|
||||||
Padding(
|
padding: const EdgeInsets.only(bottom: 16.0),
|
||||||
padding: const EdgeInsets.only(bottom: 16.0),
|
child: GestureDetector(
|
||||||
child: GestureDetector(
|
onTap: _hangUp,
|
||||||
onTap: _hangUp,
|
child: Container(
|
||||||
child: Container(
|
padding: const EdgeInsets.all(12),
|
||||||
padding: const EdgeInsets.all(12),
|
decoration: const BoxDecoration(
|
||||||
decoration: const BoxDecoration(
|
color: Colors.red,
|
||||||
color: Colors.red,
|
shape: BoxShape.circle,
|
||||||
shape: BoxShape.circle,
|
),
|
||||||
),
|
child: const Icon(
|
||||||
child: const Icon(
|
Icons.call_end,
|
||||||
Icons.call_end,
|
color: Colors.white,
|
||||||
color: Colors.white,
|
size: 32,
|
||||||
size: 32,
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
],
|
||||||
],
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2,9 +2,8 @@ import 'package:flutter/material.dart';
|
|||||||
import 'package:flutter_contacts/flutter_contacts.dart';
|
import 'package:flutter_contacts/flutter_contacts.dart';
|
||||||
import 'package:url_launcher/url_launcher.dart';
|
import 'package:url_launcher/url_launcher.dart';
|
||||||
import '../../services/contact_service.dart';
|
import '../../services/contact_service.dart';
|
||||||
import '../../services/obfuscate_service.dart'; // Import ObfuscateService
|
import '../../services/obfuscate_service.dart';
|
||||||
import '../../services/call_service.dart'; // Import the CallService
|
import '../../services/call_service.dart';
|
||||||
import '../contacts/widgets/add_contact_button.dart';
|
|
||||||
|
|
||||||
class CompositionPage extends StatefulWidget {
|
class CompositionPage extends StatefulWidget {
|
||||||
const CompositionPage({super.key});
|
const CompositionPage({super.key});
|
||||||
@ -18,11 +17,7 @@ class _CompositionPageState extends State<CompositionPage> {
|
|||||||
List<Contact> _allContacts = [];
|
List<Contact> _allContacts = [];
|
||||||
List<Contact> _filteredContacts = [];
|
List<Contact> _filteredContacts = [];
|
||||||
final ContactService _contactService = ContactService();
|
final ContactService _contactService = ContactService();
|
||||||
|
|
||||||
// Instantiate the ObfuscateService
|
|
||||||
final ObfuscateService _obfuscateService = ObfuscateService();
|
final ObfuscateService _obfuscateService = ObfuscateService();
|
||||||
|
|
||||||
// Instantiate the CallService
|
|
||||||
final CallService _callService = CallService();
|
final CallService _callService = CallService();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -40,8 +35,13 @@ class _CompositionPageState extends State<CompositionPage> {
|
|||||||
void _filterContacts() {
|
void _filterContacts() {
|
||||||
setState(() {
|
setState(() {
|
||||||
_filteredContacts = _allContacts.where((contact) {
|
_filteredContacts = _allContacts.where((contact) {
|
||||||
final phoneMatch = contact.phones.any((phone) =>
|
bool phoneMatch = contact.phones.any((phone) {
|
||||||
phone.number.replaceAll(RegExp(r'\D'), '').contains(dialedNumber));
|
final rawPhoneNumber = phone.number;
|
||||||
|
final strippedPhoneNumber = rawPhoneNumber.replaceAll(RegExp(r'\D'), '');
|
||||||
|
final strippedDialedNumber = dialedNumber.replaceAll(RegExp(r'\D'), '');
|
||||||
|
return rawPhoneNumber.contains(dialedNumber) ||
|
||||||
|
strippedPhoneNumber.contains(strippedDialedNumber);
|
||||||
|
});
|
||||||
final nameMatch = contact.displayName
|
final nameMatch = contact.displayName
|
||||||
.toLowerCase()
|
.toLowerCase()
|
||||||
.contains(dialedNumber.toLowerCase());
|
.contains(dialedNumber.toLowerCase());
|
||||||
@ -57,6 +57,13 @@ class _CompositionPageState extends State<CompositionPage> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _onPlusPress() {
|
||||||
|
setState(() {
|
||||||
|
dialedNumber += '+';
|
||||||
|
_filterContacts();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
void _onDeletePress() {
|
void _onDeletePress() {
|
||||||
setState(() {
|
setState(() {
|
||||||
if (dialedNumber.isNotEmpty) {
|
if (dialedNumber.isNotEmpty) {
|
||||||
@ -73,7 +80,6 @@ class _CompositionPageState extends State<CompositionPage> {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Function to call a contact's number using the CallService
|
|
||||||
void _makeCall(String phoneNumber) async {
|
void _makeCall(String phoneNumber) async {
|
||||||
try {
|
try {
|
||||||
await _callService.makeGsmCall(context, phoneNumber: phoneNumber);
|
await _callService.makeGsmCall(context, phoneNumber: phoneNumber);
|
||||||
@ -82,10 +88,12 @@ class _CompositionPageState extends State<CompositionPage> {
|
|||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
debugPrint("Error making call: $e");
|
debugPrint("Error making call: $e");
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text('Failed to make call: $e')),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Function to send an SMS to a contact's number
|
|
||||||
void _launchSms(String phoneNumber) async {
|
void _launchSms(String phoneNumber) async {
|
||||||
final uri = Uri(scheme: 'sms', path: phoneNumber);
|
final uri = Uri(scheme: 'sms', path: phoneNumber);
|
||||||
if (await canLaunchUrl(uri)) {
|
if (await canLaunchUrl(uri)) {
|
||||||
@ -95,6 +103,20 @@ class _CompositionPageState extends State<CompositionPage> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void _addContact() async {
|
||||||
|
if (await FlutterContacts.requestPermission()) {
|
||||||
|
final newContact = Contact()
|
||||||
|
..phones = [Phone(dialedNumber.isNotEmpty ? dialedNumber : '')];
|
||||||
|
final updatedContact = await FlutterContacts.openExternalInsert(newContact);
|
||||||
|
if (updatedContact != null) {
|
||||||
|
_fetchContacts();
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text('Contact added successfully!')),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
@ -103,7 +125,6 @@ class _CompositionPageState extends State<CompositionPage> {
|
|||||||
children: [
|
children: [
|
||||||
Column(
|
Column(
|
||||||
children: [
|
children: [
|
||||||
// Top half: Display contacts matching dialed number
|
|
||||||
Expanded(
|
Expanded(
|
||||||
flex: 2,
|
flex: 2,
|
||||||
child: Container(
|
child: Container(
|
||||||
@ -115,57 +136,51 @@ class _CompositionPageState extends State<CompositionPage> {
|
|||||||
children: [
|
children: [
|
||||||
Expanded(
|
Expanded(
|
||||||
child: ListView(
|
child: ListView(
|
||||||
children: _filteredContacts.isNotEmpty
|
children: [
|
||||||
? _filteredContacts.map((contact) {
|
..._filteredContacts.map((contact) {
|
||||||
final phoneNumber = contact.phones.isNotEmpty
|
final phoneNumber = contact.phones.isNotEmpty
|
||||||
? contact.phones.first.number
|
? contact.phones.first.number
|
||||||
: 'No phone number';
|
: 'No phone number';
|
||||||
return ListTile(
|
return ListTile(
|
||||||
title: Text(
|
title: Text(
|
||||||
_obfuscateService.obfuscateData(contact.displayName),
|
_obfuscateService.obfuscateData(contact.displayName),
|
||||||
style: const TextStyle(color: Colors.white),
|
style: const TextStyle(color: Colors.white),
|
||||||
|
),
|
||||||
|
subtitle: Text(
|
||||||
|
_obfuscateService.obfuscateData(phoneNumber),
|
||||||
|
style: const TextStyle(color: Colors.grey),
|
||||||
|
),
|
||||||
|
trailing: Row(
|
||||||
|
mainAxisSize: MainAxisSize.min,
|
||||||
|
children: [
|
||||||
|
IconButton(
|
||||||
|
icon: Icon(Icons.phone, color: Colors.green[300], size: 20),
|
||||||
|
onPressed: () => _makeCall(phoneNumber),
|
||||||
|
),
|
||||||
|
IconButton(
|
||||||
|
icon: Icon(Icons.message, color: Colors.blue[300], size: 20),
|
||||||
|
onPressed: () => _launchSms(phoneNumber),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
onTap: () {},
|
||||||
|
);
|
||||||
|
}).toList(),
|
||||||
|
ListTile(
|
||||||
|
title: const Text(
|
||||||
|
'Add a contact',
|
||||||
|
style: TextStyle(color: Colors.white),
|
||||||
),
|
),
|
||||||
subtitle: Text(
|
trailing: Icon(Icons.add, color: Colors.grey[600]),
|
||||||
_obfuscateService.obfuscateData(phoneNumber),
|
onTap: _addContact,
|
||||||
style: const TextStyle(color: Colors.grey),
|
),
|
||||||
),
|
],
|
||||||
trailing: Row(
|
|
||||||
mainAxisSize: MainAxisSize.min,
|
|
||||||
children: [
|
|
||||||
// Call button (Now using CallService)
|
|
||||||
IconButton(
|
|
||||||
icon: Icon(Icons.phone,
|
|
||||||
color: Colors.green[300],
|
|
||||||
size: 20),
|
|
||||||
onPressed: () {
|
|
||||||
_makeCall(phoneNumber); // Make a call using CallService
|
|
||||||
},
|
|
||||||
),
|
|
||||||
// Message button
|
|
||||||
IconButton(
|
|
||||||
icon: Icon(Icons.message,
|
|
||||||
color: Colors.blue[300],
|
|
||||||
size: 20),
|
|
||||||
onPressed: () {
|
|
||||||
_launchSms(phoneNumber);
|
|
||||||
},
|
|
||||||
),
|
|
||||||
],
|
|
||||||
),
|
|
||||||
onTap: () {
|
|
||||||
// Handle contact selection if needed
|
|
||||||
},
|
|
||||||
);
|
|
||||||
}).toList()
|
|
||||||
: [],
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
// Bottom half: Dialpad and Dialed number display with erase button
|
|
||||||
Expanded(
|
Expanded(
|
||||||
flex: 2,
|
flex: 2,
|
||||||
child: Container(
|
child: Container(
|
||||||
@ -173,7 +188,6 @@ class _CompositionPageState extends State<CompositionPage> {
|
|||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.start,
|
mainAxisAlignment: MainAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
// Display dialed number with erase button
|
|
||||||
Row(
|
Row(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
children: [
|
children: [
|
||||||
@ -182,61 +196,57 @@ class _CompositionPageState extends State<CompositionPage> {
|
|||||||
alignment: Alignment.center,
|
alignment: Alignment.center,
|
||||||
child: Text(
|
child: Text(
|
||||||
dialedNumber,
|
dialedNumber,
|
||||||
style: const TextStyle(
|
style: const TextStyle(fontSize: 24, color: Colors.white),
|
||||||
fontSize: 24, color: Colors.white),
|
|
||||||
overflow: TextOverflow.ellipsis,
|
overflow: TextOverflow.ellipsis,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
IconButton(
|
GestureDetector(
|
||||||
onPressed: _onClearPress,
|
onTap: _onDeletePress,
|
||||||
icon: const Icon(Icons.backspace,
|
onLongPress: _onClearPress,
|
||||||
color: Colors.white),
|
child: const Padding(
|
||||||
|
padding: EdgeInsets.all(8.0),
|
||||||
|
child: Icon(Icons.backspace, color: Colors.white),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
const SizedBox(height: 10),
|
const SizedBox(height: 10),
|
||||||
|
|
||||||
// Dialpad
|
|
||||||
Expanded(
|
Expanded(
|
||||||
child: SingleChildScrollView(
|
child: SingleChildScrollView(
|
||||||
child: Column(
|
child: Column(
|
||||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||||
children: [
|
children: [
|
||||||
Row(
|
Row(
|
||||||
mainAxisAlignment:
|
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||||
MainAxisAlignment.spaceEvenly,
|
|
||||||
children: [
|
children: [
|
||||||
_buildDialButton('1'),
|
_buildDialButton('1', Colors.white),
|
||||||
_buildDialButton('2'),
|
_buildDialButton('2', Colors.white),
|
||||||
_buildDialButton('3'),
|
_buildDialButton('3', Colors.white),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
Row(
|
Row(
|
||||||
mainAxisAlignment:
|
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||||
MainAxisAlignment.spaceEvenly,
|
|
||||||
children: [
|
children: [
|
||||||
_buildDialButton('4'),
|
_buildDialButton('4', Colors.white),
|
||||||
_buildDialButton('5'),
|
_buildDialButton('5', Colors.white),
|
||||||
_buildDialButton('6'),
|
_buildDialButton('6', Colors.white),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
Row(
|
Row(
|
||||||
mainAxisAlignment:
|
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||||
MainAxisAlignment.spaceEvenly,
|
|
||||||
children: [
|
children: [
|
||||||
_buildDialButton('7'),
|
_buildDialButton('7', Colors.white),
|
||||||
_buildDialButton('8'),
|
_buildDialButton('8', Colors.white),
|
||||||
_buildDialButton('9'),
|
_buildDialButton('9', Colors.white),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
Row(
|
Row(
|
||||||
mainAxisAlignment:
|
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||||
MainAxisAlignment.spaceEvenly,
|
|
||||||
children: [
|
children: [
|
||||||
_buildDialButton('*'),
|
_buildDialButton('*', const Color.fromRGBO(117, 117, 117, 1)),
|
||||||
_buildDialButton('0'),
|
_buildDialButtonWithPlus('0'),
|
||||||
_buildDialButton('#'),
|
_buildDialButton('#', const Color.fromRGBO(117, 117, 117, 1)),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@ -249,26 +259,28 @@ class _CompositionPageState extends State<CompositionPage> {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|
||||||
// Add Contact Button
|
|
||||||
Positioned(
|
Positioned(
|
||||||
bottom: 20.0,
|
bottom: 20.0,
|
||||||
left: 0,
|
left: 0,
|
||||||
right: 0,
|
right: 0,
|
||||||
child: Center(
|
child: Center(
|
||||||
child: AddContactButton(),
|
child: ElevatedButton(
|
||||||
|
onPressed: dialedNumber.isNotEmpty ? () => _makeCall(dialedNumber) : null,
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: Colors.green[700],
|
||||||
|
shape: const CircleBorder(),
|
||||||
|
padding: const EdgeInsets.all(20),
|
||||||
|
),
|
||||||
|
child: const Icon(Icons.phone, color: Colors.white, size: 30),
|
||||||
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|
||||||
// Top Row with Back Arrow
|
|
||||||
Positioned(
|
Positioned(
|
||||||
top: 40.0,
|
top: 40.0,
|
||||||
left: 16.0,
|
left: 16.0,
|
||||||
child: IconButton(
|
child: IconButton(
|
||||||
icon: const Icon(Icons.arrow_back, color: Colors.white),
|
icon: const Icon(Icons.arrow_back, color: Colors.white),
|
||||||
onPressed: () {
|
onPressed: () => Navigator.pop(context),
|
||||||
Navigator.pop(context);
|
|
||||||
},
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
@ -276,7 +288,7 @@ class _CompositionPageState extends State<CompositionPage> {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
Widget _buildDialButton(String number) {
|
Widget _buildDialButton(String number, Color textColor) {
|
||||||
return ElevatedButton(
|
return ElevatedButton(
|
||||||
onPressed: () => _onNumberPress(number),
|
onPressed: () => _onNumberPress(number),
|
||||||
style: ElevatedButton.styleFrom(
|
style: ElevatedButton.styleFrom(
|
||||||
@ -286,11 +298,38 @@ class _CompositionPageState extends State<CompositionPage> {
|
|||||||
),
|
),
|
||||||
child: Text(
|
child: Text(
|
||||||
number,
|
number,
|
||||||
style: const TextStyle(
|
style: TextStyle(fontSize: 24, color: textColor),
|
||||||
fontSize: 24,
|
|
||||||
color: Colors.white,
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
Widget _buildDialButtonWithPlus(String number) {
|
||||||
|
return Stack(
|
||||||
|
alignment: Alignment.center,
|
||||||
|
children: [
|
||||||
|
GestureDetector(
|
||||||
|
onLongPress: _onPlusPress,
|
||||||
|
child: ElevatedButton(
|
||||||
|
onPressed: () => _onNumberPress(number),
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: Colors.black,
|
||||||
|
shape: const CircleBorder(),
|
||||||
|
padding: const EdgeInsets.all(16),
|
||||||
|
),
|
||||||
|
child: Text(
|
||||||
|
number,
|
||||||
|
style: const TextStyle(fontSize: 24, color: Colors.white),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Positioned(
|
||||||
|
bottom: 8,
|
||||||
|
child: Text(
|
||||||
|
'+',
|
||||||
|
style: TextStyle(fontSize: 12, color: Colors.grey[600]),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -267,8 +267,12 @@ class _ContactModalState extends State<ContactModal> {
|
|||||||
),
|
),
|
||||||
onTap: () async {
|
onTap: () async {
|
||||||
if (widget.contact.phones.isNotEmpty) {
|
if (widget.contact.phones.isNotEmpty) {
|
||||||
await _callService.makeGsmCall(context,
|
await _callService.makeGsmCall(
|
||||||
phoneNumber: phoneNumber);
|
context,
|
||||||
|
phoneNumber: phoneNumber,
|
||||||
|
displayName: widget.contact.displayName,
|
||||||
|
thumbnail: widget.contact.thumbnail,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
@ -37,7 +37,7 @@ class HistoryPage extends StatefulWidget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
class _HistoryPageState extends State<HistoryPage>
|
class _HistoryPageState extends State<HistoryPage>
|
||||||
with SingleTickerProviderStateMixin {
|
with AutomaticKeepAliveClientMixin, SingleTickerProviderStateMixin {
|
||||||
List<History> histories = [];
|
List<History> histories = [];
|
||||||
bool loading = true;
|
bool loading = true;
|
||||||
int? _expandedIndex;
|
int? _expandedIndex;
|
||||||
@ -47,10 +47,13 @@ class _HistoryPageState extends State<HistoryPage>
|
|||||||
// Create a MethodChannel instance.
|
// Create a MethodChannel instance.
|
||||||
static const MethodChannel _channel = MethodChannel('com.example.calllog');
|
static const MethodChannel _channel = MethodChannel('com.example.calllog');
|
||||||
|
|
||||||
|
@override
|
||||||
|
bool get wantKeepAlive => true; // Preserve state when switching pages
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void didChangeDependencies() {
|
void didChangeDependencies() {
|
||||||
super.didChangeDependencies();
|
super.didChangeDependencies();
|
||||||
if (loading) {
|
if (loading && histories.isEmpty) {
|
||||||
_buildHistories();
|
_buildHistories();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -149,9 +152,9 @@ class _HistoryPageState extends State<HistoryPage>
|
|||||||
List<Contact> contacts = contactState.contacts;
|
List<Contact> contacts = contactState.contacts;
|
||||||
|
|
||||||
List<History> callHistories = [];
|
List<History> callHistories = [];
|
||||||
// Process each log entry.
|
// Process each log entry with intermittent yields to avoid freezing.
|
||||||
for (var entry in nativeLogs) {
|
for (int i = 0; i < nativeLogs.length; i++) {
|
||||||
// Each entry is a Map with keys: number, type, date, duration.
|
final entry = nativeLogs[i];
|
||||||
final String number = entry['number'] ?? '';
|
final String number = entry['number'] ?? '';
|
||||||
if (number.isEmpty) continue;
|
if (number.isEmpty) continue;
|
||||||
|
|
||||||
@ -197,6 +200,8 @@ class _HistoryPageState extends State<HistoryPage>
|
|||||||
|
|
||||||
callHistories
|
callHistories
|
||||||
.add(History(matchedContact, callDate, callType, callStatus, 1));
|
.add(History(matchedContact, callDate, callType, callStatus, 1));
|
||||||
|
// Yield every 10 iterations to avoid blocking the UI.
|
||||||
|
if (i % 10 == 0) await Future.delayed(Duration(milliseconds: 1));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Sort histories by most recent.
|
// Sort histories by most recent.
|
||||||
@ -272,6 +277,7 @@ class _HistoryPageState extends State<HistoryPage>
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
|
super.build(context); // required due to AutomaticKeepAliveClientMixin
|
||||||
final contactState = ContactState.of(context);
|
final contactState = ContactState.of(context);
|
||||||
|
|
||||||
if (loading || contactState.loading) {
|
if (loading || contactState.loading) {
|
||||||
@ -419,7 +425,12 @@ class _HistoryPageState extends State<HistoryPage>
|
|||||||
icon: const Icon(Icons.phone, color: Colors.green),
|
icon: const Icon(Icons.phone, color: Colors.green),
|
||||||
onPressed: () async {
|
onPressed: () async {
|
||||||
if (contact.phones.isNotEmpty) {
|
if (contact.phones.isNotEmpty) {
|
||||||
_callService.makeGsmCall(context, phoneNumber: contact.phones.first.number);
|
await _callService.makeGsmCall(
|
||||||
|
context,
|
||||||
|
phoneNumber: contact.phones.first.number,
|
||||||
|
displayName: contact.displayName,
|
||||||
|
thumbnail: contact.thumbnail,
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
const SnackBar(
|
const SnackBar(
|
||||||
|
102
dialer/lib/features/home/default_dialer_prompt.dart
Normal file
102
dialer/lib/features/home/default_dialer_prompt.dart
Normal file
@ -0,0 +1,102 @@
|
|||||||
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter/services.dart';
|
||||||
|
|
||||||
|
class DefaultDialerPromptScreen extends StatelessWidget {
|
||||||
|
const DefaultDialerPromptScreen({super.key});
|
||||||
|
|
||||||
|
Future<void> _requestDefaultDialer(BuildContext context) async {
|
||||||
|
const channel = MethodChannel('call_service');
|
||||||
|
try {
|
||||||
|
await channel.invokeMethod('requestDefaultDialer');
|
||||||
|
// Navigate to home page after requesting default dialer
|
||||||
|
Navigator.of(context).pushReplacementNamed('/home');
|
||||||
|
} catch (e) {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text('Error requesting default dialer: $e')),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _exploreApp(BuildContext context) {
|
||||||
|
// Navigate to home page without requesting default dialer
|
||||||
|
Navigator.of(context).pushReplacementNamed('/home');
|
||||||
|
}
|
||||||
|
|
||||||
|
@override
|
||||||
|
Widget build(BuildContext context) {
|
||||||
|
return Scaffold(
|
||||||
|
backgroundColor: Colors.black,
|
||||||
|
body: SafeArea(
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.all(16.0),
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
child: Column(
|
||||||
|
mainAxisAlignment: MainAxisAlignment.center,
|
||||||
|
children: [
|
||||||
|
Text(
|
||||||
|
'Set as Default Dialer',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 24,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
SizedBox(height: 16),
|
||||||
|
Text(
|
||||||
|
'To handle calls effectively, Icing needs to be your default dialer app. This allows Icing to manage incoming and outgoing calls seamlessly.\n\nWithout the permission, Icing will not be able to encrypt calls.',
|
||||||
|
style: TextStyle(
|
||||||
|
fontSize: 16,
|
||||||
|
color: Colors.white70,
|
||||||
|
),
|
||||||
|
textAlign: TextAlign.center,
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Row(
|
||||||
|
children: [
|
||||||
|
Expanded(
|
||||||
|
flex: 1,
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.only(right: 8.0),
|
||||||
|
child: ElevatedButton(
|
||||||
|
onPressed: () => _exploreApp(context),
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: Colors.grey[800],
|
||||||
|
foregroundColor: Colors.white,
|
||||||
|
padding: EdgeInsets.symmetric(
|
||||||
|
horizontal: 16, vertical: 12),
|
||||||
|
),
|
||||||
|
child: Text('Explore App first'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
Expanded(
|
||||||
|
flex: 1,
|
||||||
|
child: Padding(
|
||||||
|
padding: const EdgeInsets.only(left: 8.0),
|
||||||
|
child: ElevatedButton(
|
||||||
|
onPressed: () => _requestDefaultDialer(context),
|
||||||
|
style: ElevatedButton.styleFrom(
|
||||||
|
backgroundColor: Colors.blue,
|
||||||
|
foregroundColor: Colors.white,
|
||||||
|
padding: EdgeInsets.symmetric(
|
||||||
|
horizontal: 16, vertical: 12),
|
||||||
|
),
|
||||||
|
child: Text('Set as Default Dialer'),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
],
|
||||||
|
),
|
||||||
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@ -10,53 +10,82 @@ import '../../services/contact_service.dart';
|
|||||||
import 'package:dialer/features/voicemail/voicemail_page.dart';
|
import 'package:dialer/features/voicemail/voicemail_page.dart';
|
||||||
import '../contacts/widgets/contact_modal.dart';
|
import '../contacts/widgets/contact_modal.dart';
|
||||||
|
|
||||||
|
class _MyHomePageState extends State<MyHomePage> with SingleTickerProviderStateMixin {
|
||||||
class _MyHomePageState extends State<MyHomePage>
|
|
||||||
with SingleTickerProviderStateMixin {
|
|
||||||
late TabController _tabController;
|
late TabController _tabController;
|
||||||
List<Contact> _allContacts = [];
|
List<Contact> _allContacts = [];
|
||||||
List<Contact> _contactSuggestions = [];
|
List<Contact> _contactSuggestions = [];
|
||||||
final ContactService _contactService = ContactService();
|
final ContactService _contactService = ContactService();
|
||||||
final ObfuscateService _obfuscateService = ObfuscateService();
|
final ObfuscateService _obfuscateService = ObfuscateService();
|
||||||
final TextEditingController _searchController = TextEditingController();
|
final TextEditingController _searchController = TextEditingController();
|
||||||
|
late SearchController _searchBarController;
|
||||||
|
String _rawSearchInput = '';
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.initState();
|
super.initState();
|
||||||
// Set the TabController length to 4
|
_tabController = TabController(length: 4, vsync: this, initialIndex: 2);
|
||||||
_tabController = TabController(length: 4, vsync: this, initialIndex: 1);
|
|
||||||
_tabController.addListener(_handleTabIndex);
|
_tabController.addListener(_handleTabIndex);
|
||||||
|
_searchBarController = SearchController();
|
||||||
|
_searchBarController.addListener(() {
|
||||||
|
if (_searchController.text != _searchBarController.text) {
|
||||||
|
_rawSearchInput = _searchBarController.text;
|
||||||
|
_searchController.text = _rawSearchInput;
|
||||||
|
_onSearchChanged(_searchBarController.text);
|
||||||
|
}
|
||||||
|
});
|
||||||
_fetchContacts();
|
_fetchContacts();
|
||||||
}
|
}
|
||||||
|
|
||||||
void _fetchContacts() async {
|
void _fetchContacts() async {
|
||||||
_allContacts = await _contactService.fetchContacts();
|
_allContacts = await _contactService.fetchContacts();
|
||||||
setState(() {});
|
_contactSuggestions = List.from(_allContacts);
|
||||||
|
if (mounted) setState(() {});
|
||||||
}
|
}
|
||||||
|
|
||||||
void _clearSearch() {
|
void _clearSearch() {
|
||||||
_searchController.clear();
|
_searchController.clear();
|
||||||
|
_searchBarController.clear();
|
||||||
|
_rawSearchInput = '';
|
||||||
_onSearchChanged('');
|
_onSearchChanged('');
|
||||||
}
|
}
|
||||||
|
|
||||||
void _onSearchChanged(String query) {
|
void _onSearchChanged(String query) {
|
||||||
setState(() {
|
setState(() {
|
||||||
if (query.isEmpty) {
|
if (query.isEmpty) {
|
||||||
_contactSuggestions = List.from(_allContacts); // Reset suggestions
|
_contactSuggestions = List.from(_allContacts);
|
||||||
} else {
|
} else {
|
||||||
|
final normalizedQuery = _normalizeString(query.toLowerCase());
|
||||||
_contactSuggestions = _allContacts.where((contact) {
|
_contactSuggestions = _allContacts.where((contact) {
|
||||||
return contact.displayName
|
final normalizedName = _normalizeString(contact.displayName.toLowerCase());
|
||||||
.toLowerCase()
|
return normalizedName.contains(normalizedQuery);
|
||||||
.contains(query.toLowerCase());
|
|
||||||
}).toList();
|
}).toList();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
String _normalizeString(String input) {
|
||||||
|
const accentMap = {
|
||||||
|
'àáâãäå': 'a',
|
||||||
|
'èéêë': 'e',
|
||||||
|
'ìíîï': 'i',
|
||||||
|
'òóôõö': 'o',
|
||||||
|
'ùúûü': 'u',
|
||||||
|
'ç': 'c',
|
||||||
|
'ñ': 'n',
|
||||||
|
};
|
||||||
|
String normalized = input;
|
||||||
|
accentMap.forEach((accents, base) {
|
||||||
|
for (var accent in accents.split('')) {
|
||||||
|
normalized = normalized.replaceAll(accent, base);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
return normalized;
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
_searchController.dispose();
|
_searchController.dispose();
|
||||||
|
_searchBarController.dispose();
|
||||||
_tabController.removeListener(_handleTabIndex);
|
_tabController.removeListener(_handleTabIndex);
|
||||||
_tabController.dispose();
|
_tabController.dispose();
|
||||||
super.dispose();
|
super.dispose();
|
||||||
@ -69,19 +98,18 @@ class _MyHomePageState extends State<MyHomePage>
|
|||||||
void _toggleFavorite(Contact contact) async {
|
void _toggleFavorite(Contact contact) async {
|
||||||
try {
|
try {
|
||||||
if (await FlutterContacts.requestPermission()) {
|
if (await FlutterContacts.requestPermission()) {
|
||||||
Contact? fullContact = await FlutterContacts.getContact(contact.id,
|
Contact? fullContact = await FlutterContacts.getContact(
|
||||||
withProperties: true,
|
contact.id,
|
||||||
withAccounts: true,
|
withProperties: true,
|
||||||
withPhoto: true,
|
withAccounts: true,
|
||||||
withThumbnail: true);
|
withPhoto: true,
|
||||||
|
withThumbnail: true,
|
||||||
|
);
|
||||||
|
|
||||||
if (fullContact != null) {
|
if (fullContact != null) {
|
||||||
fullContact.isStarred = !fullContact.isStarred;
|
fullContact.isStarred = !fullContact.isStarred;
|
||||||
await FlutterContacts.updateContact(fullContact);
|
await FlutterContacts.updateContact(fullContact);
|
||||||
setState(() {
|
_fetchContacts();
|
||||||
// Updating the contact list after toggling the favorite
|
|
||||||
_fetchContacts();
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
print("Could not fetch contact details");
|
print("Could not fetch contact details");
|
||||||
@ -100,7 +128,6 @@ class _MyHomePageState extends State<MyHomePage>
|
|||||||
backgroundColor: Colors.black,
|
backgroundColor: Colors.black,
|
||||||
body: Column(
|
body: Column(
|
||||||
children: [
|
children: [
|
||||||
// Persistent Search Bar
|
|
||||||
Padding(
|
Padding(
|
||||||
padding: const EdgeInsets.only(
|
padding: const EdgeInsets.only(
|
||||||
top: 24.0,
|
top: 24.0,
|
||||||
@ -118,35 +145,33 @@ class _MyHomePageState extends State<MyHomePage>
|
|||||||
border: Border.all(color: Colors.grey.shade800, width: 1),
|
border: Border.all(color: Colors.grey.shade800, width: 1),
|
||||||
),
|
),
|
||||||
child: SearchAnchor(
|
child: SearchAnchor(
|
||||||
builder:
|
searchController: _searchBarController,
|
||||||
(BuildContext context, SearchController controller) {
|
builder: (BuildContext context, SearchController controller) {
|
||||||
return GestureDetector(
|
return GestureDetector(
|
||||||
onTap: () {
|
onTap: () {
|
||||||
controller.openView(); // Open the search view
|
controller.openView();
|
||||||
},
|
},
|
||||||
child: Container(
|
child: Container(
|
||||||
decoration: BoxDecoration(
|
decoration: BoxDecoration(
|
||||||
color: const Color.fromARGB(255, 30, 30, 30),
|
color: const Color.fromARGB(255, 30, 30, 30),
|
||||||
borderRadius: BorderRadius.circular(12.0),
|
borderRadius: BorderRadius.circular(12.0),
|
||||||
border: Border.all(
|
border: Border.all(color: Colors.grey.shade800, width: 1),
|
||||||
color: Colors.grey.shade800, width: 1),
|
|
||||||
),
|
),
|
||||||
padding: const EdgeInsets.symmetric(
|
padding: const EdgeInsets.symmetric(vertical: 12.0, horizontal: 16.0),
|
||||||
vertical: 12.0, horizontal: 16.0),
|
|
||||||
child: Row(
|
child: Row(
|
||||||
children: [
|
children: [
|
||||||
const Icon(Icons.search,
|
const Icon(Icons.search, color: Colors.grey, size: 24.0),
|
||||||
color: Colors.grey, size: 24.0),
|
|
||||||
const SizedBox(width: 8.0),
|
const SizedBox(width: 8.0),
|
||||||
Text(
|
Expanded(
|
||||||
_searchController.text.isEmpty
|
child: Text(
|
||||||
? 'Search contacts'
|
_rawSearchInput.isEmpty
|
||||||
: _searchController.text,
|
? 'Search contacts'
|
||||||
style: const TextStyle(
|
: _rawSearchInput,
|
||||||
color: Colors.grey, fontSize: 16.0),
|
style: const TextStyle(color: Colors.grey, fontSize: 16.0),
|
||||||
|
overflow: TextOverflow.ellipsis,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
const Spacer(),
|
if (_rawSearchInput.isNotEmpty)
|
||||||
if (_searchController.text.isNotEmpty)
|
|
||||||
GestureDetector(
|
GestureDetector(
|
||||||
onTap: _clearSearch,
|
onTap: _clearSearch,
|
||||||
child: const Icon(
|
child: const Icon(
|
||||||
@ -161,23 +186,24 @@ class _MyHomePageState extends State<MyHomePage>
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
viewOnChanged: (query) {
|
viewOnChanged: (query) {
|
||||||
_onSearchChanged(query); // Update immediately
|
|
||||||
|
if (_searchBarController.text != query) {
|
||||||
|
_rawSearchInput = query;
|
||||||
|
_searchBarController.text = query;
|
||||||
|
_searchController.text = query;
|
||||||
|
}
|
||||||
|
_onSearchChanged(query);
|
||||||
},
|
},
|
||||||
suggestionsBuilder:
|
suggestionsBuilder: (BuildContext context, SearchController controller) {
|
||||||
(BuildContext context, SearchController controller) {
|
|
||||||
return _contactSuggestions.map((contact) {
|
return _contactSuggestions.map((contact) {
|
||||||
return ListTile(
|
return ListTile(
|
||||||
key: ValueKey(contact.id),
|
key: ValueKey(contact.id),
|
||||||
title: Text(_obfuscateService.obfuscateData(contact.displayName),
|
title: Text(
|
||||||
style: const TextStyle(color: Colors.white)),
|
_obfuscateService.obfuscateData(contact.displayName),
|
||||||
|
style: const TextStyle(color: Colors.white),
|
||||||
|
),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
// Clear the search text input
|
|
||||||
controller.text = '';
|
|
||||||
|
|
||||||
// Close the search view
|
|
||||||
controller.closeView(contact.displayName);
|
controller.closeView(contact.displayName);
|
||||||
|
|
||||||
// Show the ContactModal when a contact is tapped
|
|
||||||
showModalBottomSheet(
|
showModalBottomSheet(
|
||||||
context: context,
|
context: context,
|
||||||
isScrollControlled: true,
|
isScrollControlled: true,
|
||||||
@ -186,34 +212,28 @@ class _MyHomePageState extends State<MyHomePage>
|
|||||||
return ContactModal(
|
return ContactModal(
|
||||||
contact: contact,
|
contact: contact,
|
||||||
onEdit: () async {
|
onEdit: () async {
|
||||||
if (await FlutterContacts
|
if (await FlutterContacts.requestPermission()) {
|
||||||
.requestPermission()) {
|
final updatedContact = await FlutterContacts
|
||||||
final updatedContact =
|
.openExternalEdit(contact.id);
|
||||||
await FlutterContacts
|
|
||||||
.openExternalEdit(contact.id);
|
|
||||||
if (updatedContact != null) {
|
if (updatedContact != null) {
|
||||||
_fetchContacts();
|
_fetchContacts();
|
||||||
Navigator.of(context).pop();
|
Navigator.of(context).pop();
|
||||||
ScaffoldMessenger.of(context)
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
.showSnackBar(
|
|
||||||
SnackBar(
|
SnackBar(
|
||||||
content: Text(
|
content: Text(
|
||||||
'${contact.displayName} updated successfully!'),
|
'${contact.displayName} updated successfully!'),
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
} else {
|
} else {
|
||||||
ScaffoldMessenger.of(context)
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
.showSnackBar(
|
|
||||||
SnackBar(
|
SnackBar(
|
||||||
content: Text(
|
content: Text('Edit canceled or failed.'),
|
||||||
'Edit canceled or failed.'),
|
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
onToggleFavorite: () =>
|
onToggleFavorite: () => _toggleFavorite(contact),
|
||||||
_toggleFavorite(contact),
|
|
||||||
isFavorite: contact.isStarred,
|
isFavorite: contact.isStarred,
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
@ -225,7 +245,6 @@ class _MyHomePageState extends State<MyHomePage>
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
// 3-dot menu
|
|
||||||
PopupMenuButton<String>(
|
PopupMenuButton<String>(
|
||||||
icon: const Icon(Icons.more_vert, color: Colors.white),
|
icon: const Icon(Icons.more_vert, color: Colors.white),
|
||||||
itemBuilder: (BuildContext context) => [
|
itemBuilder: (BuildContext context) => [
|
||||||
@ -238,8 +257,7 @@ class _MyHomePageState extends State<MyHomePage>
|
|||||||
if (value == 'settings') {
|
if (value == 'settings') {
|
||||||
Navigator.push(
|
Navigator.push(
|
||||||
context,
|
context,
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(builder: (context) => const SettingsPage()),
|
||||||
builder: (context) => const SettingsPage()),
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
@ -247,7 +265,6 @@ class _MyHomePageState extends State<MyHomePage>
|
|||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
// Main content with TabBarView
|
|
||||||
Expanded(
|
Expanded(
|
||||||
child: Stack(
|
child: Stack(
|
||||||
children: [
|
children: [
|
||||||
@ -322,4 +339,4 @@ class MyHomePage extends StatefulWidget {
|
|||||||
|
|
||||||
@override
|
@override
|
||||||
_MyHomePageState createState() => _MyHomePageState();
|
_MyHomePageState createState() => _MyHomePageState();
|
||||||
}
|
}
|
@ -1,4 +1,5 @@
|
|||||||
import 'package:dialer/features/home/home_page.dart';
|
import 'package:dialer/features/home/home_page.dart';
|
||||||
|
import 'package:dialer/features/home/default_dialer_prompt.dart';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:dialer/features/contacts/contact_state.dart';
|
import 'package:dialer/features/contacts/contact_state.dart';
|
||||||
import 'package:dialer/services/call_service.dart';
|
import 'package:dialer/services/call_service.dart';
|
||||||
@ -51,6 +52,17 @@ Future<void> _requestPermissions() async {
|
|||||||
class Dialer extends StatelessWidget {
|
class Dialer extends StatelessWidget {
|
||||||
const Dialer({super.key});
|
const Dialer({super.key});
|
||||||
|
|
||||||
|
Future<bool> _isDefaultDialer() async {
|
||||||
|
const channel = MethodChannel('call_service');
|
||||||
|
try {
|
||||||
|
final isDefault = await channel.invokeMethod<bool>('isDefaultDialer');
|
||||||
|
return isDefault ?? false;
|
||||||
|
} catch (e) {
|
||||||
|
print('Error checking default dialer: $e');
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return ContactState(
|
return ContactState(
|
||||||
@ -59,7 +71,24 @@ class Dialer extends StatelessWidget {
|
|||||||
theme: ThemeData(
|
theme: ThemeData(
|
||||||
brightness: Brightness.dark,
|
brightness: Brightness.dark,
|
||||||
),
|
),
|
||||||
home: SafeArea(child: MyHomePage()),
|
initialRoute: '/',
|
||||||
|
routes: {
|
||||||
|
'/': (context) => FutureBuilder<bool>(
|
||||||
|
future: _isDefaultDialer(),
|
||||||
|
builder: (context, snapshot) {
|
||||||
|
if (snapshot.connectionState == ConnectionState.waiting) {
|
||||||
|
return Scaffold(
|
||||||
|
body: Center(child: CircularProgressIndicator()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
if (snapshot.hasError || !snapshot.hasData || snapshot.data == false) {
|
||||||
|
return DefaultDialerPromptScreen();
|
||||||
|
}
|
||||||
|
return SafeArea(child: MyHomePage());
|
||||||
|
},
|
||||||
|
),
|
||||||
|
'/home': (context) => SafeArea(child: MyHomePage()),
|
||||||
|
},
|
||||||
),
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -1,154 +1,442 @@
|
|||||||
|
import 'dart:async';
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:flutter/services.dart';
|
import 'package:flutter/services.dart';
|
||||||
import '../features/call/call_page.dart';
|
import '../features/call/call_page.dart';
|
||||||
import '../features/call/incoming_call_page.dart'; // Import the new page
|
import '../features/call/incoming_call_page.dart';
|
||||||
|
import '../services/contact_service.dart';
|
||||||
|
|
||||||
class CallService {
|
class CallService {
|
||||||
static const MethodChannel _channel = MethodChannel('call_service');
|
static const MethodChannel _channel = MethodChannel('call_service');
|
||||||
static String? currentPhoneNumber;
|
static String? currentPhoneNumber;
|
||||||
|
static String? currentDisplayName;
|
||||||
|
static Uint8List? currentThumbnail;
|
||||||
static bool _isCallPageVisible = false;
|
static bool _isCallPageVisible = false;
|
||||||
|
static Map<String, dynamic>? _pendingCall;
|
||||||
|
static bool wasPhoneLocked = false;
|
||||||
|
static String? _activeCallNumber;
|
||||||
|
static bool _isNavigating = false;
|
||||||
|
final ContactService _contactService = ContactService();
|
||||||
|
final _callStateController = StreamController<String>.broadcast();
|
||||||
|
final _audioStateController = StreamController<Map<String, dynamic>>.broadcast();
|
||||||
|
Map<String, dynamic>? _currentAudioState;
|
||||||
|
|
||||||
static final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
|
static final GlobalKey<NavigatorState> navigatorKey = GlobalKey<NavigatorState>();
|
||||||
|
|
||||||
|
Stream<String> get callStateStream => _callStateController.stream;
|
||||||
|
Stream<Map<String, dynamic>> get audioStateStream => _audioStateController.stream;
|
||||||
|
Map<String, dynamic>? get currentAudioState => _currentAudioState;
|
||||||
|
|
||||||
CallService() {
|
CallService() {
|
||||||
_channel.setMethodCallHandler((call) async {
|
_channel.setMethodCallHandler((call) async {
|
||||||
final context = navigatorKey.currentContext;
|
print('CallService: Handling method call: ${call.method}, with args: ${call.arguments}');
|
||||||
print('CallService: Received method ${call.method} with args ${call.arguments}');
|
|
||||||
if (context == null) {
|
|
||||||
print('CallService: Navigator context is null, cannot navigate');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
switch (call.method) {
|
switch (call.method) {
|
||||||
case "callAdded":
|
case "callAdded":
|
||||||
final phoneNumber = call.arguments["callId"] as String;
|
final phoneNumber = call.arguments["callId"] as String?;
|
||||||
final state = call.arguments["state"] as String;
|
final state = call.arguments["state"] as String?;
|
||||||
currentPhoneNumber = phoneNumber.replaceFirst('tel:', '');
|
if (phoneNumber == null || state == null) {
|
||||||
print('CallService: Call added, number: $currentPhoneNumber, state: $state');
|
print('CallService: Invalid callAdded args: $call.arguments');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final decodedPhoneNumber = Uri.decodeComponent(phoneNumber.replaceFirst('tel:', ''));
|
||||||
|
print('CallService: Decoded phone number: $decodedPhoneNumber');
|
||||||
|
if (_activeCallNumber != decodedPhoneNumber) {
|
||||||
|
currentPhoneNumber = decodedPhoneNumber;
|
||||||
|
if (currentDisplayName == null || currentDisplayName == currentPhoneNumber) {
|
||||||
|
await _fetchContactInfo(decodedPhoneNumber);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
print('CallService: Call added, number: $currentPhoneNumber, displayName: $currentDisplayName, state: $state');
|
||||||
|
_callStateController.add(state);
|
||||||
if (state == "ringing") {
|
if (state == "ringing") {
|
||||||
_navigateToIncomingCallPage(context);
|
_handleIncomingCall(decodedPhoneNumber);
|
||||||
} else {
|
} else {
|
||||||
_navigateToCallPage(context);
|
_navigateToCallPage();
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case "callStateChanged":
|
case "callStateChanged":
|
||||||
final state = call.arguments["state"] as String;
|
final state = call.arguments["state"] as String?;
|
||||||
print('CallService: State changed to $state');
|
wasPhoneLocked = call.arguments["wasPhoneLocked"] as bool? ?? false;
|
||||||
|
if (state == null) {
|
||||||
|
print('CallService: Invalid callStateChanged args: $call.arguments');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
print('CallService: State changed to $state, wasPhoneLocked: $wasPhoneLocked');
|
||||||
|
_callStateController.add(state);
|
||||||
if (state == "disconnected" || state == "disconnecting") {
|
if (state == "disconnected" || state == "disconnecting") {
|
||||||
_closeCallPage(context);
|
_closeCallPage();
|
||||||
|
if (wasPhoneLocked) {
|
||||||
|
await _channel.invokeMethod("callEndedFromFlutter");
|
||||||
|
}
|
||||||
|
_activeCallNumber = null;
|
||||||
} else if (state == "active" || state == "dialing") {
|
} else if (state == "active" || state == "dialing") {
|
||||||
_navigateToCallPage(context);
|
final phoneNumber = call.arguments["callId"] as String?;
|
||||||
|
if (phoneNumber != null && _activeCallNumber != Uri.decodeComponent(phoneNumber.replaceFirst('tel:', ''))) {
|
||||||
|
currentPhoneNumber = Uri.decodeComponent(phoneNumber.replaceFirst('tel:', ''));
|
||||||
|
if (currentDisplayName == null || currentDisplayName == currentPhoneNumber) {
|
||||||
|
await _fetchContactInfo(currentPhoneNumber!);
|
||||||
|
}
|
||||||
|
} else if (currentPhoneNumber != null && _activeCallNumber != currentPhoneNumber) {
|
||||||
|
if (currentDisplayName == null || currentDisplayName == currentPhoneNumber) {
|
||||||
|
await _fetchContactInfo(currentPhoneNumber!);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
print('CallService: Skipping fetch, active call: $_activeCallNumber, current: $currentPhoneNumber, displayName: $currentDisplayName');
|
||||||
|
}
|
||||||
|
_navigateToCallPage();
|
||||||
} else if (state == "ringing") {
|
} else if (state == "ringing") {
|
||||||
_navigateToIncomingCallPage(context);
|
final phoneNumber = call.arguments["callId"] as String?;
|
||||||
|
if (phoneNumber == null) {
|
||||||
|
print('CallService: Invalid ringing callId: $call.arguments');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final decodedPhoneNumber = Uri.decodeComponent(phoneNumber.replaceFirst('tel:', ''));
|
||||||
|
if (_activeCallNumber != decodedPhoneNumber) {
|
||||||
|
currentPhoneNumber = decodedPhoneNumber;
|
||||||
|
if (currentDisplayName == null || currentDisplayName == currentPhoneNumber) {
|
||||||
|
await _fetchContactInfo(decodedPhoneNumber);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_handleIncomingCall(decodedPhoneNumber);
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
case "callEnded":
|
case "callEnded":
|
||||||
case "callRemoved":
|
case "callRemoved":
|
||||||
print('CallService: Call ended/removed');
|
wasPhoneLocked = call.arguments["wasPhoneLocked"] as bool? ?? false;
|
||||||
_closeCallPage(context);
|
print('CallService: Call ended/removed, wasPhoneLocked: $wasPhoneLocked');
|
||||||
|
_closeCallPage();
|
||||||
|
if (wasPhoneLocked) {
|
||||||
|
await _channel.invokeMethod("callEndedFromFlutter");
|
||||||
|
}
|
||||||
currentPhoneNumber = null;
|
currentPhoneNumber = null;
|
||||||
|
currentDisplayName = null;
|
||||||
|
currentThumbnail = null;
|
||||||
|
_activeCallNumber = null;
|
||||||
|
break;
|
||||||
|
case "incomingCallFromNotification":
|
||||||
|
final phoneNumber = call.arguments["phoneNumber"] as String?;
|
||||||
|
wasPhoneLocked = call.arguments["wasPhoneLocked"] as bool? ?? false;
|
||||||
|
if (phoneNumber == null) {
|
||||||
|
print('CallService: Invalid incomingCallFromNotification args: $call.arguments');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final decodedPhoneNumber = Uri.decodeComponent(phoneNumber);
|
||||||
|
if (_activeCallNumber != decodedPhoneNumber) {
|
||||||
|
currentPhoneNumber = decodedPhoneNumber;
|
||||||
|
if (currentDisplayName == null || currentDisplayName == currentPhoneNumber) {
|
||||||
|
await _fetchContactInfo(decodedPhoneNumber);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
print('CallService: Incoming call from notification: $decodedPhoneNumber, displayName: $currentDisplayName, wasPhoneLocked: $wasPhoneLocked');
|
||||||
|
_handleIncomingCall(decodedPhoneNumber);
|
||||||
|
break;
|
||||||
|
case "audioStateChanged":
|
||||||
|
final route = call.arguments["route"] as int?;
|
||||||
|
final muted = call.arguments["muted"] as bool?;
|
||||||
|
final speaker = call.arguments["speaker"] as bool?;
|
||||||
|
print('CallService: Audio state changed, route: $route, muted: $muted, speaker: $speaker');
|
||||||
|
final audioState = {
|
||||||
|
"route": route,
|
||||||
|
"muted": muted,
|
||||||
|
"speaker": speaker,
|
||||||
|
};
|
||||||
|
_currentAudioState = audioState;
|
||||||
|
_audioStateController.add(audioState);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void _navigateToCallPage(BuildContext context) {
|
Future<String?> getCallState() async {
|
||||||
if (_isCallPageVisible && ModalRoute.of(context)?.settings.name == '/call') {
|
try {
|
||||||
print('CallService: CallPage already visible, skipping navigation');
|
final state = await _channel.invokeMethod('getCallState');
|
||||||
|
print('CallService: getCallState returned: $state');
|
||||||
|
return state as String?;
|
||||||
|
} catch (e) {
|
||||||
|
print('CallService: Error getting call state: $e');
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Map<String, dynamic>> muteCall(BuildContext context, {required bool mute}) async {
|
||||||
|
try {
|
||||||
|
print('CallService: Toggling mute to $mute');
|
||||||
|
final result = await _channel.invokeMethod('muteCall', {'mute': mute});
|
||||||
|
print('CallService: muteCall result: $result');
|
||||||
|
final resultMap = Map<String, dynamic>.from(result as Map);
|
||||||
|
if (resultMap['status'] != 'success') {
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text('Failed to toggle mute')),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return resultMap;
|
||||||
|
} catch (e) {
|
||||||
|
print('CallService: Error toggling mute: $e');
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text('Error toggling mute: $e')),
|
||||||
|
);
|
||||||
|
return {'status': 'error', 'message': e.toString()};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<Map<String, dynamic>> speakerCall(BuildContext context, {required bool speaker}) async {
|
||||||
|
try {
|
||||||
|
print('CallService: Toggling speaker to $speaker');
|
||||||
|
final result = await _channel.invokeMethod('speakerCall', {'speaker': speaker});
|
||||||
|
print('CallService: speakerCall result: $result');
|
||||||
|
return Map<String, dynamic>.from(result);
|
||||||
|
} catch (e) {
|
||||||
|
print('CallService: Error toggling speaker: $e');
|
||||||
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
|
SnackBar(content: Text('Failed to toggle speaker: $e')),
|
||||||
|
);
|
||||||
|
return {'status': 'error', 'message': e.toString()};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void dispose() {
|
||||||
|
_callStateController.close();
|
||||||
|
_audioStateController.close();
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _fetchContactInfo(String phoneNumber) async {
|
||||||
|
try {
|
||||||
|
print('CallService: Fetching contact info for $phoneNumber');
|
||||||
|
final contacts = await _contactService.fetchContacts();
|
||||||
|
print('CallService: Retrieved ${contacts.length} contacts');
|
||||||
|
final normalizedPhoneNumber = _normalizePhoneNumber(phoneNumber);
|
||||||
|
print('CallService: Normalized phone number: $normalizedPhoneNumber');
|
||||||
|
for (var contact in contacts) {
|
||||||
|
for (var phone in contact.phones) {
|
||||||
|
final normalizedContactNumber = _normalizePhoneNumber(phone.number);
|
||||||
|
print('CallService: Comparing $normalizedPhoneNumber with contact number $normalizedContactNumber');
|
||||||
|
if (normalizedContactNumber == normalizedPhoneNumber) {
|
||||||
|
currentDisplayName = contact.displayName;
|
||||||
|
currentThumbnail = contact.thumbnail;
|
||||||
|
print('CallService: Found contact - displayName: $currentDisplayName, thumbnail: ${currentThumbnail != null ? "present" : "null"}');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
currentDisplayName = phoneNumber;
|
||||||
|
currentThumbnail = null;
|
||||||
|
print('CallService: No contact match, using phoneNumber as displayName: $currentDisplayName');
|
||||||
|
} catch (e) {
|
||||||
|
print('CallService: Error fetching contact info: $e');
|
||||||
|
currentDisplayName = phoneNumber;
|
||||||
|
currentThumbnail = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
String _normalizePhoneNumber(String number) {
|
||||||
|
return number.replaceAll(RegExp(r'[\s\-\(\)]'), '').replaceFirst(RegExp(r'^\+'), '');
|
||||||
|
}
|
||||||
|
|
||||||
|
void _handleIncomingCall(String phoneNumber) {
|
||||||
|
if (_activeCallNumber == phoneNumber && _isCallPageVisible) {
|
||||||
|
print('CallService: Incoming call for $phoneNumber already active, skipping');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
print('CallService: Navigating to CallPage');
|
_activeCallNumber = phoneNumber;
|
||||||
|
|
||||||
|
final context = navigatorKey.currentContext;
|
||||||
|
if (context == null) {
|
||||||
|
print('CallService: Context is null, queuing incoming call: $phoneNumber');
|
||||||
|
_pendingCall = {"phoneNumber": phoneNumber};
|
||||||
|
Future.delayed(Duration(milliseconds: 500), () => _checkPendingCall());
|
||||||
|
} else {
|
||||||
|
_navigateToIncomingCallPage(context);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Future<void> _checkPendingCall() async {
|
||||||
|
if (_pendingCall == null) {
|
||||||
|
print('CallService: No pending call to process');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final phoneNumber = _pendingCall!["phoneNumber"];
|
||||||
|
if (_activeCallNumber == phoneNumber && _isCallPageVisible) {
|
||||||
|
print('CallService: Pending call for $phoneNumber already active, clearing');
|
||||||
|
_pendingCall = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
final context = navigatorKey.currentContext;
|
||||||
|
if (context != null) {
|
||||||
|
print('CallService: Processing queued call: $phoneNumber');
|
||||||
|
currentPhoneNumber = phoneNumber;
|
||||||
|
_activeCallNumber = phoneNumber;
|
||||||
|
await _fetchContactInfo(phoneNumber);
|
||||||
|
_navigateToIncomingCallPage(context);
|
||||||
|
_pendingCall = null;
|
||||||
|
} else {
|
||||||
|
print('CallService: Context still null, retrying...');
|
||||||
|
Future.delayed(Duration(milliseconds: 500), () => _checkPendingCall());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
void _navigateToCallPage() {
|
||||||
|
if (_isNavigating) {
|
||||||
|
print('CallService: Navigation already in progress, skipping');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_isNavigating = true;
|
||||||
|
|
||||||
|
final context = navigatorKey.currentContext;
|
||||||
|
if (context == null) {
|
||||||
|
print('CallService: Cannot navigate to CallPage, context is null');
|
||||||
|
_isNavigating = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
final currentRoute = ModalRoute.of(context)?.settings.name ?? 'unknown';
|
||||||
|
print('CallService: Navigating to CallPage, Visible: $_isCallPageVisible, Current Route: $currentRoute, Active Call: $_activeCallNumber, DisplayName: $currentDisplayName');
|
||||||
|
if (_isCallPageVisible && currentRoute == '/call' && _activeCallNumber == currentPhoneNumber) {
|
||||||
|
print('CallService: CallPage already visible for $_activeCallNumber, skipping navigation');
|
||||||
|
_isNavigating = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (_isCallPageVisible && currentRoute == '/incoming_call' && _activeCallNumber == currentPhoneNumber) {
|
||||||
|
print('CallService: Popping IncomingCallPage before navigating to CallPage');
|
||||||
|
Navigator.pop(context);
|
||||||
|
_isCallPageVisible = false;
|
||||||
|
}
|
||||||
|
if (currentPhoneNumber == null) {
|
||||||
|
print('CallService: Cannot navigate to CallPage, currentPhoneNumber is null');
|
||||||
|
_isNavigating = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_activeCallNumber = currentPhoneNumber;
|
||||||
Navigator.pushReplacement(
|
Navigator.pushReplacement(
|
||||||
context,
|
context,
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
settings: const RouteSettings(name: '/call'),
|
settings: const RouteSettings(name: '/call'),
|
||||||
builder: (context) => CallPage(
|
builder: (context) => CallPage(
|
||||||
displayName: currentPhoneNumber!,
|
displayName: currentDisplayName ?? currentPhoneNumber!,
|
||||||
phoneNumber: currentPhoneNumber!,
|
phoneNumber: currentPhoneNumber!,
|
||||||
thumbnail: null,
|
thumbnail: currentThumbnail,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
).then((_) {
|
).then((_) {
|
||||||
_isCallPageVisible = false;
|
_isCallPageVisible = false;
|
||||||
|
_isNavigating = false;
|
||||||
|
print('CallService: CallPage popped, _isCallPageVisible set to false');
|
||||||
});
|
});
|
||||||
_isCallPageVisible = true;
|
_isCallPageVisible = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
void _navigateToIncomingCallPage(BuildContext context) {
|
void _navigateToIncomingCallPage(BuildContext context) {
|
||||||
if (_isCallPageVisible && ModalRoute.of(context)?.settings.name == '/incoming_call') {
|
if (_isNavigating) {
|
||||||
print('CallService: IncomingCallPage already visible, skipping navigation');
|
print('CallService: Navigation already in progress, skipping');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
_isNavigating = true;
|
||||||
|
|
||||||
|
final currentRoute = ModalRoute.of(context)?.settings.name ?? 'unknown';
|
||||||
|
print('CallService: Navigating to IncomingCallPage, Visible: $_isCallPageVisible, Current Route: $currentRoute, Active Call: $_activeCallNumber, DisplayName: $currentDisplayName');
|
||||||
|
if (_isCallPageVisible && currentRoute == '/incoming_call' && _activeCallNumber == currentPhoneNumber) {
|
||||||
|
print('CallService: IncomingCallPage already visible for $_activeCallNumber, skipping navigation');
|
||||||
|
_isNavigating = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (_isCallPageVisible && currentRoute == '/call') {
|
||||||
|
print('CallService: CallPage visible, not showing IncomingCallPage');
|
||||||
|
_isNavigating = false;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (currentPhoneNumber == null) {
|
||||||
|
print('CallService: Cannot navigate to IncomingCallPage, currentPhoneNumber is null');
|
||||||
|
_isNavigating = false;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
print('CallService: Navigating to IncomingCallPage');
|
|
||||||
Navigator.push(
|
Navigator.push(
|
||||||
context,
|
context,
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
settings: const RouteSettings(name: '/incoming_call'),
|
settings: const RouteSettings(name: '/incoming_call'),
|
||||||
builder: (context) => IncomingCallPage(
|
builder: (context) => IncomingCallPage(
|
||||||
displayName: currentPhoneNumber!,
|
displayName: currentDisplayName ?? currentPhoneNumber!,
|
||||||
phoneNumber: currentPhoneNumber!,
|
phoneNumber: currentPhoneNumber!,
|
||||||
thumbnail: null,
|
thumbnail: currentThumbnail,
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
).then((_) {
|
).then((_) {
|
||||||
_isCallPageVisible = false;
|
_isCallPageVisible = false;
|
||||||
|
_isNavigating = false;
|
||||||
|
print('CallService: IncomingCallPage popped, _isCallPageVisible set to false');
|
||||||
});
|
});
|
||||||
_isCallPageVisible = true;
|
_isCallPageVisible = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
void _closeCallPage(BuildContext context) {
|
void _closeCallPage() {
|
||||||
if (!_isCallPageVisible) {
|
final context = navigatorKey.currentContext;
|
||||||
print('CallService: CallPage not visible, skipping pop');
|
if (context == null) {
|
||||||
|
print('CallService: Cannot close page, context is null');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
print('CallService: Closing call page, _isCallPageVisible: $_isCallPageVisible');
|
||||||
if (Navigator.canPop(context)) {
|
if (Navigator.canPop(context)) {
|
||||||
print('CallService: Popping CallPage');
|
print('CallService: Popping call page');
|
||||||
Navigator.pop(context);
|
Navigator.pop(context);
|
||||||
_isCallPageVisible = false;
|
_isCallPageVisible = false;
|
||||||
|
} else {
|
||||||
|
print('CallService: No page to pop');
|
||||||
}
|
}
|
||||||
|
_activeCallNumber = null;
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> makeGsmCall(
|
Future<Map<String, dynamic>> makeGsmCall(
|
||||||
BuildContext context, {
|
BuildContext context, {
|
||||||
required String phoneNumber,
|
required String phoneNumber,
|
||||||
String? displayName,
|
String? displayName,
|
||||||
Uint8List? thumbnail,
|
Uint8List? thumbnail,
|
||||||
}) async {
|
}) async {
|
||||||
try {
|
try {
|
||||||
|
if (_activeCallNumber == phoneNumber && _isCallPageVisible) {
|
||||||
|
print('CallService: Call already active for $phoneNumber, skipping');
|
||||||
|
return {"status": "already_active", "message": "Call already in progress"};
|
||||||
|
}
|
||||||
currentPhoneNumber = phoneNumber;
|
currentPhoneNumber = phoneNumber;
|
||||||
print('CallService: Making GSM call to $phoneNumber');
|
currentDisplayName = displayName ?? phoneNumber;
|
||||||
|
currentThumbnail = thumbnail;
|
||||||
|
if (displayName == null || thumbnail == null) {
|
||||||
|
await _fetchContactInfo(phoneNumber);
|
||||||
|
}
|
||||||
|
print('CallService: Making GSM call to $phoneNumber, displayName: $currentDisplayName');
|
||||||
final result = await _channel.invokeMethod('makeGsmCall', {"phoneNumber": phoneNumber});
|
final result = await _channel.invokeMethod('makeGsmCall', {"phoneNumber": phoneNumber});
|
||||||
print('CallService: makeGsmCall result: $result');
|
print('CallService: makeGsmCall result: $result');
|
||||||
if (result["status"] != "calling") {
|
final resultMap = Map<String, dynamic>.from(result as Map);
|
||||||
|
if (resultMap["status"] != "calling") {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(content: Text("Failed to initiate call")),
|
SnackBar(content: Text("Failed to initiate call")),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
return resultMap;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print("CallService: Error making call: $e");
|
print("CallService: Error making call: $e");
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(content: Text("Error making call: $e")),
|
SnackBar(content: Text("Error making call: $e")),
|
||||||
);
|
);
|
||||||
rethrow;
|
return {"status": "error", "message": e.toString()};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Future<void> hangUpCall(BuildContext context) async {
|
Future<Map<String, dynamic>> hangUpCall(BuildContext context) async {
|
||||||
try {
|
try {
|
||||||
print('CallService: Hanging up call');
|
print('CallService: Hanging up call');
|
||||||
final result = await _channel.invokeMethod('hangUpCall');
|
final result = await _channel.invokeMethod('hangUpCall');
|
||||||
print('CallService: hangUpCall result: $result');
|
print('CallService: hangUpCall result: $result');
|
||||||
if (result["status"] != "ended") {
|
final resultMap = Map<String, dynamic>.from(result as Map);
|
||||||
|
if (resultMap["status"] != "ended") {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(content: Text("Failed to end call")),
|
SnackBar(content: Text("Failed to end call")),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
return resultMap;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
print("CallService: Error hanging up call: $e");
|
print("CallService: Error hanging up call: $e");
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(context).showSnackBar(
|
||||||
SnackBar(content: Text("Error hanging up call: $e")),
|
SnackBar(content: Text("Error hanging up call: $e")),
|
||||||
);
|
);
|
||||||
rethrow;
|
return {"status": "error", "message": e.toString()};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -27,14 +27,13 @@ The protocol definition will include as completed:
|
|||||||
- Handshakes
|
- Handshakes
|
||||||
- Real-time data-stream encryption (and decryption)
|
- Real-time data-stream encryption (and decryption)
|
||||||
- Encrypted stream compression
|
- Encrypted stream compression
|
||||||
- Transmission over audio stream
|
- Transmission over audio stream (at least one modulation type)
|
||||||
- Minimal error correction in audio-based transmission
|
- First steps in FEC (Forward Error Correction): detecting half of transmission errors
|
||||||
- Error handling and user prevention
|
|
||||||
|
|
||||||
And should include prototype or scratches functionalities, among which:
|
And should include prototype or scratches functionalities, among which:
|
||||||
- Embedded silent data transmission (silently transmit light data during an encrypted phone call)
|
- Embedded silent data transmission (such as DTMF)
|
||||||
- On-the-fly key exchange (does not require prior key exchange, sacrifying some security)
|
- On-the-fly key exchange (does not require prior key exchange, sacrifying some security)
|
||||||
- Strong error correction
|
- Stronger FEC: detecting >80%, correcting 20% of transmission errors
|
||||||
|
|
||||||
#### The Icing dialer (based on Icing kotlin library, an Icing protocol implementation)
|
#### The Icing dialer (based on Icing kotlin library, an Icing protocol implementation)
|
||||||
|
|
||||||
@ -128,16 +127,15 @@ The remote bank advisor asks him to authenticate, making him type his password o
|
|||||||
By using the Icing protocol, not only would Jeff and the bank be assured that the informations are transmitted safely,
|
By using the Icing protocol, not only would Jeff and the bank be assured that the informations are transmitted safely,
|
||||||
but also that the call is coming from Jeff's phone and not an impersonator.
|
but also that the call is coming from Jeff's phone and not an impersonator.
|
||||||
|
|
||||||
Elise is a 42 years-old extreme reporter.
|
Elise, 42 years-old, is a journalist covering sensitive topics.
|
||||||
After interviewing Russians opposition's leader, the FSB is looking to interview her.
|
Her work draws attention from people who want to know what she's saying - and to whom.
|
||||||
She tries to stay discreet and hidden, but those measures constrains her to barely receive cellular network.
|
Forced to stay discreet, with unreliable signal and a likely monitored phone line,
|
||||||
She suspects her phone line to be monitored, so the best she can do to call safely, is to use her Icing dialer.
|
she uses Icing dialer to make secure calls without exposing herself.
|
||||||
|
|
||||||
Paul, a 22 years-old developer working for a big company, decides to go to China for vacations.
|
Paul, a 22 years-old developer, is enjoying its vacations abroad.
|
||||||
But everything goes wrong! The company's product he works on, is failling in the middle of the day and no one is
|
But everything goes wrong! The company's product he works on, is failling in the middle of the day and no one is
|
||||||
qualified to fix it. Paul doesn't have WiFi and his phone plan only covers voice calls in China.
|
qualified to fix it. Paul doesn't have WiFi and his phone plan only covers voice calls in his country.
|
||||||
With Icing dialer, he can call his collegues and help fix the
|
With Icing dialer, he can call his collegues and help fix the problem, completely safe.
|
||||||
problem, safe from potential Chinese spies.
|
|
||||||
|
|
||||||
## Evaluation Criteria
|
## Evaluation Criteria
|
||||||
### Protocol and lib
|
### Protocol and lib
|
||||||
|
Loading…
Reference in New Issue
Block a user