Compare commits
3 Commits
dev
...
about-page
Author | SHA1 | Date | |
---|---|---|---|
|
a418844534 | ||
|
051ce4680a | ||
|
0023f1a7c5 |
1
.gitignore
vendored
1
.gitignore
vendored
@ -9,7 +9,6 @@
|
||||
.history
|
||||
.svn/
|
||||
migrate_working_dir/
|
||||
protocol_prototype/venv
|
||||
|
||||
# IntelliJ related
|
||||
*.iml
|
||||
|
1
dialer/android/.gitignore
vendored
1
dialer/android/.gitignore
vendored
@ -7,7 +7,6 @@ gradle-wrapper.jar
|
||||
/gradle.properties
|
||||
GeneratedPluginRegistrant.java
|
||||
gradle.properties
|
||||
.cxx
|
||||
|
||||
# Remember to never publicly share your keystore.
|
||||
# See https://flutter.dev/to/reference-keystore
|
||||
|
@ -1,6 +1,4 @@
|
||||
<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-permission android:name="android.permission.READ_CONTACTS"/>
|
||||
<uses-permission android:name="android.permission.WRITE_CONTACTS"/>
|
||||
<uses-permission android:name="android.permission.CALL_PHONE" />
|
||||
@ -9,11 +7,7 @@
|
||||
<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_PHONE_STATE"/>
|
||||
<uses-permission android:name="android.permission.MANAGE_OWN_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.POST_NOTIFICATIONS" />
|
||||
<uses-permission android:name="android.permission.SYSTEM_ALERT_WINDOW" />
|
||||
|
||||
|
||||
<application
|
||||
android:label="Icing Dialer"
|
||||
@ -41,48 +35,7 @@
|
||||
<action android:name="android.intent.action.MAIN"/>
|
||||
<category android:name="android.intent.category.LAUNCHER"/>
|
||||
</intent-filter>
|
||||
<!-- Dialer intent filters (required for default dialer eligibility) -->
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.DIAL" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.DIAL" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<data android:scheme="tel" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.CALL" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
</intent-filter>
|
||||
<intent-filter>
|
||||
<action android:name="android.intent.action.VIEW" />
|
||||
<category android:name="android.intent.category.DEFAULT" />
|
||||
<category android:name="android.intent.category.BROWSABLE" />
|
||||
<data android:scheme="tel" />
|
||||
</intent-filter>
|
||||
</activity>
|
||||
<service
|
||||
android:name=".services.MyInCallService"
|
||||
android:permission="android.permission.BIND_INCALL_SERVICE"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.telecom.InCallService" />
|
||||
</intent-filter>
|
||||
<meta-data
|
||||
android:name="android.telecom.IN_CALL_SERVICE_UI"
|
||||
android:value="true" />
|
||||
</service>
|
||||
<!-- Custom ConnextionService, will be needed at some point when we implement our own protocol -->
|
||||
<!-- <service
|
||||
android:name=".services.CallConnectionService"
|
||||
android:permission="android.permission.BIND_TELECOM_CONNECTION_SERVICE"
|
||||
android:exported="true">
|
||||
<intent-filter>
|
||||
<action android:name="android.telecom.ConnectionService" />
|
||||
</intent-filter>
|
||||
</service> -->
|
||||
|
||||
<!-- Don't delete the meta-data below.
|
||||
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
|
||||
<meta-data
|
||||
|
@ -0,0 +1,28 @@
|
||||
package com.icing.dialer
|
||||
|
||||
import java.security.KeyStore
|
||||
|
||||
object KeyDeleterHelper {
|
||||
|
||||
private const val ANDROID_KEYSTORE = "AndroidKeyStore"
|
||||
|
||||
/**
|
||||
* Deletes the key pair associated with the given alias from the Android Keystore.
|
||||
*
|
||||
* @param alias The alias of the key pair to delete.
|
||||
* @throws Exception if deletion fails.
|
||||
*/
|
||||
fun deleteKeyPair(alias: String) {
|
||||
try {
|
||||
val keyStore = KeyStore.getInstance(ANDROID_KEYSTORE).apply { load(null) }
|
||||
|
||||
if (!keyStore.containsAlias(alias)) {
|
||||
throw Exception("No key found with alias \"$alias\" to delete.")
|
||||
}
|
||||
|
||||
keyStore.deleteEntry(alias)
|
||||
} catch (e: Exception) {
|
||||
throw Exception("Failed to delete key pair: ${e.message}", e)
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,47 @@
|
||||
package com.icing.dialer
|
||||
|
||||
import android.security.keystore.KeyGenParameterSpec
|
||||
import android.security.keystore.KeyProperties
|
||||
import java.security.KeyPairGenerator
|
||||
import java.security.KeyStore
|
||||
|
||||
object KeyGeneratorHelper {
|
||||
|
||||
private const val ANDROID_KEYSTORE = "AndroidKeyStore"
|
||||
|
||||
/**
|
||||
* Generates an ECDSA P-256 key pair and stores it in the Android Keystore.
|
||||
*
|
||||
* @param alias Unique identifier for the key pair.
|
||||
* @throws Exception if key generation fails.
|
||||
*/
|
||||
fun generateECKeyPair(alias: String) {
|
||||
try {
|
||||
val keyStore = KeyStore.getInstance(ANDROID_KEYSTORE).apply { load(null) }
|
||||
|
||||
// Check if the key already exists
|
||||
if (keyStore.containsAlias(alias)) {
|
||||
throw Exception("Key with alias \"$alias\" already exists.")
|
||||
}
|
||||
|
||||
val keyPairGenerator = KeyPairGenerator.getInstance(
|
||||
KeyProperties.KEY_ALGORITHM_EC,
|
||||
ANDROID_KEYSTORE
|
||||
)
|
||||
|
||||
val parameterSpec = KeyGenParameterSpec.Builder(
|
||||
alias,
|
||||
KeyProperties.PURPOSE_SIGN or KeyProperties.PURPOSE_VERIFY
|
||||
)
|
||||
.setAlgorithmParameterSpec(java.security.spec.ECGenParameterSpec("secp256r1"))
|
||||
.setDigests(KeyProperties.DIGEST_SHA256, KeyProperties.DIGEST_SHA384, KeyProperties.DIGEST_SHA512)
|
||||
.setUserAuthenticationRequired(false) // Set to true if you require user authentication
|
||||
.build()
|
||||
|
||||
keyPairGenerator.initialize(parameterSpec)
|
||||
keyPairGenerator.generateKeyPair()
|
||||
} catch (e: Exception) {
|
||||
throw Exception("Failed to generate EC key pair: ${e.message}", e)
|
||||
}
|
||||
}
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
package com.icing.dialer
|
||||
|
||||
import android.os.Build
|
||||
import java.security.PrivateKey
|
||||
import android.security.keystore.KeyGenParameterSpec
|
||||
import android.security.keystore.KeyProperties
|
||||
import android.util.Base64
|
||||
@ -8,21 +8,15 @@ import io.flutter.plugin.common.MethodCall
|
||||
import io.flutter.plugin.common.MethodChannel
|
||||
import java.security.KeyPairGenerator
|
||||
import java.security.KeyStore
|
||||
import java.security.PrivateKey
|
||||
import java.security.Signature
|
||||
import java.security.spec.ECGenParameterSpec
|
||||
|
||||
class KeystoreHelper(private val call: MethodCall, private val result: MethodChannel.Result) {
|
||||
|
||||
private val ANDROID_KEYSTORE = "AndroidKeyStore"
|
||||
|
||||
fun handleMethodCall() {
|
||||
if (Build.VERSION.SDK_INT < Build.VERSION_CODES.R) {
|
||||
result.error("UNSUPPORTED_API", "ED25519 requires Android 11 (API 30) or higher", null)
|
||||
return
|
||||
}
|
||||
when (call.method) {
|
||||
"generateKeyPair" -> generateEDKeyPair()
|
||||
"generateKeyPair" -> generateECKeyPair()
|
||||
"signData" -> signData()
|
||||
"getPublicKey" -> getPublicKey()
|
||||
"deleteKeyPair" -> deleteKeyPair()
|
||||
@ -31,7 +25,7 @@ class KeystoreHelper(private val call: MethodCall, private val result: MethodCha
|
||||
}
|
||||
}
|
||||
|
||||
private fun generateEDKeyPair() {
|
||||
private fun generateECKeyPair() {
|
||||
val alias = call.argument<String>("alias")
|
||||
if (alias == null) {
|
||||
result.error("INVALID_ARGUMENT", "Alias is required", null)
|
||||
@ -50,14 +44,16 @@ class KeystoreHelper(private val call: MethodCall, private val result: MethodCha
|
||||
KeyProperties.KEY_ALGORITHM_EC,
|
||||
ANDROID_KEYSTORE
|
||||
)
|
||||
|
||||
val parameterSpec = KeyGenParameterSpec.Builder(
|
||||
alias,
|
||||
KeyProperties.PURPOSE_SIGN or KeyProperties.PURPOSE_VERIFY
|
||||
)
|
||||
.setAlgorithmParameterSpec(ECGenParameterSpec("ed25519"))
|
||||
.setDigests(KeyProperties.DIGEST_SHA256)
|
||||
.setAlgorithmParameterSpec(java.security.spec.ECGenParameterSpec("secp256r1"))
|
||||
.setDigests(KeyProperties.DIGEST_SHA256, KeyProperties.DIGEST_SHA384, KeyProperties.DIGEST_SHA512)
|
||||
.setUserAuthenticationRequired(false)
|
||||
.build()
|
||||
|
||||
keyPairGenerator.initialize(parameterSpec)
|
||||
keyPairGenerator.generateKeyPair()
|
||||
|
||||
@ -77,14 +73,17 @@ class KeystoreHelper(private val call: MethodCall, private val result: MethodCha
|
||||
|
||||
try {
|
||||
val keyStore = KeyStore.getInstance(ANDROID_KEYSTORE).apply { load(null) }
|
||||
|
||||
val privateKey = keyStore.getKey(alias, null) as? PrivateKey ?: run {
|
||||
result.error("KEY_NOT_FOUND", "Private key not found for alias \"$alias\".", null)
|
||||
return
|
||||
}
|
||||
val signature = Signature.getInstance("Ed25519")
|
||||
|
||||
val signature = Signature.getInstance("SHA256withECDSA")
|
||||
signature.initSign(privateKey)
|
||||
signature.update(data.toByteArray())
|
||||
val signedBytes = signature.sign()
|
||||
|
||||
val signatureBase64 = Base64.encodeToString(signedBytes, Base64.DEFAULT)
|
||||
result.success(signatureBase64)
|
||||
} catch (e: Exception) {
|
||||
|
@ -0,0 +1,30 @@
|
||||
package com.icing.dialer
|
||||
|
||||
import java.security.KeyStore
|
||||
import java.security.PublicKey
|
||||
import android.util.Base64
|
||||
|
||||
object PublicKeyHelper {
|
||||
|
||||
private const val ANDROID_KEYSTORE = "AndroidKeyStore"
|
||||
|
||||
/**
|
||||
* Retrieves the public key associated with the given alias.
|
||||
*
|
||||
* @param alias The alias of the key pair.
|
||||
* @return The public key as a Base64-encoded string.
|
||||
* @throws Exception if retrieval fails.
|
||||
*/
|
||||
fun getPublicKey(alias: String): String {
|
||||
try {
|
||||
val keyStore = KeyStore.getInstance(ANDROID_KEYSTORE).apply { load(null) }
|
||||
|
||||
val certificate = keyStore.getCertificate(alias) ?: throw Exception("Certificate not found for alias \"$alias\".")
|
||||
val publicKey: PublicKey = certificate.publicKey
|
||||
|
||||
return Base64.encodeToString(publicKey.encoded, Base64.DEFAULT)
|
||||
} catch (e: Exception) {
|
||||
throw Exception("Failed to retrieve public key: ${e.message}", e)
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,37 @@
|
||||
package com.icing.dialer
|
||||
|
||||
import android.security.keystore.KeyProperties
|
||||
import java.security.KeyStore
|
||||
import java.security.Signature
|
||||
import android.util.Base64
|
||||
import java.security.PrivateKey
|
||||
|
||||
object SignerHelper {
|
||||
|
||||
private const val ANDROID_KEYSTORE = "AndroidKeyStore"
|
||||
|
||||
/**
|
||||
* Signs the provided data using the private key associated with the given alias.
|
||||
*
|
||||
* @param alias The alias of the key pair.
|
||||
* @param data The data to sign.
|
||||
* @return The signature as a Base64-encoded string.
|
||||
* @throws Exception if signing fails.
|
||||
*/
|
||||
fun signData(alias: String, data: ByteArray): String {
|
||||
try {
|
||||
val keyStore = KeyStore.getInstance(ANDROID_KEYSTORE).apply { load(null) }
|
||||
|
||||
val privateKey = keyStore.getKey(alias, null) as? PrivateKey?: throw Exception("Private key not found for alias \"$alias\".")
|
||||
|
||||
val signature = Signature.getInstance("SHA256withECDSA")
|
||||
signature.initSign(privateKey)
|
||||
signature.update(data)
|
||||
val signedBytes = signature.sign()
|
||||
|
||||
return Base64.encodeToString(signedBytes, Base64.DEFAULT)
|
||||
} catch (e: Exception) {
|
||||
throw Exception("Failed to sign data: ${e.message}", e)
|
||||
}
|
||||
}
|
||||
}
|
@ -1,357 +1,94 @@
|
||||
package com.icing.dialer.activities
|
||||
|
||||
import android.Manifest
|
||||
import android.app.role.RoleManager
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.pm.PackageManager
|
||||
import android.database.Cursor
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.provider.CallLog
|
||||
import android.telecom.TelecomManager
|
||||
import android.util.Log
|
||||
import androidx.core.content.ContextCompat
|
||||
import com.icing.dialer.KeystoreHelper
|
||||
import com.icing.dialer.services.CallService
|
||||
import com.icing.dialer.services.MyInCallService
|
||||
import io.flutter.embedding.android.FlutterActivity
|
||||
import io.flutter.embedding.engine.FlutterEngine
|
||||
import io.flutter.plugin.common.MethodCall
|
||||
import io.flutter.plugin.common.MethodChannel
|
||||
import com.icing.dialer.KeystoreHelper
|
||||
import com.icing.dialer.services.CallService
|
||||
|
||||
class MainActivity : FlutterActivity() {
|
||||
class MainActivity: FlutterActivity() {
|
||||
// Existing channel for keystore operations.
|
||||
private val KEYSTORE_CHANNEL = "com.example.keystore"
|
||||
// New channel for call log access.
|
||||
private val CALLLOG_CHANNEL = "com.example.calllog"
|
||||
|
||||
private val CALL_CHANNEL = "call_service"
|
||||
private val TAG = "MainActivity"
|
||||
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?) {
|
||||
super.onCreate(savedInstanceState)
|
||||
Log.d(TAG, "onCreate started")
|
||||
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) {
|
||||
super.configureFlutterEngine(flutterEngine)
|
||||
Log.d(TAG, "Configuring Flutter engine")
|
||||
MyInCallService.channel = MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CALL_CHANNEL)
|
||||
|
||||
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CALL_CHANNEL)
|
||||
.setMethodCallHandler { call, result ->
|
||||
when (call.method) {
|
||||
"permissionsGranted" -> {
|
||||
Log.d(TAG, "Received permissionsGranted from Flutter")
|
||||
pendingIncomingCall?.let { (phoneNumber, showScreen) ->
|
||||
if (showScreen) {
|
||||
MyInCallService.channel?.invokeMethod("incomingCallFromNotification", mapOf(
|
||||
"phoneNumber" to phoneNumber,
|
||||
"wasPhoneLocked" to wasPhoneLocked
|
||||
))
|
||||
pendingIncomingCall = null
|
||||
}
|
||||
}
|
||||
result.success(true)
|
||||
// Call service channel
|
||||
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CALL_CHANNEL).setMethodCallHandler { call, result ->
|
||||
when (call.method) {
|
||||
"makeGsmCall" -> {
|
||||
val phoneNumber = call.argument<String>("phoneNumber")
|
||||
if (phoneNumber != null) {
|
||||
CallService.makeGsmCall(this, phoneNumber)
|
||||
result.success("Calling $phoneNumber")
|
||||
} else {
|
||||
result.error("INVALID_PHONE_NUMBER", "Phone number is required", null)
|
||||
}
|
||||
"makeGsmCall" -> {
|
||||
val phoneNumber = call.argument<String>("phoneNumber")
|
||||
if (phoneNumber != null) {
|
||||
val success = CallService.makeGsmCall(this, phoneNumber)
|
||||
if (success) {
|
||||
result.success(mapOf("status" to "calling", "phoneNumber" to phoneNumber))
|
||||
} else {
|
||||
result.error("CALL_FAILED", "Failed to initiate call", null)
|
||||
}
|
||||
} else {
|
||||
result.error("INVALID_PHONE_NUMBER", "Phone number is required", null)
|
||||
}
|
||||
}
|
||||
"hangUpCall" -> {
|
||||
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) {
|
||||
result.success(mapOf("status" to "ended"))
|
||||
if (wasPhoneLocked) {
|
||||
Log.d(TAG, "Finishing and removing task after hangup, phone was locked")
|
||||
finishAndRemoveTask()
|
||||
}
|
||||
} else {
|
||||
Log.w(TAG, "No active call to hang up")
|
||||
result.error("HANGUP_FAILED", "No active call to hang up", null)
|
||||
}
|
||||
}
|
||||
"answerCall" -> {
|
||||
val success = MyInCallService.currentCall?.let {
|
||||
it.answer(0)
|
||||
Log.d(TAG, "Answered call")
|
||||
true
|
||||
} ?: false
|
||||
if (success) {
|
||||
result.success(mapOf("status" to "answered"))
|
||||
} else {
|
||||
Log.w(TAG, "No active call to answer")
|
||||
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()
|
||||
}
|
||||
"hangUpCall" -> {
|
||||
CallService.hangUpCall(this)
|
||||
result.success("Call ended")
|
||||
}
|
||||
else -> result.notImplemented()
|
||||
}
|
||||
}
|
||||
|
||||
// Set up the keystore channel.
|
||||
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, KEYSTORE_CHANNEL)
|
||||
.setMethodCallHandler { call, result ->
|
||||
// Delegate method calls to KeystoreHelper.
|
||||
KeystoreHelper(call, result).handleMethodCall()
|
||||
}
|
||||
|
||||
// Set up the call log channel.
|
||||
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CALLLOG_CHANNEL)
|
||||
.setMethodCallHandler { call, result ->
|
||||
if (call.method == "getCallLogs") {
|
||||
if (ContextCompat.checkSelfPermission(this, Manifest.permission.READ_CALL_LOG) == PackageManager.PERMISSION_GRANTED) {
|
||||
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)
|
||||
}
|
||||
val callLogs = getCallLogs()
|
||||
result.success(callLogs)
|
||||
} else {
|
||||
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() {
|
||||
val telecomManager = getSystemService(TELECOM_SERVICE) as TelecomManager
|
||||
val currentDefault = telecomManager.defaultDialerPackage
|
||||
Log.d(TAG, "Current default dialer: $currentDefault, My package: $packageName")
|
||||
|
||||
if (currentDefault != packageName) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
val roleManager = getSystemService(Context.ROLE_SERVICE) as RoleManager
|
||||
if (roleManager.isRoleAvailable(RoleManager.ROLE_DIALER) && !roleManager.isRoleHeld(RoleManager.ROLE_DIALER)) {
|
||||
val intent = roleManager.createRequestRoleIntent(RoleManager.ROLE_DIALER)
|
||||
startActivityForResult(intent, REQUEST_CODE_SET_DEFAULT_DIALER)
|
||||
Log.d(TAG, "Launched RoleManager intent for default dialer on API 29+")
|
||||
}
|
||||
} else {
|
||||
val intent = Intent(TelecomManager.ACTION_CHANGE_DEFAULT_DIALER)
|
||||
.putExtra(TelecomManager.EXTRA_CHANGE_DEFAULT_DIALER_PACKAGE_NAME, packageName)
|
||||
.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
|
||||
try {
|
||||
startActivityForResult(intent, REQUEST_CODE_SET_DEFAULT_DIALER)
|
||||
Log.d(TAG, "Launched TelecomManager intent for default dialer")
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Failed to launch default dialer prompt: ${e.message}", e)
|
||||
launchDefaultAppsSettings()
|
||||
}
|
||||
}
|
||||
} else {
|
||||
Log.d(TAG, "Already the default dialer")
|
||||
}
|
||||
}
|
||||
|
||||
private fun launchDefaultAppsSettings() {
|
||||
val settingsIntent = Intent(android.provider.Settings.ACTION_MANAGE_DEFAULT_APPS_SETTINGS)
|
||||
startActivity(settingsIntent)
|
||||
Log.d(TAG, "Opened default apps settings as fallback")
|
||||
}
|
||||
|
||||
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
|
||||
super.onActivityResult(requestCode, resultCode, data)
|
||||
Log.d(TAG, "onActivityResult: requestCode=$requestCode, resultCode=$resultCode, data=$data")
|
||||
if (requestCode == REQUEST_CODE_SET_DEFAULT_DIALER) {
|
||||
if (resultCode == RESULT_OK) {
|
||||
Log.d(TAG, "User accepted default dialer change")
|
||||
} else {
|
||||
Log.w(TAG, "Default dialer prompt canceled or failed (resultCode=$resultCode)")
|
||||
launchDefaultAppsSettings()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Queries the Android call log and returns a list of maps.
|
||||
* Each map contains keys: "number", "type", "date", and "duration".
|
||||
*/
|
||||
private fun getCallLogs(): List<Map<String, Any?>> {
|
||||
val logsList = mutableListOf<Map<String, Any?>>()
|
||||
val cursor: Cursor? = contentResolver.query(
|
||||
CallLog.Calls.CONTENT_URI, null, null, null, CallLog.Calls.DATE + " DESC"
|
||||
CallLog.Calls.CONTENT_URI,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
CallLog.Calls.DATE + " DESC"
|
||||
)
|
||||
cursor?.use {
|
||||
while (it.moveToNext()) {
|
||||
val number = it.getString(it.getColumnIndexOrThrow(CallLog.Calls.NUMBER))
|
||||
val type = it.getInt(it.getColumnIndexOrThrow(CallLog.Calls.TYPE))
|
||||
val date = it.getLong(it.getColumnIndexOrThrow(CallLog.Calls.DATE))
|
||||
val duration = it.getLong(it.getColumnIndexOrThrow(CallLog.Calls.DURATION))
|
||||
if (cursor != null) {
|
||||
while (cursor.moveToNext()) {
|
||||
val number = cursor.getString(cursor.getColumnIndexOrThrow(CallLog.Calls.NUMBER))
|
||||
val type = cursor.getInt(cursor.getColumnIndexOrThrow(CallLog.Calls.TYPE))
|
||||
val date = cursor.getLong(cursor.getColumnIndexOrThrow(CallLog.Calls.DATE))
|
||||
val duration = cursor.getLong(cursor.getColumnIndexOrThrow(CallLog.Calls.DURATION))
|
||||
|
||||
val map = mutableMapOf<String, Any?>(
|
||||
"number" to number,
|
||||
"type" to type,
|
||||
"date" to date,
|
||||
"duration" to duration
|
||||
)
|
||||
val map = HashMap<String, Any?>()
|
||||
map["number"] = number
|
||||
map["type"] = type // Typically: 1 for incoming, 2 for outgoing, 3 for missed.
|
||||
map["date"] = date
|
||||
map["duration"] = duration
|
||||
logsList.add(map)
|
||||
}
|
||||
cursor.close()
|
||||
}
|
||||
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,82 +0,0 @@
|
||||
// package com.icing.dialer.services
|
||||
|
||||
// import android.telecom.Connection
|
||||
// import android.telecom.ConnectionService
|
||||
// import android.telecom.PhoneAccountHandle
|
||||
// import android.telecom.TelecomManager
|
||||
// import android.telecom.DisconnectCause
|
||||
// import android.util.Log
|
||||
// import io.flutter.plugin.common.MethodChannel
|
||||
|
||||
// class CallConnectionService : ConnectionService() {
|
||||
// companion object {
|
||||
// var channel: MethodChannel? = null
|
||||
// private const val TAG = "CallConnectionService"
|
||||
// }
|
||||
|
||||
// init {
|
||||
// Log.d(TAG, "CallConnectionService initialized")
|
||||
// }
|
||||
|
||||
// override fun onCreate() {
|
||||
// super.onCreate()
|
||||
// Log.d(TAG, "Service created")
|
||||
// }
|
||||
|
||||
// override fun onDestroy() {
|
||||
// super.onDestroy()
|
||||
// Log.d(TAG, "Service destroyed")
|
||||
// }
|
||||
|
||||
// override fun onCreateOutgoingConnection(
|
||||
// connectionManagerPhoneAccount: PhoneAccountHandle?,
|
||||
// request: android.telecom.ConnectionRequest
|
||||
// ): Connection {
|
||||
// Log.d(TAG, "Creating outgoing connection for ${request.address}, account: $connectionManagerPhoneAccount")
|
||||
// val connection = object : Connection() {
|
||||
// override fun onStateChanged(state: Int) {
|
||||
// super.onStateChanged(state)
|
||||
// Log.d(TAG, "Connection state changed: $state")
|
||||
// val stateStr = when (state) {
|
||||
// STATE_DIALING -> "dialing"
|
||||
// STATE_ACTIVE -> "active"
|
||||
// STATE_DISCONNECTED -> "disconnected"
|
||||
// else -> "unknown"
|
||||
// }
|
||||
// channel?.invokeMethod("callStateChanged", mapOf("state" to stateStr, "phoneNumber" to request.address.toString()))
|
||||
// }
|
||||
|
||||
// override fun onDisconnect() {
|
||||
// Log.d(TAG, "Connection disconnected")
|
||||
// setDisconnected(DisconnectCause(DisconnectCause.LOCAL))
|
||||
// destroy()
|
||||
// }
|
||||
// }
|
||||
// connection.setAddress(request.address, TelecomManager.PRESENTATION_ALLOWED)
|
||||
// connection.setInitialized()
|
||||
// connection.setDialing()
|
||||
// return connection
|
||||
// }
|
||||
|
||||
// override fun onCreateIncomingConnection(
|
||||
// connectionManagerPhoneAccount: PhoneAccountHandle?,
|
||||
// request: android.telecom.ConnectionRequest
|
||||
// ): Connection {
|
||||
// Log.d(TAG, "Creating incoming connection for ${request.address}, account: $connectionManagerPhoneAccount")
|
||||
// val connection = object : Connection() {
|
||||
// override fun onAnswer() {
|
||||
// Log.d(TAG, "Connection answered")
|
||||
// setActive()
|
||||
// }
|
||||
|
||||
// override fun onDisconnect() {
|
||||
// Log.d(TAG, "Connection disconnected")
|
||||
// setDisconnected(DisconnectCause(DisconnectCause.LOCAL))
|
||||
// destroy()
|
||||
// }
|
||||
// }
|
||||
// connection.setAddress(request.address, TelecomManager.PRESENTATION_ALLOWED)
|
||||
// connection.setRinging()
|
||||
// return connection
|
||||
// }
|
||||
// }
|
@ -1,55 +1,30 @@
|
||||
package com.icing.dialer.services
|
||||
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.net.Uri
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import android.telecom.TelecomManager
|
||||
import android.os.Build
|
||||
import android.util.Log
|
||||
import androidx.core.content.ContextCompat
|
||||
import android.content.pm.PackageManager
|
||||
import android.Manifest
|
||||
|
||||
object CallService {
|
||||
private val TAG = "CallService"
|
||||
|
||||
fun makeGsmCall(context: Context, phoneNumber: String): Boolean {
|
||||
return try {
|
||||
val telecomManager = context.getSystemService(Context.TELECOM_SERVICE) as TelecomManager
|
||||
val uri = Uri.parse("tel:$phoneNumber")
|
||||
|
||||
if (ContextCompat.checkSelfPermission(context, Manifest.permission.CALL_PHONE) == PackageManager.PERMISSION_GRANTED) {
|
||||
telecomManager.placeCall(uri, Bundle())
|
||||
Log.d(TAG, "Initiated call to $phoneNumber")
|
||||
true
|
||||
} else {
|
||||
Log.e(TAG, "CALL_PHONE permission not granted")
|
||||
false
|
||||
}
|
||||
|
||||
fun makeGsmCall(context: Context, phoneNumber: String) {
|
||||
try {
|
||||
val intent = Intent(Intent.ACTION_CALL)
|
||||
intent.data = Uri.parse("tel:$phoneNumber")
|
||||
context.startActivity(intent)
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Error making GSM call: ${e.message}", e)
|
||||
false
|
||||
Log.e("CallService", "Error making GSM call: ${e.message}")
|
||||
}
|
||||
}
|
||||
|
||||
fun hangUpCall(context: Context): Boolean {
|
||||
return try {
|
||||
if (MyInCallService.currentCall != null) {
|
||||
MyInCallService.currentCall?.disconnect()
|
||||
Log.d(TAG, "Disconnected active call via MyInCallService")
|
||||
true
|
||||
} else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||
val telecomManager = context.getSystemService(Context.TELECOM_SERVICE) as TelecomManager
|
||||
telecomManager.endCall()
|
||||
Log.d(TAG, "Ended call via TelecomManager (no active call in MyInCallService)")
|
||||
true
|
||||
} else {
|
||||
Log.e(TAG, "No active call and hangup not supported below Android P")
|
||||
false
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
Log.e(TAG, "Error hanging up call: ${e.message}", e)
|
||||
false
|
||||
fun hangUpCall(context: Context) {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
|
||||
val telecomManager = context.getSystemService(Context.TELECOM_SERVICE) as TelecomManager
|
||||
telecomManager.endCall()
|
||||
} else {
|
||||
Log.e("CallService", "Hangup call is only supported on Android P or later.")
|
||||
}
|
||||
}
|
||||
}
|
@ -1,218 +0,0 @@
|
||||
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.InCallService
|
||||
import android.telecom.CallAudioState
|
||||
import android.util.Log
|
||||
import androidx.core.app.NotificationCompat
|
||||
import com.icing.dialer.activities.MainActivity
|
||||
import io.flutter.plugin.common.MethodChannel
|
||||
|
||||
class MyInCallService : InCallService() {
|
||||
companion object {
|
||||
var channel: MethodChannel? = null
|
||||
var currentCall: Call? = null
|
||||
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() {
|
||||
override fun onStateChanged(call: Call, state: Int) {
|
||||
super.onStateChanged(call, state)
|
||||
val stateStr = when (state) {
|
||||
Call.STATE_DIALING -> "dialing"
|
||||
Call.STATE_ACTIVE -> "active"
|
||||
Call.STATE_DISCONNECTED -> "disconnected"
|
||||
Call.STATE_DISCONNECTING -> "disconnecting"
|
||||
Call.STATE_RINGING -> "ringing"
|
||||
else -> "unknown"
|
||||
}
|
||||
Log.d(TAG, "State changed: $stateStr for call ${call.details.handle}")
|
||||
channel?.invokeMethod("callStateChanged", mapOf(
|
||||
"callId" to call.details.handle.toString(),
|
||||
"state" to stateStr,
|
||||
"wasPhoneLocked" to wasPhoneLocked
|
||||
))
|
||||
if (state == Call.STATE_RINGING) {
|
||||
val keyguardManager = getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager
|
||||
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
|
||||
cancelNotification()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun onCallAdded(call: Call) {
|
||||
super.onCallAdded(call)
|
||||
instance = this
|
||||
currentCall = call
|
||||
val stateStr = when (call.state) {
|
||||
Call.STATE_DIALING -> "dialing"
|
||||
Call.STATE_ACTIVE -> "active"
|
||||
Call.STATE_RINGING -> "ringing"
|
||||
else -> "dialing"
|
||||
}
|
||||
Log.d(TAG, "Call added: ${call.details.handle}, state: $stateStr")
|
||||
channel?.invokeMethod("callAdded", mapOf(
|
||||
"callId" to call.details.handle.toString(),
|
||||
"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)
|
||||
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) {
|
||||
super.onCallRemoved(call)
|
||||
Log.d(TAG, "Call removed: ${call.details.handle}, wasPhoneLocked: $wasPhoneLocked")
|
||||
call.unregisterCallback(callCallback)
|
||||
channel?.invokeMethod("callRemoved", mapOf(
|
||||
"callId" to call.details.handle.toString(),
|
||||
"wasPhoneLocked" to wasPhoneLocked
|
||||
))
|
||||
currentCall = null
|
||||
instance = null
|
||||
cancelNotification()
|
||||
}
|
||||
|
||||
override fun onCallAudioStateChanged(state: CallAudioState) {
|
||||
super.onCallAudioStateChanged(state)
|
||||
Log.d(TAG, "Audio state changed: route=${state.route}, muted=${state.isMuted}")
|
||||
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,14 +0,0 @@
|
||||
class AppConfig {
|
||||
// Private constructor to prevent instantiation
|
||||
AppConfig._();
|
||||
|
||||
// Global configuration
|
||||
static bool isStealthMode = false;
|
||||
|
||||
// App initialization
|
||||
static Future<void> initialize() async {
|
||||
const stealthFlag = String.fromEnvironment('STEALTH', defaultValue: 'false');
|
||||
isStealthMode = stealthFlag.toLowerCase() == 'true';
|
||||
print('Stealth mode is ${isStealthMode ? 'enabled' : 'disabled'}');
|
||||
}
|
||||
}
|
@ -1,57 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../presentation/features/call/call_page.dart';
|
||||
import '../../presentation/features/call/incoming_call_page.dart';
|
||||
import '../../presentation/features/home/home_page.dart';
|
||||
import '../../presentation/features/settings/settings.dart'; // Updated import
|
||||
import '../../presentation/features/contacts/contact_page.dart';
|
||||
import '../../presentation/features/composition/composition.dart';
|
||||
import 'dart:typed_data';
|
||||
|
||||
class AppRouter {
|
||||
static Route<dynamic> generateRoute(RouteSettings settings) {
|
||||
switch (settings.name) {
|
||||
case '/':
|
||||
return MaterialPageRoute(builder: (_) => const MyHomePage());
|
||||
|
||||
case '/settings':
|
||||
return MaterialPageRoute(builder: (_) => const SettingsPage()); // Now correctly imported
|
||||
|
||||
case '/composition':
|
||||
return MaterialPageRoute(builder: (_) => const CompositionPage());
|
||||
|
||||
case '/contacts':
|
||||
return MaterialPageRoute(builder: (_) => const ContactPage());
|
||||
|
||||
case '/call':
|
||||
final args = settings.arguments as Map<String, dynamic>;
|
||||
return MaterialPageRoute(
|
||||
settings: settings,
|
||||
builder: (_) => CallPage(
|
||||
displayName: args['displayName'] as String,
|
||||
phoneNumber: args['phoneNumber'] as String,
|
||||
thumbnail: args['thumbnail'] as Uint8List?,
|
||||
),
|
||||
);
|
||||
|
||||
case '/incoming_call':
|
||||
final args = settings.arguments as Map<String, dynamic>;
|
||||
return MaterialPageRoute(
|
||||
settings: settings,
|
||||
builder: (_) => IncomingCallPage(
|
||||
displayName: args['displayName'] as String,
|
||||
phoneNumber: args['phoneNumber'] as String,
|
||||
thumbnail: args['thumbnail'] as Uint8List?,
|
||||
),
|
||||
);
|
||||
|
||||
default:
|
||||
return MaterialPageRoute(
|
||||
builder: (_) => Scaffold(
|
||||
body: Center(
|
||||
child: Text('No route defined for ${settings.name}'),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,78 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
/// Service for managing blocked phone numbers
|
||||
class BlockService {
|
||||
static const String _blockedNumbersKey = 'blocked_numbers';
|
||||
|
||||
// Private constructor
|
||||
BlockService._privateConstructor();
|
||||
|
||||
// Singleton instance
|
||||
static final BlockService _instance = BlockService._privateConstructor();
|
||||
|
||||
// Factory constructor to return the same instance
|
||||
factory BlockService() {
|
||||
return _instance;
|
||||
}
|
||||
|
||||
/// Block a phone number
|
||||
Future<bool> blockNumber(String phoneNumber) async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final blockedNumbers = prefs.getStringList(_blockedNumbersKey) ?? [];
|
||||
|
||||
// Don't add if already blocked
|
||||
if (blockedNumbers.contains(phoneNumber)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
blockedNumbers.add(phoneNumber);
|
||||
return await prefs.setStringList(_blockedNumbersKey, blockedNumbers);
|
||||
} catch (e) {
|
||||
debugPrint('Error blocking number: $e');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// Unblock a phone number
|
||||
Future<bool> unblockNumber(String phoneNumber) async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final blockedNumbers = prefs.getStringList(_blockedNumbersKey) ?? [];
|
||||
|
||||
if (!blockedNumbers.contains(phoneNumber)) {
|
||||
return true;
|
||||
}
|
||||
|
||||
blockedNumbers.remove(phoneNumber);
|
||||
return await prefs.setStringList(_blockedNumbersKey, blockedNumbers);
|
||||
} catch (e) {
|
||||
debugPrint('Error unblocking number: $e');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// Check if a number is blocked
|
||||
Future<bool> isNumberBlocked(String phoneNumber) async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
final blockedNumbers = prefs.getStringList(_blockedNumbersKey) ?? [];
|
||||
return blockedNumbers.contains(phoneNumber);
|
||||
} catch (e) {
|
||||
debugPrint('Error checking if number is blocked: $e');
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/// Get all blocked numbers
|
||||
Future<List<String>> getBlockedNumbers() async {
|
||||
try {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
return prefs.getStringList(_blockedNumbersKey) ?? [];
|
||||
} catch (e) {
|
||||
debugPrint('Error getting blocked numbers: $e');
|
||||
return [];
|
||||
}
|
||||
}
|
||||
}
|
@ -1,442 +0,0 @@
|
||||
import 'dart:async';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import '../../presentation/features/call/call_page.dart';
|
||||
import '../../presentation/features/call/incoming_call_page.dart'; // Import the new page
|
||||
import 'contact_service.dart';
|
||||
|
||||
class CallService {
|
||||
static const MethodChannel _channel = MethodChannel('call_service');
|
||||
static String? currentPhoneNumber;
|
||||
static String? currentDisplayName;
|
||||
static Uint8List? currentThumbnail;
|
||||
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>();
|
||||
|
||||
Stream<String> get callStateStream => _callStateController.stream;
|
||||
Stream<Map<String, dynamic>> get audioStateStream => _audioStateController.stream;
|
||||
Map<String, dynamic>? get currentAudioState => _currentAudioState;
|
||||
|
||||
CallService() {
|
||||
_channel.setMethodCallHandler((call) async {
|
||||
print('CallService: Handling method call: ${call.method}, with args: ${call.arguments}');
|
||||
switch (call.method) {
|
||||
case "callAdded":
|
||||
final phoneNumber = call.arguments["callId"] as String?;
|
||||
final state = call.arguments["state"] as String?;
|
||||
if (phoneNumber == null || state == null) {
|
||||
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") {
|
||||
_handleIncomingCall(decodedPhoneNumber);
|
||||
} else {
|
||||
_navigateToCallPage();
|
||||
}
|
||||
break;
|
||||
case "callStateChanged":
|
||||
final state = call.arguments["state"] as String?;
|
||||
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") {
|
||||
_closeCallPage();
|
||||
if (wasPhoneLocked) {
|
||||
await _channel.invokeMethod("callEndedFromFlutter");
|
||||
}
|
||||
_activeCallNumber = null;
|
||||
} else if (state == "active" || state == "dialing") {
|
||||
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") {
|
||||
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;
|
||||
case "callEnded":
|
||||
case "callRemoved":
|
||||
wasPhoneLocked = call.arguments["wasPhoneLocked"] as bool? ?? false;
|
||||
print('CallService: Call ended/removed, wasPhoneLocked: $wasPhoneLocked');
|
||||
_closeCallPage();
|
||||
if (wasPhoneLocked) {
|
||||
await _channel.invokeMethod("callEndedFromFlutter");
|
||||
}
|
||||
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;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
Future<String?> getCallState() async {
|
||||
try {
|
||||
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;
|
||||
}
|
||||
_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(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
settings: const RouteSettings(name: '/call'),
|
||||
builder: (context) => CallPage(
|
||||
displayName: currentDisplayName ?? currentPhoneNumber!,
|
||||
phoneNumber: currentPhoneNumber!,
|
||||
thumbnail: currentThumbnail,
|
||||
),
|
||||
),
|
||||
).then((_) {
|
||||
_isCallPageVisible = false;
|
||||
_isNavigating = false;
|
||||
print('CallService: CallPage popped, _isCallPageVisible set to false');
|
||||
});
|
||||
_isCallPageVisible = true;
|
||||
}
|
||||
|
||||
void _navigateToIncomingCallPage(BuildContext context) {
|
||||
if (_isNavigating) {
|
||||
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;
|
||||
}
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
settings: const RouteSettings(name: '/incoming_call'),
|
||||
builder: (context) => IncomingCallPage(
|
||||
displayName: currentDisplayName ?? currentPhoneNumber!,
|
||||
phoneNumber: currentPhoneNumber!,
|
||||
thumbnail: currentThumbnail,
|
||||
),
|
||||
),
|
||||
).then((_) {
|
||||
_isCallPageVisible = false;
|
||||
_isNavigating = false;
|
||||
print('CallService: IncomingCallPage popped, _isCallPageVisible set to false');
|
||||
});
|
||||
_isCallPageVisible = true;
|
||||
}
|
||||
|
||||
void _closeCallPage() {
|
||||
final context = navigatorKey.currentContext;
|
||||
if (context == null) {
|
||||
print('CallService: Cannot close page, context is null');
|
||||
return;
|
||||
}
|
||||
print('CallService: Closing call page, _isCallPageVisible: $_isCallPageVisible');
|
||||
if (Navigator.canPop(context)) {
|
||||
print('CallService: Popping call page');
|
||||
Navigator.pop(context);
|
||||
_isCallPageVisible = false;
|
||||
} else {
|
||||
print('CallService: No page to pop');
|
||||
}
|
||||
_activeCallNumber = null;
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>> makeGsmCall(
|
||||
BuildContext context, {
|
||||
required String phoneNumber,
|
||||
String? displayName,
|
||||
Uint8List? thumbnail,
|
||||
}) async {
|
||||
try {
|
||||
if (_activeCallNumber == phoneNumber && _isCallPageVisible) {
|
||||
print('CallService: Call already active for $phoneNumber, skipping');
|
||||
return {"status": "already_active", "message": "Call already in progress"};
|
||||
}
|
||||
currentPhoneNumber = 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});
|
||||
print('CallService: makeGsmCall result: $result');
|
||||
final resultMap = Map<String, dynamic>.from(result as Map);
|
||||
if (resultMap["status"] != "calling") {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text("Failed to initiate call")),
|
||||
);
|
||||
}
|
||||
return resultMap;
|
||||
} catch (e) {
|
||||
print("CallService: Error making call: $e");
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text("Error making call: $e")),
|
||||
);
|
||||
return {"status": "error", "message": e.toString()};
|
||||
}
|
||||
}
|
||||
|
||||
Future<Map<String, dynamic>> hangUpCall(BuildContext context) async {
|
||||
try {
|
||||
print('CallService: Hanging up call');
|
||||
final result = await _channel.invokeMethod('hangUpCall');
|
||||
print('CallService: hangUpCall result: $result');
|
||||
final resultMap = Map<String, dynamic>.from(result as Map);
|
||||
if (resultMap["status"] != "ended") {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text("Failed to end call")),
|
||||
);
|
||||
}
|
||||
return resultMap;
|
||||
} catch (e) {
|
||||
print("CallService: Error hanging up call: $e");
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text("Error hanging up call: $e")),
|
||||
);
|
||||
return {"status": "error", "message": e.toString()};
|
||||
}
|
||||
}
|
||||
}
|
@ -1,86 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_contacts/flutter_contacts.dart';
|
||||
import 'package:qr_flutter/qr_flutter.dart';
|
||||
|
||||
class ContactService {
|
||||
Future<List<Contact>> fetchContacts() async {
|
||||
if (await FlutterContacts.requestPermission()) {
|
||||
List<Contact> contacts = await FlutterContacts.getContacts(
|
||||
withProperties: true,
|
||||
withThumbnail: true,
|
||||
);
|
||||
return contacts;
|
||||
} else {
|
||||
// Permission denied
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
Future<List<Contact>> fetchFavoriteContacts() async {
|
||||
if (await FlutterContacts.requestPermission()) {
|
||||
// Get all contacts and filter for favorites
|
||||
List<Contact> allContacts = await FlutterContacts.getContacts(
|
||||
withProperties: true,
|
||||
withThumbnail: true,
|
||||
);
|
||||
return allContacts.where((c) => c.isStarred).toList();
|
||||
} else {
|
||||
// Permission denied
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
Future<Contact?> addNewContact(Contact contact) async {
|
||||
if (await FlutterContacts.requestPermission()) {
|
||||
try {
|
||||
return await FlutterContacts.insertContact(contact);
|
||||
} catch (e) {
|
||||
debugPrint('Error adding contact: $e');
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
void showContactQRCodeDialog(BuildContext context, Contact contact) {
|
||||
final String vCard = contact.toVCard();
|
||||
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (BuildContext context) {
|
||||
return AlertDialog(
|
||||
backgroundColor: Colors.grey[900],
|
||||
title: Text(
|
||||
'QR Code for ${contact.displayName}',
|
||||
style: const TextStyle(color: Colors.white),
|
||||
),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Container(
|
||||
color: Colors.white,
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: QrImageView(
|
||||
data: vCard,
|
||||
version: QrVersions.auto,
|
||||
size: 200.0,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16.0),
|
||||
const Text(
|
||||
'Scan this code to add this contact',
|
||||
style: TextStyle(color: Colors.white70),
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(),
|
||||
child: const Text('Close'),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
@ -1,57 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:mobile_scanner/mobile_scanner.dart';
|
||||
|
||||
class QRCodeScannerScreen extends StatefulWidget {
|
||||
const QRCodeScannerScreen({super.key});
|
||||
|
||||
@override
|
||||
_QRCodeScannerScreenState createState() => _QRCodeScannerScreenState();
|
||||
}
|
||||
|
||||
class _QRCodeScannerScreenState extends State<QRCodeScannerScreen> {
|
||||
MobileScannerController cameraController = MobileScannerController();
|
||||
bool _flashEnabled = false;
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
cameraController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
title: const Text('Scan QR Code'),
|
||||
actions: [
|
||||
IconButton(
|
||||
icon: Icon(_flashEnabled ? Icons.flash_on : Icons.flash_off),
|
||||
onPressed: () {
|
||||
setState(() {
|
||||
_flashEnabled = !_flashEnabled;
|
||||
cameraController.toggleTorch();
|
||||
});
|
||||
},
|
||||
),
|
||||
IconButton(
|
||||
icon: const Icon(Icons.flip_camera_ios),
|
||||
onPressed: () => cameraController.switchCamera(),
|
||||
),
|
||||
],
|
||||
),
|
||||
body: MobileScanner(
|
||||
controller: cameraController,
|
||||
onDetect: (capture) {
|
||||
final List<Barcode> barcodes = capture.barcodes;
|
||||
if (barcodes.isNotEmpty) {
|
||||
// Return the first barcode value
|
||||
final String? code = barcodes.first.rawValue;
|
||||
if (code != null) {
|
||||
Navigator.pop(context, code);
|
||||
}
|
||||
}
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
322
dialer/lib/features/about/about.dart
Normal file
322
dialer/lib/features/about/about.dart
Normal file
@ -0,0 +1,322 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:package_info_plus/package_info_plus.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
|
||||
class AboutPage extends StatefulWidget {
|
||||
const AboutPage({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
_AboutPageState createState() => _AboutPageState();
|
||||
}
|
||||
|
||||
class _AboutPageState extends State<AboutPage> {
|
||||
String _version = '';
|
||||
String _buildNumber = '';
|
||||
bool _isLoading = true;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_loadPackageInfo();
|
||||
}
|
||||
|
||||
Future<void> _loadPackageInfo() async {
|
||||
final packageInfo = await PackageInfo.fromPlatform();
|
||||
setState(() {
|
||||
_version = packageInfo.version;
|
||||
_buildNumber = packageInfo.buildNumber;
|
||||
_isLoading = false;
|
||||
});
|
||||
}
|
||||
|
||||
Future<void> _launchURL(String url) async {
|
||||
final uri = Uri.parse(url);
|
||||
if (await canLaunchUrl(uri)) {
|
||||
await launchUrl(uri, mode: LaunchMode.externalApplication);
|
||||
} else {
|
||||
throw 'Could not launch $url';
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.black,
|
||||
appBar: AppBar(
|
||||
title: const Text('About'),
|
||||
backgroundColor: Colors.black,
|
||||
elevation: 0,
|
||||
),
|
||||
body: _isLoading
|
||||
? const Center(child: CircularProgressIndicator())
|
||||
: SingleChildScrollView(
|
||||
padding: const EdgeInsets.all(20.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// App Logo and Name
|
||||
Center(
|
||||
child: Column(
|
||||
children: [
|
||||
Container(
|
||||
width: 100,
|
||||
height: 100,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.blue,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.phone,
|
||||
size: 60,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const Text(
|
||||
'Icing Dialer',
|
||||
style: TextStyle(
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'Version $_version (Build $_buildNumber)',
|
||||
style: TextStyle(
|
||||
color: Colors.grey[400],
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
// App Description
|
||||
const _SectionHeader(title: 'About This App'),
|
||||
const _ContentCard(
|
||||
child: Text(
|
||||
'Icing Dialer is a secure and privacy-focused phone dialer application '
|
||||
'designed to provide a modern, user-friendly interface for making calls, '
|
||||
'managing contacts, and accessing your call history. With features like '
|
||||
'contact obfuscation and encryption protocols, your communication remains '
|
||||
'private and secure.',
|
||||
style: TextStyle(color: Colors.white, height: 1.5),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
// Features
|
||||
const _SectionHeader(title: 'Key Features'),
|
||||
const _ContentCard(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
_FeatureItem(
|
||||
icon: Icons.phone,
|
||||
title: 'Intuitive Calling Interface',
|
||||
description: 'Easy-to-use dialer with quick access to frequent contacts.',
|
||||
),
|
||||
SizedBox(height: 16),
|
||||
_FeatureItem(
|
||||
icon: Icons.lock,
|
||||
title: 'Secure Communications',
|
||||
description: 'End-to-end encrypted calls with the Icing protocol.',
|
||||
),
|
||||
SizedBox(height: 16),
|
||||
_FeatureItem(
|
||||
icon: Icons.history,
|
||||
title: 'Comprehensive Call History',
|
||||
description: 'View, filter, and manage your recent calls.',
|
||||
),
|
||||
SizedBox(height: 16),
|
||||
_FeatureItem(
|
||||
icon: Icons.contacts,
|
||||
title: 'Advanced Contact Management',
|
||||
description: 'Organize, share, and secure your contacts.',
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
// Support & Privacy
|
||||
const _SectionHeader(title: 'Support & Privacy'),
|
||||
_ContentCard(
|
||||
child: Column(
|
||||
children: [
|
||||
_LinkButton(
|
||||
icon: Icons.help_outline,
|
||||
text: 'Help & Support',
|
||||
onTap: () => _launchURL('https://icing.org/support'),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
_LinkButton(
|
||||
icon: Icons.privacy_tip_outlined,
|
||||
text: 'Privacy Policy',
|
||||
onTap: () => _launchURL('https://icing.org/privacy'),
|
||||
),
|
||||
const SizedBox(height: 12),
|
||||
_LinkButton(
|
||||
icon: Icons.description_outlined,
|
||||
text: 'Terms of Service',
|
||||
onTap: () => _launchURL('https://icing.org/terms'),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
// Credits
|
||||
const _SectionHeader(title: 'Credits'),
|
||||
const _ContentCard(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'Developed by Icing Team',
|
||||
style: TextStyle(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
SizedBox(height: 8),
|
||||
Text(
|
||||
'This application utilizes open-source libraries and follows best '
|
||||
'practices for secure software development.',
|
||||
style: TextStyle(color: Colors.white70, height: 1.5),
|
||||
),
|
||||
SizedBox(height: 12),
|
||||
Text(
|
||||
'© 2025 Icing Organization. All Rights Reserved.',
|
||||
style: TextStyle(color: Colors.grey, fontSize: 12),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 40),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _SectionHeader extends StatelessWidget {
|
||||
final String title;
|
||||
|
||||
const _SectionHeader({Key? key, required this.title}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.only(bottom: 8.0),
|
||||
child: Text(
|
||||
title,
|
||||
style: const TextStyle(
|
||||
color: Colors.blue,
|
||||
fontSize: 18,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _ContentCard extends StatelessWidget {
|
||||
final Widget child;
|
||||
|
||||
const _ContentCard({Key? key, required this.child}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey[900],
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: child,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _FeatureItem extends StatelessWidget {
|
||||
final IconData icon;
|
||||
final String title;
|
||||
final String description;
|
||||
|
||||
const _FeatureItem({
|
||||
Key? key,
|
||||
required this.icon,
|
||||
required this.title,
|
||||
required this.description,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Row(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Icon(icon, color: Colors.blue, size: 20),
|
||||
const SizedBox(width: 12),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
title,
|
||||
style: const TextStyle(
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
description,
|
||||
style: const TextStyle(color: Colors.white70),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class _LinkButton extends StatelessWidget {
|
||||
final IconData icon;
|
||||
final String text;
|
||||
final VoidCallback onTap;
|
||||
|
||||
const _LinkButton({
|
||||
Key? key,
|
||||
required this.icon,
|
||||
required this.text,
|
||||
required this.onTap,
|
||||
}) : super(key: key);
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return InkWell(
|
||||
onTap: onTap,
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||
child: Row(
|
||||
children: [
|
||||
Icon(icon, color: Colors.blue),
|
||||
const SizedBox(width: 16),
|
||||
Text(
|
||||
text,
|
||||
style: const TextStyle(color: Colors.white),
|
||||
),
|
||||
const Spacer(),
|
||||
const Icon(
|
||||
Icons.arrow_forward_ios,
|
||||
color: Colors.white70,
|
||||
size: 16,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
332
dialer/lib/features/call/call_page.dart
Normal file
332
dialer/lib/features/call/call_page.dart
Normal file
@ -0,0 +1,332 @@
|
||||
import 'dart:typed_data';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:dialer/services/obfuscate_service.dart';
|
||||
import 'package:dialer/widgets/username_color_generator.dart';
|
||||
|
||||
class CallPage extends StatefulWidget {
|
||||
final String displayName;
|
||||
final Uint8List? thumbnail;
|
||||
|
||||
const CallPage({super.key, required this.displayName, this.thumbnail});
|
||||
|
||||
@override
|
||||
_CallPageState createState() => _CallPageState();
|
||||
}
|
||||
|
||||
class _CallPageState extends State<CallPage> {
|
||||
final ObfuscateService _obfuscateService = ObfuscateService();
|
||||
bool isMuted = false;
|
||||
bool isSpeakerOn = false;
|
||||
bool isKeypadVisible = false;
|
||||
bool icingProtocolOk = true;
|
||||
String _typedDigits = ""; // New state variable for pressed digits
|
||||
|
||||
void _addDigit(String digit) {
|
||||
setState(() {
|
||||
_typedDigits += digit;
|
||||
});
|
||||
}
|
||||
|
||||
void _toggleMute() {
|
||||
setState(() {
|
||||
isMuted = !isMuted;
|
||||
});
|
||||
}
|
||||
|
||||
void _toggleSpeaker() {
|
||||
setState(() {
|
||||
isSpeakerOn = !isSpeakerOn;
|
||||
});
|
||||
}
|
||||
|
||||
void _toggleKeypad() {
|
||||
setState(() {
|
||||
isKeypadVisible = !isKeypadVisible;
|
||||
});
|
||||
}
|
||||
|
||||
void _toggleIcingProtocol() {
|
||||
setState(() {
|
||||
icingProtocolOk = !icingProtocolOk;
|
||||
});
|
||||
}
|
||||
|
||||
void _hangUp() {
|
||||
Navigator.pop(context);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final double avatarRadius = isKeypadVisible ? 45.0 : 45.0; // Smaller avatar
|
||||
final double nameFontSize = isKeypadVisible ? 24.0 : 24.0; // Smaller font
|
||||
final double statusFontSize = isKeypadVisible ? 16.0 : 16.0; // Smaller status
|
||||
|
||||
return Scaffold(
|
||||
body: Container(
|
||||
color: Colors.black,
|
||||
child: SafeArea(
|
||||
child: Column(
|
||||
children: [
|
||||
// Top section - make it more compact
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
SizedBox(height: 35),
|
||||
ObfuscatedAvatar(
|
||||
imageBytes: widget.thumbnail,
|
||||
radius: avatarRadius,
|
||||
backgroundColor: generateColorFromName(widget.displayName),
|
||||
fallbackInitial: widget.displayName,
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
icingProtocolOk ? Icons.lock : Icons.lock_open,
|
||||
color: icingProtocolOk ? Colors.green : Colors.red,
|
||||
size: 16,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
'Icing protocol: ${icingProtocolOk ? "ok" : "ko"}',
|
||||
style: TextStyle(
|
||||
color: icingProtocolOk ? Colors.green : Colors.red,
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
_obfuscateService.obfuscateData(widget.displayName),
|
||||
style: TextStyle(
|
||||
fontSize: nameFontSize,
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
'Calling...',
|
||||
style: TextStyle(fontSize: statusFontSize, color: Colors.white70),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// Middle section - make it flexible and scrollable if needed
|
||||
Expanded(
|
||||
child: Column(
|
||||
children: [
|
||||
if (isKeypadVisible) ...[
|
||||
// Add spacer to push keypad down
|
||||
const Spacer(flex: 2),
|
||||
|
||||
// Typed digits display
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20.0),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
_typedDigits,
|
||||
maxLines: 1,
|
||||
textAlign: TextAlign.right,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(
|
||||
fontSize: 24,
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
padding: EdgeInsets.zero,
|
||||
onPressed: _toggleKeypad,
|
||||
icon: const Icon(Icons.close, color: Colors.white),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// Keypad grid
|
||||
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),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
),
|
||||
|
||||
// Add spacer after keypad
|
||||
const Spacer(flex: 1),
|
||||
] else ...[
|
||||
const Spacer(),
|
||||
// Control buttons
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 32.0),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// Main control buttons
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
// Mute
|
||||
Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
IconButton(
|
||||
onPressed: _toggleMute,
|
||||
icon: Icon(
|
||||
isMuted ? Icons.mic_off : Icons.mic,
|
||||
color: Colors.white,
|
||||
size: 32,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
isMuted ? 'Unmute' : 'Mute',
|
||||
style: const TextStyle(color: Colors.white, fontSize: 14),
|
||||
),
|
||||
],
|
||||
),
|
||||
// Keypad
|
||||
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),
|
||||
),
|
||||
],
|
||||
),
|
||||
// Speaker
|
||||
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),
|
||||
// Additional buttons
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
// Add Contact
|
||||
Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
// ...existing code...
|
||||
},
|
||||
icon: const Icon(Icons.person_add, color: Colors.white, size: 32),
|
||||
),
|
||||
const Text('Add Contact',
|
||||
style: TextStyle(color: Colors.white, fontSize: 14)),
|
||||
],
|
||||
),
|
||||
// Change SIM
|
||||
Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
IconButton(
|
||||
onPressed: () {
|
||||
// ...existing code...
|
||||
},
|
||||
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),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
// Bottom section - hang up button
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 16.0),
|
||||
child: GestureDetector(
|
||||
onTap: _hangUp,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: const BoxDecoration(
|
||||
color: Colors.red,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.call_end,
|
||||
color: Colors.white,
|
||||
size: 32,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
296
dialer/lib/features/composition/composition.dart
Normal file
296
dialer/lib/features/composition/composition.dart
Normal file
@ -0,0 +1,296 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_contacts/flutter_contacts.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
import '../../services/contact_service.dart';
|
||||
import '../../services/obfuscate_service.dart'; // Import ObfuscateService
|
||||
import '../../services/call_service.dart'; // Import the CallService
|
||||
import '../contacts/widgets/add_contact_button.dart';
|
||||
|
||||
class CompositionPage extends StatefulWidget {
|
||||
const CompositionPage({super.key});
|
||||
|
||||
@override
|
||||
_CompositionPageState createState() => _CompositionPageState();
|
||||
}
|
||||
|
||||
class _CompositionPageState extends State<CompositionPage> {
|
||||
String dialedNumber = "";
|
||||
List<Contact> _allContacts = [];
|
||||
List<Contact> _filteredContacts = [];
|
||||
final ContactService _contactService = ContactService();
|
||||
|
||||
// Instantiate the ObfuscateService
|
||||
final ObfuscateService _obfuscateService = ObfuscateService();
|
||||
|
||||
// Instantiate the CallService
|
||||
final CallService _callService = CallService();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_fetchContacts();
|
||||
}
|
||||
|
||||
Future<void> _fetchContacts() async {
|
||||
_allContacts = await _contactService.fetchContacts();
|
||||
_filteredContacts = _allContacts;
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
void _filterContacts() {
|
||||
setState(() {
|
||||
_filteredContacts = _allContacts.where((contact) {
|
||||
final phoneMatch = contact.phones.any((phone) =>
|
||||
phone.number.replaceAll(RegExp(r'\D'), '').contains(dialedNumber));
|
||||
final nameMatch = contact.displayName
|
||||
.toLowerCase()
|
||||
.contains(dialedNumber.toLowerCase());
|
||||
return phoneMatch || nameMatch;
|
||||
}).toList();
|
||||
});
|
||||
}
|
||||
|
||||
void _onNumberPress(String number) {
|
||||
setState(() {
|
||||
dialedNumber += number;
|
||||
_filterContacts();
|
||||
});
|
||||
}
|
||||
|
||||
void _onDeletePress() {
|
||||
setState(() {
|
||||
if (dialedNumber.isNotEmpty) {
|
||||
dialedNumber = dialedNumber.substring(0, dialedNumber.length - 1);
|
||||
_filterContacts();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void _onClearPress() {
|
||||
setState(() {
|
||||
dialedNumber = "";
|
||||
_filteredContacts = _allContacts;
|
||||
});
|
||||
}
|
||||
|
||||
// Function to call a contact's number using the CallService
|
||||
void _makeCall(String phoneNumber) async {
|
||||
try {
|
||||
await _callService.makeGsmCall(phoneNumber);
|
||||
setState(() {
|
||||
dialedNumber = phoneNumber;
|
||||
});
|
||||
} catch (e) {
|
||||
debugPrint("Error making call: $e");
|
||||
}
|
||||
}
|
||||
|
||||
// Function to send an SMS to a contact's number
|
||||
void _launchSms(String phoneNumber) async {
|
||||
final uri = Uri(scheme: 'sms', path: phoneNumber);
|
||||
if (await canLaunchUrl(uri)) {
|
||||
await launchUrl(uri);
|
||||
} else {
|
||||
debugPrint('Could not send SMS to $phoneNumber');
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.black,
|
||||
body: Stack(
|
||||
children: [
|
||||
Column(
|
||||
children: [
|
||||
// Top half: Display contacts matching dialed number
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.only(
|
||||
top: 42.0, left: 16.0, right: 16.0, bottom: 16.0),
|
||||
color: Colors.black,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(
|
||||
child: ListView(
|
||||
children: _filteredContacts.isNotEmpty
|
||||
? _filteredContacts.map((contact) {
|
||||
final phoneNumber = contact.phones.isNotEmpty
|
||||
? contact.phones.first.number
|
||||
: 'No phone number';
|
||||
return ListTile(
|
||||
title: Text(
|
||||
_obfuscateService.obfuscateData(contact.displayName),
|
||||
style: const TextStyle(color: Colors.white),
|
||||
),
|
||||
subtitle: Text(
|
||||
_obfuscateService.obfuscateData(phoneNumber),
|
||||
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(
|
||||
flex: 2,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: [
|
||||
// Display dialed number with erase button
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Align(
|
||||
alignment: Alignment.center,
|
||||
child: Text(
|
||||
dialedNumber,
|
||||
style: const TextStyle(
|
||||
fontSize: 24, color: Colors.white),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
),
|
||||
IconButton(
|
||||
onPressed: _onClearPress,
|
||||
icon: const Icon(Icons.backspace,
|
||||
color: Colors.white),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
|
||||
// Dialpad
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment:
|
||||
MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
_buildDialButton('1'),
|
||||
_buildDialButton('2'),
|
||||
_buildDialButton('3'),
|
||||
],
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment:
|
||||
MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
_buildDialButton('4'),
|
||||
_buildDialButton('5'),
|
||||
_buildDialButton('6'),
|
||||
],
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment:
|
||||
MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
_buildDialButton('7'),
|
||||
_buildDialButton('8'),
|
||||
_buildDialButton('9'),
|
||||
],
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment:
|
||||
MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
_buildDialButton('*'),
|
||||
_buildDialButton('0'),
|
||||
_buildDialButton('#'),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
||||
// Add Contact Button
|
||||
Positioned(
|
||||
bottom: 20.0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: Center(
|
||||
child: AddContactButton(),
|
||||
),
|
||||
),
|
||||
|
||||
// Top Row with Back Arrow
|
||||
Positioned(
|
||||
top: 40.0,
|
||||
left: 16.0,
|
||||
child: IconButton(
|
||||
icon: const Icon(Icons.arrow_back, color: Colors.white),
|
||||
onPressed: () {
|
||||
Navigator.pop(context);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDialButton(String number) {
|
||||
return 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,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
26
dialer/lib/features/contacts/contact_page.dart
Normal file
26
dialer/lib/features/contacts/contact_page.dart
Normal file
@ -0,0 +1,26 @@
|
||||
import 'package:dialer/features/contacts/contact_state.dart';
|
||||
import 'package:dialer/features/contacts/widgets/alphabet_scroll_page.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:dialer/widgets/loading_indicator.dart';
|
||||
|
||||
class ContactPage extends StatefulWidget {
|
||||
const ContactPage({super.key});
|
||||
|
||||
@override
|
||||
_ContactPageState createState() => _ContactPageState();
|
||||
}
|
||||
|
||||
class _ContactPageState extends State<ContactPage> {
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final contactState = ContactState.of(context);
|
||||
return Scaffold(
|
||||
body: contactState.loading
|
||||
? const LoadingIndicatorWidget()
|
||||
: AlphabetScrollPage(
|
||||
scrollOffset: contactState.scrollOffset,
|
||||
contacts: contactState.contacts, // Use all contacts here
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -1,6 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_contacts/flutter_contacts.dart';
|
||||
import '../../../domain/services/contact_service.dart';
|
||||
import '../../services/contact_service.dart';
|
||||
|
||||
class ContactState extends StatefulWidget {
|
||||
final Widget child;
|
||||
@ -23,7 +23,7 @@ class _ContactStateState extends State<ContactState> {
|
||||
List<Contact> _favoriteContacts = [];
|
||||
bool _loading = true;
|
||||
double _scrollOffset = 0.0;
|
||||
Contact? _selfContact;
|
||||
Contact? _selfContact = Contact();
|
||||
|
||||
// Getters for all contacts and favorites
|
||||
List<Contact> get contacts => _allContacts;
|
||||
@ -35,23 +35,11 @@ class _ContactStateState extends State<ContactState> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_initializeContacts(); // Rename to make it clear this is initialization
|
||||
fetchContacts(); // Fetch all contacts by default
|
||||
FlutterContacts.addListener(_onContactChange);
|
||||
}
|
||||
|
||||
// Private method to initialize contacts without setState during build
|
||||
Future<void> _initializeContacts() async {
|
||||
try {
|
||||
List<Contact> contacts = await _contactService.fetchContacts();
|
||||
_processContactsInitial(contacts);
|
||||
} catch (e) {
|
||||
debugPrint('Error fetching contacts: $e');
|
||||
}
|
||||
}
|
||||
|
||||
void _onContactChange() async {
|
||||
await fetchContacts();
|
||||
}
|
||||
void _onContactChange() => fetchContacts();
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
@ -59,92 +47,47 @@ class _ContactStateState extends State<ContactState> {
|
||||
super.dispose();
|
||||
}
|
||||
|
||||
// Fetch all contacts - public method that can be called after build
|
||||
// Fetch all contacts
|
||||
Future<void> fetchContacts() async {
|
||||
if (!mounted) return;
|
||||
|
||||
setState(() => _loading = true);
|
||||
try {
|
||||
List<Contact> contacts = await _contactService.fetchContacts();
|
||||
if (mounted) {
|
||||
_processContacts(contacts);
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('Error fetching contacts: $e');
|
||||
_processContacts(contacts);
|
||||
} finally {
|
||||
if (mounted) {
|
||||
setState(() => _loading = false);
|
||||
}
|
||||
setState(() => _loading = false);
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch only favorite contacts
|
||||
Future<void> fetchFavoriteContacts() async {
|
||||
if (!mounted) return;
|
||||
|
||||
setState(() => _loading = true);
|
||||
try {
|
||||
List<Contact> contacts = await _contactService.fetchFavoriteContacts();
|
||||
if (mounted) {
|
||||
setState(() => _favoriteContacts = contacts);
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint('Error fetching favorite contacts: $e');
|
||||
setState(() => _favoriteContacts = contacts);
|
||||
} finally {
|
||||
if (mounted) {
|
||||
setState(() => _loading = false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Process contacts without setState for initial loading
|
||||
void _processContactsInitial(List<Contact> contacts) {
|
||||
if (!mounted) return;
|
||||
|
||||
// Optimize by doing a single pass through contacts instead of multiple iterations
|
||||
final filteredContacts = contacts.where((contact) => contact.phones.isNotEmpty).toList()
|
||||
..sort((a, b) => a.displayName.compareTo(b.displayName));
|
||||
|
||||
_selfContact = contacts.firstWhere(
|
||||
(contact) => contact.displayName.toLowerCase() == "user",
|
||||
orElse: () => Contact(),
|
||||
);
|
||||
|
||||
if (_selfContact!.phones.isEmpty) {
|
||||
_selfContact = null;
|
||||
}
|
||||
|
||||
_allContacts = filteredContacts;
|
||||
_favoriteContacts = filteredContacts.where((contact) => contact.isStarred).toList();
|
||||
_loading = false;
|
||||
|
||||
// Force a rebuild after initialization is complete
|
||||
if (mounted) {
|
||||
WidgetsBinding.instance.addPostFrameCallback((_) {
|
||||
setState(() {});
|
||||
});
|
||||
setState(() => _loading = false);
|
||||
}
|
||||
}
|
||||
|
||||
void _processContacts(List<Contact> contacts) {
|
||||
if (!mounted) return;
|
||||
|
||||
// Same optimization as above
|
||||
final filteredContacts = contacts.where((contact) => contact.phones.isNotEmpty).toList()
|
||||
..sort((a, b) => a.displayName.compareTo(b.displayName));
|
||||
|
||||
_selfContact = contacts.firstWhere(
|
||||
(contact) => contact.displayName.toLowerCase() == "user",
|
||||
orElse: () => Contact(),
|
||||
);
|
||||
|
||||
if (_selfContact!.phones.isEmpty) {
|
||||
debugPrint("Self contact has no phone numbers");
|
||||
_selfContact = null;
|
||||
}
|
||||
|
||||
contacts = contacts.where((contact) => contact.phones.isNotEmpty).toList();
|
||||
contacts.sort((a, b) => a.displayName.compareTo(b.displayName));
|
||||
|
||||
setState(() {
|
||||
_allContacts = filteredContacts;
|
||||
_favoriteContacts = filteredContacts.where((contact) => contact.isStarred).toList();
|
||||
_allContacts = contacts;
|
||||
_favoriteContacts =
|
||||
contacts.where((contact) => contact.isStarred).toList();
|
||||
_selfContact = _selfContact;
|
||||
});
|
||||
}
|
||||
|
||||
@ -159,6 +102,25 @@ class _ContactStateState extends State<ContactState> {
|
||||
});
|
||||
}
|
||||
|
||||
bool doesContactExist(Contact contact) {
|
||||
// Example: consider it "existing" if there's a matching phone number
|
||||
for (final existing in _allContacts) {
|
||||
if (existing.toVCard() == contact.toVCard()) {
|
||||
return true;
|
||||
}
|
||||
// for (final phone in existing.phones) {
|
||||
// for (final newPhone in contact.phones) {
|
||||
// // Simple exact match; you can do more advanced logic
|
||||
// if (phone.normalizedNumber == newPhone.normalizedNumber) {
|
||||
// return true;
|
||||
// }
|
||||
// }
|
||||
// } We might switch to finer and smarter logic later, ex: remove trailing spaces, capitals
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return _InheritedContactState(
|
||||
@ -168,6 +130,7 @@ class _ContactStateState extends State<ContactState> {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
class _InheritedContactState extends InheritedWidget {
|
||||
final _ContactStateState data;
|
||||
|
@ -1,6 +1,6 @@
|
||||
import 'package:dialer/widgets/qr_scanner.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_contacts/flutter_contacts.dart';
|
||||
import '../../../../domain/services/qr/qr_scanner.dart';
|
||||
|
||||
class AddContactButton extends StatelessWidget {
|
||||
const AddContactButton({super.key});
|
217
dialer/lib/features/contacts/widgets/alphabet_scroll_page.dart
Normal file
217
dialer/lib/features/contacts/widgets/alphabet_scroll_page.dart
Normal file
@ -0,0 +1,217 @@
|
||||
import 'package:dialer/services/obfuscate_service.dart';
|
||||
import 'package:dialer/widgets/username_color_generator.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_contacts/flutter_contacts.dart';
|
||||
import '../contact_state.dart';
|
||||
import 'add_contact_button.dart';
|
||||
import 'contact_modal.dart';
|
||||
import 'share_own_qr.dart';
|
||||
|
||||
class AlphabetScrollPage extends StatefulWidget {
|
||||
final double scrollOffset;
|
||||
final List<Contact> contacts;
|
||||
|
||||
const AlphabetScrollPage({
|
||||
super.key,
|
||||
required this.scrollOffset,
|
||||
required this.contacts,
|
||||
});
|
||||
|
||||
@override
|
||||
_AlphabetScrollPageState createState() => _AlphabetScrollPageState();
|
||||
}
|
||||
|
||||
class _AlphabetScrollPageState extends State<AlphabetScrollPage> {
|
||||
late ScrollController _scrollController;
|
||||
|
||||
final ObfuscateService _obfuscateService = ObfuscateService();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_scrollController = ScrollController(initialScrollOffset: widget.scrollOffset);
|
||||
_scrollController.addListener(_onScroll);
|
||||
}
|
||||
|
||||
void _onScroll() {
|
||||
final contactState = ContactState.of(context);
|
||||
contactState.setScrollOffset(_scrollController.offset);
|
||||
}
|
||||
|
||||
Future<void> _refreshContacts() async {
|
||||
final contactState = ContactState.of(context);
|
||||
try {
|
||||
await contactState.fetchContacts();
|
||||
} catch (e) {
|
||||
print('Error refreshing contacts: $e');
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('Failed to refresh contacts')),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void _toggleFavorite(Contact contact) async {
|
||||
try {
|
||||
if (await FlutterContacts.requestPermission()) {
|
||||
Contact? fullContact = await FlutterContacts.getContact(contact.id,
|
||||
withProperties: true,
|
||||
withAccounts: true,
|
||||
withPhoto: true,
|
||||
withThumbnail: true);
|
||||
|
||||
if (fullContact != null) {
|
||||
fullContact.isStarred = !fullContact.isStarred;
|
||||
await FlutterContacts.updateContact(fullContact);
|
||||
}
|
||||
await _refreshContacts();
|
||||
} else {
|
||||
print("Could not fetch contact details");
|
||||
}
|
||||
} catch (e) {
|
||||
print("Error updating favorite status: $e");
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('Failed to update contact favorite status')),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final contacts = widget.contacts;
|
||||
final selfContact = ContactState.of(context).selfContact;
|
||||
|
||||
Map<String, List<Contact>> alphabetizedContacts = {};
|
||||
for (var contact in contacts) {
|
||||
String firstLetter = contact.displayName.isNotEmpty
|
||||
? contact.displayName[0].toUpperCase()
|
||||
: '#';
|
||||
if (!alphabetizedContacts.containsKey(firstLetter)) {
|
||||
alphabetizedContacts[firstLetter] = [];
|
||||
}
|
||||
alphabetizedContacts[firstLetter]!.add(contact);
|
||||
}
|
||||
|
||||
List<String> alphabetKeys = alphabetizedContacts.keys.toList()..sort();
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.black,
|
||||
body: Column(
|
||||
children: [
|
||||
// Top buttons row
|
||||
Container(
|
||||
color: Colors.black,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
AddContactButton(),
|
||||
QRCodeButton(contacts: contacts, selfContact: selfContact),
|
||||
],
|
||||
),
|
||||
),
|
||||
// Contact List
|
||||
Expanded(
|
||||
child: ListView.builder(
|
||||
controller: _scrollController,
|
||||
itemCount: alphabetKeys.length,
|
||||
itemBuilder: (context, index) {
|
||||
String letter = alphabetKeys[index];
|
||||
List<Contact> contactsForLetter = alphabetizedContacts[letter]!;
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Alphabet Letter Header
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 8.0, horizontal: 16.0),
|
||||
child: Text(
|
||||
letter,
|
||||
style: const TextStyle(
|
||||
fontSize: 28,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
// Contact Entries
|
||||
...contactsForLetter.map((contact) {
|
||||
String phoneNumber = contact.phones.isNotEmpty
|
||||
? _obfuscateService.obfuscateData(contact.phones.first.number)
|
||||
: 'No phone number';
|
||||
Color avatarColor =
|
||||
generateColorFromName(contact.displayName);
|
||||
return ListTile(
|
||||
leading: ObfuscatedAvatar(
|
||||
imageBytes: contact.thumbnail,
|
||||
radius: 25,
|
||||
backgroundColor: avatarColor,
|
||||
fallbackInitial: contact.displayName,
|
||||
),
|
||||
title: Text(
|
||||
_obfuscateService.obfuscateData(contact.displayName),
|
||||
style: const TextStyle(color: Colors.white),
|
||||
),
|
||||
subtitle: Text(
|
||||
phoneNumber,
|
||||
style: const TextStyle(color: Colors.white70),
|
||||
),
|
||||
onTap: () {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
backgroundColor: Colors.transparent,
|
||||
builder: (context) {
|
||||
return ContactModal(
|
||||
contact: contact,
|
||||
onEdit: () async {
|
||||
if (await FlutterContacts.requestPermission()) {
|
||||
final updatedContact =
|
||||
await FlutterContacts.openExternalEdit(
|
||||
contact.id);
|
||||
if (updatedContact != null) {
|
||||
await _refreshContacts();
|
||||
Navigator.of(context).pop();
|
||||
ScaffoldMessenger.of(context)
|
||||
.showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
'${contact.displayName} updated successfully!'),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
ScaffoldMessenger.of(context)
|
||||
.showSnackBar(
|
||||
SnackBar(
|
||||
content:
|
||||
Text('Edit canceled or failed.'),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
onToggleFavorite: () {
|
||||
_toggleFavorite(contact);
|
||||
},
|
||||
isFavorite: contact.isStarred,
|
||||
);
|
||||
},
|
||||
);
|
||||
},
|
||||
);
|
||||
}),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_scrollController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
361
dialer/lib/features/contacts/widgets/contact_modal.dart
Normal file
361
dialer/lib/features/contacts/widgets/contact_modal.dart
Normal file
@ -0,0 +1,361 @@
|
||||
import 'package:dialer/services/obfuscate_service.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_contacts/flutter_contacts.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
import 'package:dialer/widgets/username_color_generator.dart';
|
||||
import '../../../services/block_service.dart';
|
||||
import '../../../services/contact_service.dart';
|
||||
import '../../../features/call/call_page.dart';
|
||||
import '../../../services/call_service.dart'; // Import CallService
|
||||
|
||||
class ContactModal extends StatefulWidget {
|
||||
final Contact contact;
|
||||
final Function onEdit;
|
||||
final Function onToggleFavorite;
|
||||
final bool isFavorite;
|
||||
|
||||
const ContactModal({
|
||||
super.key,
|
||||
required this.contact,
|
||||
required this.onEdit,
|
||||
required this.onToggleFavorite,
|
||||
required this.isFavorite,
|
||||
});
|
||||
|
||||
@override
|
||||
_ContactModalState createState() => _ContactModalState();
|
||||
}
|
||||
|
||||
class _ContactModalState extends State<ContactModal> {
|
||||
late String phoneNumber;
|
||||
bool isBlocked = false;
|
||||
final ObfuscateService _obfuscateService = ObfuscateService();
|
||||
final CallService _callService = CallService(); // Instantiate CallService
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
phoneNumber = widget.contact.phones.isNotEmpty
|
||||
? widget.contact.phones.first.number
|
||||
: 'No phone number';
|
||||
_checkIfBlocked();
|
||||
}
|
||||
|
||||
Future<void> _checkIfBlocked() async {
|
||||
if (phoneNumber != 'No phone number') {
|
||||
bool blocked = await BlockService().isNumberBlocked(phoneNumber);
|
||||
setState(() {
|
||||
isBlocked = blocked;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _toggleBlockState() async {
|
||||
if (phoneNumber == 'No phone number') {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('No phone number to block or unblock')),
|
||||
);
|
||||
} else if (isBlocked) {
|
||||
await BlockService().unblockNumber(phoneNumber);
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('$phoneNumber has been unblocked')),
|
||||
);
|
||||
} else {
|
||||
await BlockService().blockNumber(phoneNumber);
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('$phoneNumber has been blocked')),
|
||||
);
|
||||
}
|
||||
|
||||
if (phoneNumber != 'No phone number') {
|
||||
_checkIfBlocked();
|
||||
}
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
|
||||
void _launchPhoneDialer(String phoneNumber) async {
|
||||
final uri = Uri(scheme: 'tel', path: phoneNumber);
|
||||
if (await canLaunchUrl(uri)) {
|
||||
await launchUrl(uri);
|
||||
} else {
|
||||
debugPrint('Could not launch $phoneNumber');
|
||||
}
|
||||
}
|
||||
|
||||
void _launchSms(String phoneNumber) async {
|
||||
final uri = Uri(scheme: 'sms', path: phoneNumber);
|
||||
if (await canLaunchUrl(uri)) {
|
||||
await launchUrl(uri);
|
||||
} else {
|
||||
debugPrint('Could not launch SMS to $phoneNumber');
|
||||
}
|
||||
}
|
||||
|
||||
void _launchEmail(String email) async {
|
||||
final uri = Uri(scheme: 'mailto', path: email);
|
||||
if (await canLaunchUrl(uri)) {
|
||||
await launchUrl(uri);
|
||||
} else {
|
||||
debugPrint('Could not launch email to $email');
|
||||
}
|
||||
}
|
||||
|
||||
void _deleteContact() async {
|
||||
final bool shouldDelete = await showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('Delete Contact'),
|
||||
content: Text(
|
||||
'Are you sure you want to delete ${_obfuscateService.obfuscateData(widget.contact.displayName)}?'),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(false),
|
||||
child: const Text('Cancel'),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(true),
|
||||
child: const Text('Delete'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
if (shouldDelete) {
|
||||
try {
|
||||
// Delete the contact
|
||||
await FlutterContacts.deleteContact(widget.contact);
|
||||
|
||||
// Show success message
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('${_obfuscateService.obfuscateData(widget.contact.displayName)} deleted')),
|
||||
);
|
||||
|
||||
// Close the modal
|
||||
Navigator.of(context).pop();
|
||||
} catch (e) {
|
||||
// Handle errors and show a failure message
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('Failed to delete ${widget.contact.displayName}: $e')),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _shareContactAsQRCode() {
|
||||
// Use the ContactService to show the QR code for the contact's vCard
|
||||
ContactService().showContactQRCodeDialog(context, widget.contact);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
String email = widget.contact.emails.isNotEmpty
|
||||
? _obfuscateService.obfuscateData(widget.contact.emails.first.address)
|
||||
: 'No email';
|
||||
|
||||
return GestureDetector(
|
||||
onTap: () => Navigator.of(context).pop(),
|
||||
child: Container(
|
||||
color: Colors.black.withOpacity(0.5),
|
||||
child: GestureDetector(
|
||||
onTap: () {},
|
||||
child: FractionallySizedBox(
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey[900],
|
||||
borderRadius:
|
||||
const BorderRadius.vertical(top: Radius.circular(20)),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// Modal Handle and Three-Dot Menu
|
||||
Stack(
|
||||
children: [
|
||||
Align(
|
||||
alignment: Alignment.center,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 10),
|
||||
child: Container(
|
||||
width: 50,
|
||||
height: 5,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.shade300,
|
||||
borderRadius: BorderRadius.circular(5),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Align(
|
||||
alignment: Alignment.topRight,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(top: 10, right: 10),
|
||||
child: PopupMenuButton<String>(
|
||||
icon: const Icon(Icons.more_vert,
|
||||
color: Colors.white),
|
||||
onSelected: (String choice) {
|
||||
if (choice == 'delete') {
|
||||
_deleteContact();
|
||||
} else if (choice == 'share') {
|
||||
_shareContactAsQRCode();
|
||||
}
|
||||
// Handle other choices if needed
|
||||
},
|
||||
itemBuilder: (BuildContext context) {
|
||||
return [
|
||||
const PopupMenuItem<String>(
|
||||
value: 'show_associated_contacts',
|
||||
child: Text('Show associated contacts'),
|
||||
),
|
||||
const PopupMenuItem<String>(
|
||||
value: 'delete',
|
||||
child: Text('Delete'),
|
||||
),
|
||||
const PopupMenuItem<String>(
|
||||
value: 'share',
|
||||
child: Text('Share (via QR code)'),
|
||||
),
|
||||
const PopupMenuItem<String>(
|
||||
value: 'create_shortcut',
|
||||
child:
|
||||
Text('Create shortcut (to home screen)'),
|
||||
),
|
||||
const PopupMenuItem<String>(
|
||||
value: 'set_ringtone',
|
||||
child: Text('Set ringtone'),
|
||||
),
|
||||
];
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
// Contact Profile
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
children: [
|
||||
ObfuscatedAvatar(
|
||||
imageBytes: widget.contact.thumbnail,
|
||||
radius: 50,
|
||||
backgroundColor:
|
||||
generateColorFromName(widget.contact.displayName),
|
||||
fallbackInitial: widget.contact.displayName,
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Text(
|
||||
_obfuscateService.obfuscateData(widget.contact.displayName),
|
||||
style: const TextStyle(
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const Divider(color: Colors.grey),
|
||||
// Contact Actions
|
||||
ListTile(
|
||||
leading: const Icon(Icons.phone, color: Colors.green),
|
||||
title: Text(
|
||||
_obfuscateService.obfuscateData(phoneNumber),
|
||||
style: const TextStyle(color: Colors.white),
|
||||
),
|
||||
onTap: () async {
|
||||
if (widget.contact.phones.isNotEmpty) {
|
||||
await _callService.makeGsmCall(phoneNumber);
|
||||
}
|
||||
},
|
||||
onLongPress: () {
|
||||
// Navigate to the beautiful calling page demo
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (_) => CallPage(
|
||||
displayName: widget.contact.displayName,
|
||||
thumbnail: widget.contact.thumbnail,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.message, color: Colors.blue),
|
||||
title: Text(
|
||||
_obfuscateService.obfuscateData(phoneNumber),
|
||||
style: const TextStyle(color: Colors.white),
|
||||
),
|
||||
onTap: () {
|
||||
if (widget.contact.phones.isNotEmpty) {
|
||||
_launchSms(phoneNumber);
|
||||
}
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.email, color: Colors.orange),
|
||||
title: Text(
|
||||
email,
|
||||
style: const TextStyle(color: Colors.white),
|
||||
),
|
||||
onTap: () {
|
||||
if (widget.contact.emails.isNotEmpty) {
|
||||
_launchEmail(email);
|
||||
}
|
||||
},
|
||||
),
|
||||
const Divider(color: Colors.grey),
|
||||
// Favorite, Edit, and Block/Unblock Buttons
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||
child: Column(
|
||||
children: [
|
||||
// Favorite button
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: () {
|
||||
Navigator.of(context).pop();
|
||||
widget.onToggleFavorite();
|
||||
},
|
||||
icon: Icon(widget.isFavorite
|
||||
? Icons.star
|
||||
: Icons.star_border),
|
||||
label: Text(widget.isFavorite
|
||||
? 'Unfavorite'
|
||||
: 'Favorite'),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
// Edit button
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: () => widget.onEdit(),
|
||||
icon: const Icon(Icons.edit),
|
||||
label: const Text('Edit Contact'),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
// Block/Unblock button
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: _toggleBlockState,
|
||||
icon: Icon(
|
||||
isBlocked ? Icons.block : Icons.block_flipped),
|
||||
label: Text(isBlocked ? 'Unblock' : 'Block'),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -1,7 +1,7 @@
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_contacts/contact.dart';
|
||||
import 'package:dialer/domain/services/contact_service.dart';
|
||||
import 'package:dialer/services/contact_service.dart';
|
||||
|
||||
class QRCodeButton extends StatelessWidget {
|
||||
final List<Contact> contacts;
|
32
dialer/lib/features/favorites/favorites_page.dart
Normal file
32
dialer/lib/features/favorites/favorites_page.dart
Normal file
@ -0,0 +1,32 @@
|
||||
import 'package:dialer/features/contacts/contact_state.dart';
|
||||
import 'package:dialer/features/contacts/widgets/alphabet_scroll_page.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:dialer/widgets/loading_indicator.dart';
|
||||
|
||||
class FavoritesPage extends StatefulWidget {
|
||||
const FavoritesPage({super.key});
|
||||
|
||||
@override
|
||||
_FavoritesPageState createState() => _FavoritesPageState();
|
||||
}
|
||||
|
||||
class _FavoritesPageState extends State<FavoritesPage> {
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final contactState = ContactState.of(context);
|
||||
return Scaffold(
|
||||
body: contactState.loading
|
||||
? const LoadingIndicatorWidget()
|
||||
: AlphabetScrollPage(
|
||||
scrollOffset: contactState.scrollOffset,
|
||||
contacts:
|
||||
contactState.favoriteContacts, // Use only favorites here
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -1,17 +1,17 @@
|
||||
import 'dart:async';
|
||||
import 'package:dialer/services/obfuscate_service.dart';
|
||||
import 'package:dialer/widgets/color_darkener.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:flutter_contacts/flutter_contacts.dart';
|
||||
import 'package:intl/intl.dart';
|
||||
import 'package:permission_handler/permission_handler.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
import '../../../domain/services/obfuscate_service.dart';
|
||||
import '../../common/widgets/color_darkener.dart';
|
||||
import '../../common/widgets/username_color_generator.dart';
|
||||
import '../../../domain/services/block_service.dart';
|
||||
import '../../../domain/services/call_service.dart';
|
||||
import '../contacts/contact_state.dart';
|
||||
import 'package:dialer/features/contacts/contact_state.dart';
|
||||
import 'package:dialer/widgets/username_color_generator.dart';
|
||||
import '../../services/block_service.dart';
|
||||
import '../contacts/widgets/contact_modal.dart';
|
||||
import '../../services/call_service.dart';
|
||||
|
||||
class History {
|
||||
final Contact contact;
|
||||
@ -37,7 +37,7 @@ class HistoryPage extends StatefulWidget {
|
||||
}
|
||||
|
||||
class _HistoryPageState extends State<HistoryPage>
|
||||
with AutomaticKeepAliveClientMixin, SingleTickerProviderStateMixin {
|
||||
with SingleTickerProviderStateMixin {
|
||||
List<History> histories = [];
|
||||
bool loading = true;
|
||||
int? _expandedIndex;
|
||||
@ -47,13 +47,10 @@ class _HistoryPageState extends State<HistoryPage>
|
||||
// Create a MethodChannel instance.
|
||||
static const MethodChannel _channel = MethodChannel('com.example.calllog');
|
||||
|
||||
@override
|
||||
bool get wantKeepAlive => true; // Preserve state when switching pages
|
||||
|
||||
@override
|
||||
void didChangeDependencies() {
|
||||
super.didChangeDependencies();
|
||||
if (loading && histories.isEmpty) {
|
||||
if (loading) {
|
||||
_buildHistories();
|
||||
}
|
||||
}
|
||||
@ -82,19 +79,14 @@ class _HistoryPageState extends State<HistoryPage>
|
||||
fullContact.isStarred = !fullContact.isStarred;
|
||||
await FlutterContacts.updateContact(fullContact);
|
||||
}
|
||||
// Check if still mounted before accessing context
|
||||
if (mounted) {
|
||||
await _refreshContacts();
|
||||
}
|
||||
await _refreshContacts();
|
||||
} else {
|
||||
debugPrint("Could not fetch contact details");
|
||||
print("Could not fetch contact details");
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint("Error updating favorite status: $e");
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('Failed to update favorite status')));
|
||||
}
|
||||
print("Error updating favorite status: $e");
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('Failed to update favorite status')));
|
||||
}
|
||||
}
|
||||
|
||||
@ -157,9 +149,9 @@ class _HistoryPageState extends State<HistoryPage>
|
||||
List<Contact> contacts = contactState.contacts;
|
||||
|
||||
List<History> callHistories = [];
|
||||
// Process each log entry with intermittent yields to avoid freezing.
|
||||
for (int i = 0; i < nativeLogs.length; i++) {
|
||||
final entry = nativeLogs[i];
|
||||
// Process each log entry.
|
||||
for (var entry in nativeLogs) {
|
||||
// Each entry is a Map with keys: number, type, date, duration.
|
||||
final String number = entry['number'] ?? '';
|
||||
if (number.isEmpty) continue;
|
||||
|
||||
@ -205,8 +197,6 @@ class _HistoryPageState extends State<HistoryPage>
|
||||
|
||||
callHistories
|
||||
.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.
|
||||
@ -282,7 +272,6 @@ class _HistoryPageState extends State<HistoryPage>
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
super.build(context); // required due to AutomaticKeepAliveClientMixin
|
||||
final contactState = ContactState.of(context);
|
||||
|
||||
if (loading || contactState.loading) {
|
||||
@ -430,12 +419,7 @@ class _HistoryPageState extends State<HistoryPage>
|
||||
icon: const Icon(Icons.phone, color: Colors.green),
|
||||
onPressed: () async {
|
||||
if (contact.phones.isNotEmpty) {
|
||||
await _callService.makeGsmCall(
|
||||
context,
|
||||
phoneNumber: contact.phones.first.number,
|
||||
displayName: contact.displayName,
|
||||
thumbnail: contact.thumbnail,
|
||||
);
|
||||
_callService.makeGsmCall(contact.phones.first.number);
|
||||
} else {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(
|
@ -1,91 +1,63 @@
|
||||
import 'package:dialer/services/obfuscate_service.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import '../../../domain/services/obfuscate_service.dart';
|
||||
import '../contacts/contact_page.dart';
|
||||
import '../favorites/favorites_page.dart';
|
||||
import '../history/history_page.dart';
|
||||
import '../composition/composition.dart';
|
||||
import '../settings/settings.dart';
|
||||
import '../voicemail/voicemail_page.dart';
|
||||
import '../contacts/widgets/contact_modal.dart';
|
||||
import 'package:dialer/features/contacts/contact_page.dart';
|
||||
import 'package:dialer/features/favorites/favorites_page.dart';
|
||||
import 'package:dialer/features/history/history_page.dart';
|
||||
import 'package:dialer/features/composition/composition.dart';
|
||||
import 'package:flutter_contacts/flutter_contacts.dart';
|
||||
import 'package:dialer/domain/services/contact_service.dart';
|
||||
import 'package:dialer/features/settings/settings.dart';
|
||||
import '../../services/contact_service.dart';
|
||||
import 'package:dialer/features/voicemail/voicemail_page.dart';
|
||||
import '../contacts/widgets/contact_modal.dart';
|
||||
import 'package:dialer/features/about/about.dart'; // Add this import
|
||||
|
||||
class _MyHomePageState extends State<MyHomePage> with SingleTickerProviderStateMixin {
|
||||
|
||||
class _MyHomePageState extends State<MyHomePage>
|
||||
with SingleTickerProviderStateMixin {
|
||||
late TabController _tabController;
|
||||
List<Contact> _allContacts = [];
|
||||
List<Contact> _contactSuggestions = [];
|
||||
final ContactService _contactService = ContactService();
|
||||
final ObfuscateService _obfuscateService = ObfuscateService();
|
||||
final TextEditingController _searchController = TextEditingController();
|
||||
late SearchController _searchBarController;
|
||||
String _rawSearchInput = '';
|
||||
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_tabController = TabController(length: 4, vsync: this, initialIndex: 2);
|
||||
// Set the TabController length to 4
|
||||
_tabController = TabController(length: 4, vsync: this, initialIndex: 1);
|
||||
_tabController.addListener(_handleTabIndex);
|
||||
_searchBarController = SearchController();
|
||||
_searchBarController.addListener(() {
|
||||
if (_searchController.text != _searchBarController.text) {
|
||||
_rawSearchInput = _searchBarController.text;
|
||||
_searchController.text = _rawSearchInput;
|
||||
_onSearchChanged(_searchBarController.text);
|
||||
}
|
||||
});
|
||||
_fetchContacts();
|
||||
}
|
||||
|
||||
void _fetchContacts() async {
|
||||
_allContacts = await _contactService.fetchContacts();
|
||||
_contactSuggestions = List.from(_allContacts);
|
||||
if (mounted) setState(() {});
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
void _clearSearch() {
|
||||
_searchController.clear();
|
||||
_searchBarController.clear();
|
||||
_rawSearchInput = '';
|
||||
_onSearchChanged('');
|
||||
}
|
||||
|
||||
void _onSearchChanged(String query) {
|
||||
setState(() {
|
||||
if (query.isEmpty) {
|
||||
_contactSuggestions = List.from(_allContacts);
|
||||
_contactSuggestions = List.from(_allContacts); // Reset suggestions
|
||||
} else {
|
||||
final normalizedQuery = _normalizeString(query.toLowerCase());
|
||||
_contactSuggestions = _allContacts.where((contact) {
|
||||
final normalizedName = _normalizeString(contact.displayName.toLowerCase());
|
||||
return normalizedName.contains(normalizedQuery);
|
||||
return contact.displayName
|
||||
.toLowerCase()
|
||||
.contains(query.toLowerCase());
|
||||
}).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
|
||||
void dispose() {
|
||||
_searchController.dispose();
|
||||
_searchBarController.dispose();
|
||||
_tabController.removeListener(_handleTabIndex);
|
||||
_tabController.dispose();
|
||||
super.dispose();
|
||||
@ -98,18 +70,19 @@ class _MyHomePageState extends State<MyHomePage> with SingleTickerProviderStateM
|
||||
void _toggleFavorite(Contact contact) async {
|
||||
try {
|
||||
if (await FlutterContacts.requestPermission()) {
|
||||
Contact? fullContact = await FlutterContacts.getContact(
|
||||
contact.id,
|
||||
withProperties: true,
|
||||
withAccounts: true,
|
||||
withPhoto: true,
|
||||
withThumbnail: true,
|
||||
);
|
||||
Contact? fullContact = await FlutterContacts.getContact(contact.id,
|
||||
withProperties: true,
|
||||
withAccounts: true,
|
||||
withPhoto: true,
|
||||
withThumbnail: true);
|
||||
|
||||
if (fullContact != null) {
|
||||
fullContact.isStarred = !fullContact.isStarred;
|
||||
await FlutterContacts.updateContact(fullContact);
|
||||
_fetchContacts();
|
||||
setState(() {
|
||||
// Updating the contact list after toggling the favorite
|
||||
_fetchContacts();
|
||||
});
|
||||
}
|
||||
} else {
|
||||
print("Could not fetch contact details");
|
||||
@ -128,6 +101,7 @@ class _MyHomePageState extends State<MyHomePage> with SingleTickerProviderStateM
|
||||
backgroundColor: Colors.black,
|
||||
body: Column(
|
||||
children: [
|
||||
// Persistent Search Bar
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(
|
||||
top: 24.0,
|
||||
@ -145,33 +119,35 @@ class _MyHomePageState extends State<MyHomePage> with SingleTickerProviderStateM
|
||||
border: Border.all(color: Colors.grey.shade800, width: 1),
|
||||
),
|
||||
child: SearchAnchor(
|
||||
searchController: _searchBarController,
|
||||
builder: (BuildContext context, SearchController controller) {
|
||||
builder:
|
||||
(BuildContext context, SearchController controller) {
|
||||
return GestureDetector(
|
||||
onTap: () {
|
||||
controller.openView();
|
||||
controller.openView(); // Open the search view
|
||||
},
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: const Color.fromARGB(255, 30, 30, 30),
|
||||
borderRadius: BorderRadius.circular(12.0),
|
||||
border: Border.all(color: Colors.grey.shade800, width: 1),
|
||||
border: Border.all(
|
||||
color: Colors.grey.shade800, width: 1),
|
||||
),
|
||||
padding: const EdgeInsets.symmetric(vertical: 12.0, horizontal: 16.0),
|
||||
padding: const EdgeInsets.symmetric(
|
||||
vertical: 12.0, horizontal: 16.0),
|
||||
child: Row(
|
||||
children: [
|
||||
const Icon(Icons.search, color: Colors.grey, size: 24.0),
|
||||
const Icon(Icons.search,
|
||||
color: Colors.grey, size: 24.0),
|
||||
const SizedBox(width: 8.0),
|
||||
Expanded(
|
||||
child: Text(
|
||||
_rawSearchInput.isEmpty
|
||||
? 'Search contacts'
|
||||
: _rawSearchInput,
|
||||
style: const TextStyle(color: Colors.grey, fontSize: 16.0),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
Text(
|
||||
_searchController.text.isEmpty
|
||||
? 'Search contacts'
|
||||
: _searchController.text,
|
||||
style: const TextStyle(
|
||||
color: Colors.grey, fontSize: 16.0),
|
||||
),
|
||||
if (_rawSearchInput.isNotEmpty)
|
||||
const Spacer(),
|
||||
if (_searchController.text.isNotEmpty)
|
||||
GestureDetector(
|
||||
onTap: _clearSearch,
|
||||
child: const Icon(
|
||||
@ -186,24 +162,23 @@ class _MyHomePageState extends State<MyHomePage> with SingleTickerProviderStateM
|
||||
);
|
||||
},
|
||||
viewOnChanged: (query) {
|
||||
|
||||
if (_searchBarController.text != query) {
|
||||
_rawSearchInput = query;
|
||||
_searchBarController.text = query;
|
||||
_searchController.text = query;
|
||||
}
|
||||
_onSearchChanged(query);
|
||||
_onSearchChanged(query); // Update immediately
|
||||
},
|
||||
suggestionsBuilder: (BuildContext context, SearchController controller) {
|
||||
suggestionsBuilder:
|
||||
(BuildContext context, SearchController controller) {
|
||||
return _contactSuggestions.map((contact) {
|
||||
return ListTile(
|
||||
key: ValueKey(contact.id),
|
||||
title: Text(
|
||||
_obfuscateService.obfuscateData(contact.displayName),
|
||||
style: const TextStyle(color: Colors.white),
|
||||
),
|
||||
title: Text(_obfuscateService.obfuscateData(contact.displayName),
|
||||
style: const TextStyle(color: Colors.white)),
|
||||
onTap: () {
|
||||
// Clear the search text input
|
||||
controller.text = '';
|
||||
|
||||
// Close the search view
|
||||
controller.closeView(contact.displayName);
|
||||
|
||||
// Show the ContactModal when a contact is tapped
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
@ -212,28 +187,34 @@ class _MyHomePageState extends State<MyHomePage> with SingleTickerProviderStateM
|
||||
return ContactModal(
|
||||
contact: contact,
|
||||
onEdit: () async {
|
||||
if (await FlutterContacts.requestPermission()) {
|
||||
final updatedContact = await FlutterContacts
|
||||
.openExternalEdit(contact.id);
|
||||
if (await FlutterContacts
|
||||
.requestPermission()) {
|
||||
final updatedContact =
|
||||
await FlutterContacts
|
||||
.openExternalEdit(contact.id);
|
||||
if (updatedContact != null) {
|
||||
_fetchContacts();
|
||||
Navigator.of(context).pop();
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
ScaffoldMessenger.of(context)
|
||||
.showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
'${contact.displayName} updated successfully!'),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
ScaffoldMessenger.of(context)
|
||||
.showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('Edit canceled or failed.'),
|
||||
content: Text(
|
||||
'Edit canceled or failed.'),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
onToggleFavorite: () => _toggleFavorite(contact),
|
||||
onToggleFavorite: () =>
|
||||
_toggleFavorite(contact),
|
||||
isFavorite: contact.isStarred,
|
||||
);
|
||||
},
|
||||
@ -245,6 +226,7 @@ class _MyHomePageState extends State<MyHomePage> with SingleTickerProviderStateM
|
||||
),
|
||||
),
|
||||
),
|
||||
// 3-dot menu
|
||||
PopupMenuButton<String>(
|
||||
icon: const Icon(Icons.more_vert, color: Colors.white),
|
||||
itemBuilder: (BuildContext context) => [
|
||||
@ -252,12 +234,23 @@ class _MyHomePageState extends State<MyHomePage> with SingleTickerProviderStateM
|
||||
value: 'settings',
|
||||
child: Text('Settings'),
|
||||
),
|
||||
const PopupMenuItem<String>( // Add About menu item
|
||||
value: 'about',
|
||||
child: Text('About'),
|
||||
),
|
||||
],
|
||||
onSelected: (String value) {
|
||||
if (value == 'settings') {
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(builder: (context) => const SettingsPage()),
|
||||
MaterialPageRoute(
|
||||
builder: (context) => const SettingsPage()),
|
||||
);
|
||||
} else if (value == 'about') { // Handle About selection
|
||||
Navigator.push(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => const AboutPage()),
|
||||
);
|
||||
}
|
||||
},
|
||||
@ -265,6 +258,7 @@ class _MyHomePageState extends State<MyHomePage> with SingleTickerProviderStateM
|
||||
],
|
||||
),
|
||||
),
|
||||
// Main content with TabBarView
|
||||
Expanded(
|
||||
child: Stack(
|
||||
children: [
|
||||
@ -339,4 +333,4 @@ class MyHomePage extends StatefulWidget {
|
||||
|
||||
@override
|
||||
_MyHomePageState createState() => _MyHomePageState();
|
||||
}
|
||||
}
|
@ -77,7 +77,7 @@ class _SettingsCallPageState extends State<SettingsCallPage> {
|
||||
},
|
||||
child: const Text('Beep'),
|
||||
),
|
||||
// ...existing options...
|
||||
// Add more ringtone options
|
||||
],
|
||||
);
|
||||
},
|
@ -1,5 +1,5 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:dialer/domain/services/cryptography/asymmetric_crypto_service.dart';
|
||||
import 'package:dialer/services/cryptography/asymmetric_crypto_service.dart';
|
||||
|
||||
class ManageKeysPage extends StatefulWidget {
|
||||
const ManageKeysPage({Key? key}) : super(key: key);
|
@ -1,7 +1,10 @@
|
||||
// settings.dart
|
||||
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:dialer/presentation/features/settings/call/settings_call.dart';
|
||||
import 'package:dialer/presentation/features/settings/cryptography/key_management.dart';
|
||||
import 'package:dialer/presentation/features/settings/blocked/settings_blocked.dart';
|
||||
import 'package:dialer/features/settings/call/settingsCall.dart';
|
||||
// import 'package:dialer/features/settings/cryptography/';
|
||||
import 'package:dialer/features/settings/blocked/settings_blocked.dart';
|
||||
import 'cryptography/key_management.dart';
|
||||
|
||||
class SettingsPage extends StatelessWidget {
|
||||
const SettingsPage({super.key});
|
@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
|
||||
import 'package:audioplayers/audioplayers.dart';
|
||||
|
||||
class VoicemailPage extends StatefulWidget {
|
||||
const VoicemailPage({super.key});
|
||||
const VoicemailPage({Key? key}) : super(key: key);
|
||||
|
||||
@override
|
||||
State<VoicemailPage> createState() => _VoicemailPageState();
|
||||
@ -14,7 +14,6 @@ class _VoicemailPageState extends State<VoicemailPage> {
|
||||
Duration _duration = Duration.zero;
|
||||
Duration _position = Duration.zero;
|
||||
late AudioPlayer _audioPlayer;
|
||||
bool _loading = false;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
@ -51,12 +50,12 @@ class _VoicemailPageState extends State<VoicemailPage> {
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
if (_loading) {
|
||||
return const Center(child: CircularProgressIndicator());
|
||||
}
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.black,
|
||||
// appBar: AppBar(
|
||||
// // title: const Text('Voicemail'),
|
||||
// backgroundColor: Colors.black,
|
||||
// ),
|
||||
body: ListView(
|
||||
children: [
|
||||
GestureDetector(
|
@ -1,7 +1 @@
|
||||
// Global variables accessible throughout the app
|
||||
library globals;
|
||||
|
||||
import 'core/config/app_config.dart';
|
||||
|
||||
// Whether the app is in stealth mode (obfuscated content)
|
||||
bool get isStealthMode => AppConfig.isStealthMode;
|
||||
bool isStealthMode = false;
|
@ -1,35 +1,17 @@
|
||||
import 'package:dialer/presentation/features/home/home_page.dart';
|
||||
import 'package:dialer/presentation/features/home/default_dialer_prompt.dart';
|
||||
import 'package:dialer/features/home/home_page.dart';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'package:permission_handler/permission_handler.dart';
|
||||
import 'package:dialer/features/contacts/contact_state.dart';
|
||||
import 'globals.dart' as globals;
|
||||
import 'package:dialer/services/cryptography/asymmetric_crypto_service.dart';
|
||||
import 'package:provider/provider.dart';
|
||||
|
||||
import 'core/config/app_config.dart';
|
||||
import 'domain/services/call_service.dart';
|
||||
import 'domain/services/cryptography/asymmetric_crypto_service.dart';
|
||||
import 'presentation/features/contacts/contact_state.dart';
|
||||
|
||||
void main() async {
|
||||
WidgetsFlutterBinding.ensureInitialized();
|
||||
|
||||
// Initialize app configuration
|
||||
await AppConfig.initialize();
|
||||
const stealthFlag = String.fromEnvironment('STEALTH', defaultValue: 'false');
|
||||
globals.isStealthMode = stealthFlag.toLowerCase() == 'true';
|
||||
|
||||
// Initialize cryptography service with error handling
|
||||
final AsymmetricCryptoService cryptoService = AsymmetricCryptoService();
|
||||
try {
|
||||
await cryptoService.initializeDefaultKeyPair();
|
||||
} catch (e) {
|
||||
debugPrint('Error initializing cryptography: $e');
|
||||
// Continue app initialization even if crypto fails
|
||||
}
|
||||
|
||||
// Request permissions before running the app
|
||||
await _requestPermissions();
|
||||
|
||||
// Initialize call service
|
||||
CallService();
|
||||
await cryptoService.initializeDefaultKeyPair();
|
||||
|
||||
runApp(
|
||||
MultiProvider(
|
||||
@ -37,69 +19,25 @@ void main() async {
|
||||
Provider<AsymmetricCryptoService>(
|
||||
create: (_) => cryptoService,
|
||||
),
|
||||
// Add other providers here
|
||||
],
|
||||
child: const DialerApp(),
|
||||
child: Dialer(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _requestPermissions() async {
|
||||
Map<Permission, PermissionStatus> statuses = await [
|
||||
Permission.phone,
|
||||
Permission.contacts,
|
||||
Permission.microphone,
|
||||
].request();
|
||||
if (statuses.values.every((status) => status.isGranted)) {
|
||||
print("All required permissions granted");
|
||||
const channel = MethodChannel('call_service');
|
||||
await channel.invokeMethod('permissionsGranted');
|
||||
} else {
|
||||
print("Permissions denied: ${statuses.entries.where((e) => !e.value.isGranted).map((e) => e.key).join(', ')}");
|
||||
}
|
||||
}
|
||||
|
||||
class DialerApp extends StatelessWidget {
|
||||
const DialerApp({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;
|
||||
}
|
||||
}
|
||||
class Dialer extends StatelessWidget {
|
||||
const Dialer({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return ContactState(
|
||||
child: MaterialApp(
|
||||
title: 'Dialer App',
|
||||
navigatorKey: CallService.navigatorKey,
|
||||
theme: ThemeData(
|
||||
brightness: Brightness.dark,
|
||||
brightness: Brightness.dark
|
||||
),
|
||||
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()),
|
||||
},
|
||||
),
|
||||
home: SafeArea(child: MyHomePage()),
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,42 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
class AppTheme {
|
||||
static ThemeData get darkTheme => ThemeData(
|
||||
brightness: Brightness.dark,
|
||||
scaffoldBackgroundColor: Colors.black,
|
||||
appBarTheme: const AppBarTheme(
|
||||
backgroundColor: Colors.black,
|
||||
elevation: 0,
|
||||
),
|
||||
tabBarTheme: const TabBarThemeData(
|
||||
labelColor: Colors.white,
|
||||
unselectedLabelColor: Color.fromARGB(255, 158, 158, 158),
|
||||
indicatorSize: TabBarIndicatorSize.label,
|
||||
indicatorColor: Colors.white,
|
||||
),
|
||||
bottomNavigationBarTheme: const BottomNavigationBarThemeData(
|
||||
backgroundColor: Colors.black,
|
||||
),
|
||||
floatingActionButtonTheme: const FloatingActionButtonThemeData(
|
||||
backgroundColor: Colors.blue,
|
||||
foregroundColor: Colors.white,
|
||||
),
|
||||
textTheme: const TextTheme(
|
||||
bodyLarge: TextStyle(color: Colors.white),
|
||||
bodyMedium: TextStyle(color: Colors.white),
|
||||
titleLarge: TextStyle(color: Colors.white),
|
||||
),
|
||||
snackBarTheme: const SnackBarThemeData(
|
||||
backgroundColor: Color(0xFF303030),
|
||||
contentTextStyle: TextStyle(color: Colors.white),
|
||||
),
|
||||
inputDecorationTheme: InputDecorationTheme(
|
||||
border: OutlineInputBorder(
|
||||
borderRadius: BorderRadius.circular(8),
|
||||
borderSide: BorderSide.none,
|
||||
),
|
||||
filled: true,
|
||||
fillColor: const Color.fromARGB(255, 30, 30, 30),
|
||||
),
|
||||
);
|
||||
}
|
@ -1,21 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// Darkens a color by a given percentage
|
||||
Color darken(Color color, [double amount = 0.3]) {
|
||||
assert(amount >= 0 && amount <= 1);
|
||||
|
||||
final hsl = HSLColor.fromColor(color);
|
||||
final darkened = hsl.withLightness((hsl.lightness - amount).clamp(0.0, 1.0));
|
||||
|
||||
return darkened.toColor();
|
||||
}
|
||||
|
||||
/// Lightens a color by a given percentage
|
||||
Color lighten(Color color, [double amount = 0.3]) {
|
||||
assert(amount >= 0 && amount <= 1);
|
||||
|
||||
final hsl = HSLColor.fromColor(color);
|
||||
final lightened = hsl.withLightness((hsl.lightness + amount).clamp(0.0, 1.0));
|
||||
|
||||
return lightened.toColor();
|
||||
}
|
@ -1,17 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// Generates a deterministic color from a string input
|
||||
Color generateColorFromName(String name) {
|
||||
if (name.isEmpty) return Colors.grey;
|
||||
|
||||
// Use the hashCode of the name to generate a consistent color
|
||||
int hash = name.hashCode;
|
||||
|
||||
// Use the hash to generate RGB values
|
||||
final r = (hash & 0xFF0000) >> 16;
|
||||
final g = (hash & 0x00FF00) >> 8;
|
||||
final b = hash & 0x0000FF;
|
||||
|
||||
// Create a color with these RGB values
|
||||
return Color.fromARGB(255, r, g, b);
|
||||
}
|
@ -1,564 +0,0 @@
|
||||
import 'dart:async';
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_contacts/flutter_contacts.dart';
|
||||
import 'package:dialer/domain/services/call_service.dart';
|
||||
import 'package:dialer/domain/services/obfuscate_service.dart';
|
||||
import 'package:dialer/presentation/common/widgets/username_color_generator.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
class CallPage extends StatefulWidget {
|
||||
final String displayName;
|
||||
final String phoneNumber;
|
||||
final Uint8List? thumbnail;
|
||||
|
||||
const CallPage({
|
||||
super.key,
|
||||
required this.displayName,
|
||||
required this.phoneNumber,
|
||||
this.thumbnail,
|
||||
});
|
||||
|
||||
@override
|
||||
_CallPageState createState() => _CallPageState();
|
||||
}
|
||||
|
||||
class _CallPageState extends State<CallPage> {
|
||||
final ObfuscateService _obfuscateService = ObfuscateService();
|
||||
final CallService _callService = CallService();
|
||||
bool isMuted = false;
|
||||
bool isSpeaker = false;
|
||||
bool isKeypadVisible = false;
|
||||
bool icingProtocolOk = true;
|
||||
String _typedDigits = "";
|
||||
Timer? _callTimer;
|
||||
int _callSeconds = 0;
|
||||
String _callStatus = "Calling...";
|
||||
StreamSubscription<String>? _callStateSubscription;
|
||||
StreamSubscription<Map<String, dynamic>>? _audioStateSubscription;
|
||||
|
||||
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(() {
|
||||
_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() async {
|
||||
try {
|
||||
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')),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _toggleSpeaker() async {
|
||||
try {
|
||||
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() {
|
||||
setState(() {
|
||||
isKeypadVisible = !isKeypadVisible;
|
||||
});
|
||||
}
|
||||
|
||||
void _toggleIcingProtocol() {
|
||||
setState(() {
|
||||
icingProtocolOk = !icingProtocolOk;
|
||||
});
|
||||
}
|
||||
|
||||
void _hangUp() async {
|
||||
try {
|
||||
print('CallPage: Initiating hangUp');
|
||||
final result = await _callService.hangUpCall(context);
|
||||
print('CallPage: Hang up result: $result');
|
||||
} catch (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')),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final double avatarRadius = isKeypadVisible ? 45.0 : 45.0;
|
||||
final double nameFontSize = isKeypadVisible ? 24.0 : 24.0;
|
||||
final double statusFontSize = isKeypadVisible ? 16.0 : 16.0;
|
||||
|
||||
print(
|
||||
'CallPage: Building UI, _callStatus: $_callStatus, route: ${ModalRoute.of(context)?.settings.name ?? "unknown"}');
|
||||
return PopScope(
|
||||
canPop: _callStatus == "Call Ended",
|
||||
onPopInvoked: (didPop) {
|
||||
if (!didPop) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('Cannot leave during an active call')),
|
||||
);
|
||||
}
|
||||
},
|
||||
child: Scaffold(
|
||||
body: Container(
|
||||
color: Colors.black,
|
||||
child: SafeArea(
|
||||
child: Column(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8.0),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
const SizedBox(height: 35),
|
||||
ObfuscatedAvatar(
|
||||
imageBytes: widget.thumbnail,
|
||||
radius: avatarRadius,
|
||||
backgroundColor:
|
||||
generateColorFromName(widget.displayName),
|
||||
fallbackInitial: widget.displayName,
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.center,
|
||||
children: [
|
||||
Icon(
|
||||
icingProtocolOk ? Icons.lock : Icons.lock_open,
|
||||
color: icingProtocolOk ? Colors.green : Colors.red,
|
||||
size: 16,
|
||||
),
|
||||
const SizedBox(width: 4),
|
||||
Text(
|
||||
'Icing protocol: ${icingProtocolOk ? "ok" : "ko"}',
|
||||
style: TextStyle(
|
||||
color:
|
||||
icingProtocolOk ? Colors.green : Colors.red,
|
||||
fontSize: 12,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
_obfuscateService.obfuscateData(widget.displayName),
|
||||
style: TextStyle(
|
||||
fontSize: nameFontSize,
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
widget.phoneNumber,
|
||||
style: TextStyle(
|
||||
fontSize: statusFontSize,
|
||||
color: Colors.white70,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
_callStatus,
|
||||
style: TextStyle(
|
||||
fontSize: statusFontSize,
|
||||
color: Colors.white70,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
child: Column(
|
||||
children: [
|
||||
if (isKeypadVisible) ...[
|
||||
const Spacer(flex: 2),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 20.0),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Text(
|
||||
_typedDigits,
|
||||
maxLines: 1,
|
||||
textAlign: TextAlign.right,
|
||||
overflow: TextOverflow.ellipsis,
|
||||
style: const TextStyle(
|
||||
fontSize: 24,
|
||||
color: Colors.white,
|
||||
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.4,
|
||||
margin: const EdgeInsets.symmetric(horizontal: 20),
|
||||
padding: const EdgeInsets.all(8),
|
||||
child: GridView.count(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
crossAxisCount: 3,
|
||||
childAspectRatio: 1.5,
|
||||
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,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}),
|
||||
),
|
||||
),
|
||||
const Spacer(flex: 1),
|
||||
] 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: const EdgeInsets.only(bottom: 16.0),
|
||||
child: GestureDetector(
|
||||
onTap: _hangUp,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: const BoxDecoration(
|
||||
color: Colors.red,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: const Icon(
|
||||
Icons.call_end,
|
||||
color: Colors.white,
|
||||
size: 32,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -1,163 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter/services.dart';
|
||||
import 'dart:typed_data';
|
||||
import '../../../domain/services/call_service.dart';
|
||||
import '../../../domain/services/obfuscate_service.dart';
|
||||
import 'package:dialer/presentation/common/widgets/username_color_generator.dart';
|
||||
import 'call_page.dart';
|
||||
|
||||
class IncomingCallPage extends StatefulWidget {
|
||||
final String displayName;
|
||||
final String phoneNumber;
|
||||
final Uint8List? thumbnail;
|
||||
|
||||
const IncomingCallPage({
|
||||
super.key,
|
||||
required this.displayName,
|
||||
required this.phoneNumber,
|
||||
this.thumbnail,
|
||||
});
|
||||
|
||||
@override
|
||||
_IncomingCallPageState createState() => _IncomingCallPageState();
|
||||
}
|
||||
|
||||
class _IncomingCallPageState extends State<IncomingCallPage> {
|
||||
static const MethodChannel _channel = MethodChannel('call_service');
|
||||
final ObfuscateService _obfuscateService = ObfuscateService();
|
||||
final CallService _callService = CallService();
|
||||
|
||||
void _answerCall() async {
|
||||
try {
|
||||
final result = await _channel.invokeMethod('answerCall');
|
||||
debugPrint('IncomingCallPage: Answer call result: $result');
|
||||
|
||||
if (result["status"] == "answered") {
|
||||
if (!mounted) return;
|
||||
|
||||
Navigator.pushReplacement(
|
||||
context,
|
||||
MaterialPageRoute(
|
||||
builder: (context) => CallPage(
|
||||
displayName: widget.displayName,
|
||||
phoneNumber: widget.phoneNumber,
|
||||
thumbnail: widget.thumbnail,
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint("IncomingCallPage: Error answering call: $e");
|
||||
if (!mounted) return;
|
||||
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text("Error answering call: $e")),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void _declineCall() async {
|
||||
try {
|
||||
await _callService.hangUpCall(context);
|
||||
} catch (e) {
|
||||
debugPrint("IncomingCallPage: Error declining call: $e");
|
||||
if (!mounted) return;
|
||||
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text("Error declining call: $e")),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.black,
|
||||
body: SafeArea(
|
||||
child: Column(
|
||||
children: [
|
||||
const Spacer(),
|
||||
ObfuscatedAvatar(
|
||||
imageBytes: widget.thumbnail,
|
||||
radius: 60,
|
||||
backgroundColor: generateColorFromName(widget.displayName),
|
||||
fallbackInitial: widget.displayName,
|
||||
),
|
||||
const SizedBox(height: 24),
|
||||
Text(
|
||||
_obfuscateService.obfuscateData(widget.displayName),
|
||||
style: const TextStyle(
|
||||
fontSize: 28,
|
||||
color: Colors.white,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
Text(
|
||||
widget.phoneNumber,
|
||||
style: const TextStyle(fontSize: 18, color: Colors.white70),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
const Text(
|
||||
'Incoming Call',
|
||||
style: TextStyle(fontSize: 20, color: Colors.white70),
|
||||
),
|
||||
const Spacer(),
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(bottom: 48.0),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
_buildActionButton(
|
||||
icon: Icons.call_end,
|
||||
color: Colors.red,
|
||||
onPressed: _declineCall,
|
||||
label: 'Decline',
|
||||
),
|
||||
_buildActionButton(
|
||||
icon: Icons.call,
|
||||
color: Colors.green,
|
||||
onPressed: _answerCall,
|
||||
label: 'Answer',
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildActionButton({
|
||||
required IconData icon,
|
||||
required Color color,
|
||||
required VoidCallback onPressed,
|
||||
required String label,
|
||||
}) {
|
||||
return Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
GestureDetector(
|
||||
onTap: onPressed,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.all(16),
|
||||
decoration: BoxDecoration(
|
||||
color: color,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Icon(
|
||||
icon,
|
||||
color: Colors.white,
|
||||
size: 32,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 8),
|
||||
Text(
|
||||
label,
|
||||
style: const TextStyle(color: Colors.white),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
@ -1,335 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_contacts/flutter_contacts.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
import '../../../domain/services/contact_service.dart';
|
||||
import '../../../domain/services/obfuscate_service.dart';
|
||||
import '../../../domain/services/call_service.dart';
|
||||
|
||||
class CompositionPage extends StatefulWidget {
|
||||
const CompositionPage({super.key});
|
||||
|
||||
@override
|
||||
_CompositionPageState createState() => _CompositionPageState();
|
||||
}
|
||||
|
||||
class _CompositionPageState extends State<CompositionPage> {
|
||||
String dialedNumber = "";
|
||||
List<Contact> _allContacts = [];
|
||||
List<Contact> _filteredContacts = [];
|
||||
final ContactService _contactService = ContactService();
|
||||
final ObfuscateService _obfuscateService = ObfuscateService();
|
||||
final CallService _callService = CallService();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_fetchContacts();
|
||||
}
|
||||
|
||||
Future<void> _fetchContacts() async {
|
||||
_allContacts = await _contactService.fetchContacts();
|
||||
_filteredContacts = _allContacts;
|
||||
setState(() {});
|
||||
}
|
||||
|
||||
void _filterContacts() {
|
||||
setState(() {
|
||||
_filteredContacts = _allContacts.where((contact) {
|
||||
bool phoneMatch = contact.phones.any((phone) {
|
||||
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
|
||||
.toLowerCase()
|
||||
.contains(dialedNumber.toLowerCase());
|
||||
return phoneMatch || nameMatch;
|
||||
}).toList();
|
||||
});
|
||||
}
|
||||
|
||||
void _onNumberPress(String number) {
|
||||
setState(() {
|
||||
dialedNumber += number;
|
||||
_filterContacts();
|
||||
});
|
||||
}
|
||||
|
||||
void _onPlusPress() {
|
||||
setState(() {
|
||||
dialedNumber += '+';
|
||||
_filterContacts();
|
||||
});
|
||||
}
|
||||
|
||||
void _onDeletePress() {
|
||||
setState(() {
|
||||
if (dialedNumber.isNotEmpty) {
|
||||
dialedNumber = dialedNumber.substring(0, dialedNumber.length - 1);
|
||||
_filterContacts();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
void _onClearPress() {
|
||||
setState(() {
|
||||
dialedNumber = "";
|
||||
_filteredContacts = _allContacts;
|
||||
});
|
||||
}
|
||||
|
||||
void _makeCall(String phoneNumber) async {
|
||||
try {
|
||||
await _callService.makeGsmCall(context, phoneNumber: phoneNumber);
|
||||
setState(() {
|
||||
dialedNumber = phoneNumber;
|
||||
});
|
||||
} catch (e) {
|
||||
debugPrint("Error making call: $e");
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('Failed to make call: $e')),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void _launchSms(String phoneNumber) async {
|
||||
final uri = Uri(scheme: 'sms', path: phoneNumber);
|
||||
if (await canLaunchUrl(uri)) {
|
||||
await launchUrl(uri);
|
||||
} else {
|
||||
debugPrint('Could not send SMS to $phoneNumber');
|
||||
}
|
||||
}
|
||||
|
||||
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
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.black,
|
||||
body: Stack(
|
||||
children: [
|
||||
Column(
|
||||
children: [
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.only(
|
||||
top: 42.0, left: 16.0, right: 16.0, bottom: 16.0),
|
||||
color: Colors.black,
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Expanded(
|
||||
child: ListView(
|
||||
children: [
|
||||
..._filteredContacts.map((contact) {
|
||||
final phoneNumber = contact.phones.isNotEmpty
|
||||
? contact.phones.first.number
|
||||
: 'No phone number';
|
||||
return ListTile(
|
||||
title: Text(
|
||||
_obfuscateService.obfuscateData(contact.displayName),
|
||||
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),
|
||||
),
|
||||
trailing: Icon(Icons.add, color: Colors.grey[600]),
|
||||
onTap: _addContact,
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
Expanded(
|
||||
flex: 2,
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
Expanded(
|
||||
child: Align(
|
||||
alignment: Alignment.center,
|
||||
child: Text(
|
||||
dialedNumber,
|
||||
style: const TextStyle(fontSize: 24, color: Colors.white),
|
||||
overflow: TextOverflow.ellipsis,
|
||||
),
|
||||
),
|
||||
),
|
||||
GestureDetector(
|
||||
onTap: _onDeletePress,
|
||||
onLongPress: _onClearPress,
|
||||
child: const Padding(
|
||||
padding: EdgeInsets.all(8.0),
|
||||
child: Icon(Icons.backspace, color: Colors.white),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
_buildDialButton('1', Colors.white),
|
||||
_buildDialButton('2', Colors.white),
|
||||
_buildDialButton('3', Colors.white),
|
||||
],
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
_buildDialButton('4', Colors.white),
|
||||
_buildDialButton('5', Colors.white),
|
||||
_buildDialButton('6', Colors.white),
|
||||
],
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
_buildDialButton('7', Colors.white),
|
||||
_buildDialButton('8', Colors.white),
|
||||
_buildDialButton('9', Colors.white),
|
||||
],
|
||||
),
|
||||
Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
|
||||
children: [
|
||||
_buildDialButton('*', const Color.fromRGBO(117, 117, 117, 1)),
|
||||
_buildDialButtonWithPlus('0'),
|
||||
_buildDialButton('#', const Color.fromRGBO(117, 117, 117, 1)),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
Positioned(
|
||||
bottom: 20.0,
|
||||
left: 0,
|
||||
right: 0,
|
||||
child: Center(
|
||||
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),
|
||||
),
|
||||
),
|
||||
),
|
||||
Positioned(
|
||||
top: 40.0,
|
||||
left: 16.0,
|
||||
child: IconButton(
|
||||
icon: const Icon(Icons.arrow_back, color: Colors.white),
|
||||
onPressed: () => Navigator.pop(context),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildDialButton(String number, Color textColor) {
|
||||
return ElevatedButton(
|
||||
onPressed: () => _onNumberPress(number),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.black,
|
||||
shape: const CircleBorder(),
|
||||
padding: const EdgeInsets.all(16),
|
||||
),
|
||||
child: Text(
|
||||
number,
|
||||
style: TextStyle(fontSize: 24, color: textColor),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
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]),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
@ -1,20 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../contacts/contact_state.dart';
|
||||
import '../contacts/widgets/alphabet_scroll_page.dart';
|
||||
|
||||
class ContactPage extends StatelessWidget {
|
||||
const ContactPage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final contactState = ContactState.of(context);
|
||||
return Scaffold(
|
||||
body: contactState.loading
|
||||
? const Center(child: CircularProgressIndicator())
|
||||
: AlphabetScrollPage(
|
||||
scrollOffset: contactState.scrollOffset,
|
||||
contacts: contactState.contacts,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -1,232 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_contacts/flutter_contacts.dart';
|
||||
import '../../../../domain/services/obfuscate_service.dart';
|
||||
import 'package:dialer/presentation/common/widgets/username_color_generator.dart';
|
||||
import '../contact_state.dart';
|
||||
import 'add_contact_button.dart';
|
||||
import 'contact_modal.dart';
|
||||
import 'share_own_qr.dart';
|
||||
|
||||
class AlphabetScrollPage extends StatefulWidget {
|
||||
final double scrollOffset;
|
||||
final List<Contact> contacts;
|
||||
|
||||
const AlphabetScrollPage({
|
||||
super.key,
|
||||
required this.scrollOffset,
|
||||
required this.contacts,
|
||||
});
|
||||
|
||||
@override
|
||||
_AlphabetScrollPageState createState() => _AlphabetScrollPageState();
|
||||
}
|
||||
|
||||
class _AlphabetScrollPageState extends State<AlphabetScrollPage> {
|
||||
late ScrollController _scrollController;
|
||||
final ObfuscateService _obfuscateService = ObfuscateService();
|
||||
late Map<String, List<Contact>> _alphabetizedContacts;
|
||||
late List<String> _alphabetKeys;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_scrollController = ScrollController(initialScrollOffset: widget.scrollOffset);
|
||||
_scrollController.addListener(_onScroll);
|
||||
_organizeContacts();
|
||||
}
|
||||
|
||||
@override
|
||||
void didUpdateWidget(AlphabetScrollPage oldWidget) {
|
||||
super.didUpdateWidget(oldWidget);
|
||||
if (oldWidget.contacts != widget.contacts) {
|
||||
_organizeContacts();
|
||||
}
|
||||
}
|
||||
|
||||
void _organizeContacts() {
|
||||
_alphabetizedContacts = {};
|
||||
for (var contact in widget.contacts) {
|
||||
String firstLetter = contact.displayName.isNotEmpty
|
||||
? contact.displayName[0].toUpperCase()
|
||||
: '#';
|
||||
(_alphabetizedContacts[firstLetter] ??= []).add(contact);
|
||||
}
|
||||
_alphabetKeys = _alphabetizedContacts.keys.toList()..sort();
|
||||
}
|
||||
|
||||
void _onScroll() {
|
||||
final contactState = ContactState.of(context);
|
||||
contactState.setScrollOffset(_scrollController.offset);
|
||||
}
|
||||
|
||||
Future<void> _refreshContacts() async {
|
||||
final contactState = ContactState.of(context);
|
||||
try {
|
||||
await contactState.fetchContacts();
|
||||
} catch (e) {
|
||||
debugPrint('Error refreshing contacts: $e');
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('Failed to refresh contacts')),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _toggleFavorite(Contact contact) async {
|
||||
try {
|
||||
if (await FlutterContacts.requestPermission()) {
|
||||
Contact? fullContact = await FlutterContacts.getContact(contact.id,
|
||||
withProperties: true,
|
||||
withAccounts: true,
|
||||
withPhoto: true,
|
||||
withThumbnail: true);
|
||||
|
||||
if (fullContact != null) {
|
||||
fullContact.isStarred = !fullContact.isStarred;
|
||||
await FlutterContacts.updateContact(fullContact);
|
||||
}
|
||||
|
||||
// Check if widget is still mounted before calling functions that use context
|
||||
if (mounted) {
|
||||
await _refreshContacts();
|
||||
}
|
||||
} else {
|
||||
debugPrint("Could not fetch contact details");
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint("Error updating favorite status: $e");
|
||||
// Only show snackbar if still mounted
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('Failed to update contact favorite status')),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final selfContact = ContactState.of(context).selfContact;
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.black,
|
||||
body: Column(
|
||||
children: [
|
||||
// Top buttons row
|
||||
Container(
|
||||
color: Colors.black,
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0, vertical: 8.0),
|
||||
child: Row(
|
||||
mainAxisAlignment: MainAxisAlignment.spaceBetween,
|
||||
children: [
|
||||
AddContactButton(),
|
||||
QRCodeButton(contacts: widget.contacts, selfContact: selfContact),
|
||||
],
|
||||
),
|
||||
),
|
||||
// Contact List
|
||||
Expanded(
|
||||
child: ListView.builder(
|
||||
controller: _scrollController,
|
||||
itemCount: _alphabetKeys.length,
|
||||
itemBuilder: (context, index) {
|
||||
String letter = _alphabetKeys[index];
|
||||
List<Contact> contactsForLetter = _alphabetizedContacts[letter]!;
|
||||
return _buildLetterSection(letter, contactsForLetter);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildLetterSection(String letter, List<Contact> contactsForLetter) {
|
||||
return Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
// Alphabet Letter Header
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 8.0, horizontal: 16.0),
|
||||
child: Text(
|
||||
letter,
|
||||
style: const TextStyle(
|
||||
fontSize: 28,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
// Contact Entries
|
||||
...contactsForLetter.map((contact) => _buildContactTile(contact)),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
Widget _buildContactTile(Contact contact) {
|
||||
String phoneNumber = contact.phones.isNotEmpty
|
||||
? _obfuscateService.obfuscateData(contact.phones.first.number)
|
||||
: 'No phone number';
|
||||
Color avatarColor = generateColorFromName(contact.displayName);
|
||||
|
||||
return ListTile(
|
||||
leading: ObfuscatedAvatar(
|
||||
imageBytes: contact.thumbnail,
|
||||
radius: 25,
|
||||
backgroundColor: avatarColor,
|
||||
fallbackInitial: contact.displayName,
|
||||
),
|
||||
title: Text(
|
||||
_obfuscateService.obfuscateData(contact.displayName),
|
||||
style: const TextStyle(color: Colors.white),
|
||||
),
|
||||
subtitle: Text(
|
||||
phoneNumber,
|
||||
style: const TextStyle(color: Colors.white70),
|
||||
),
|
||||
onTap: () => _showContactModal(contact),
|
||||
);
|
||||
}
|
||||
|
||||
void _showContactModal(Contact contact) {
|
||||
showModalBottomSheet(
|
||||
context: context,
|
||||
isScrollControlled: true,
|
||||
backgroundColor: Colors.transparent,
|
||||
builder: (context) {
|
||||
return ContactModal(
|
||||
contact: contact,
|
||||
onEdit: () => _onEditContact(contact),
|
||||
onToggleFavorite: () => _toggleFavorite(contact),
|
||||
isFavorite: contact.isStarred,
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
Future<void> _onEditContact(Contact contact) async {
|
||||
if (await FlutterContacts.requestPermission()) {
|
||||
final updatedContact = await FlutterContacts.openExternalEdit(contact.id);
|
||||
if (updatedContact != null) {
|
||||
await _refreshContacts();
|
||||
Navigator.of(context).pop();
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('${contact.displayName} updated successfully!'),
|
||||
),
|
||||
);
|
||||
} else {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text('Edit canceled or failed.'),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
void dispose() {
|
||||
_scrollController.dispose();
|
||||
super.dispose();
|
||||
}
|
||||
}
|
@ -1,377 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_contacts/flutter_contacts.dart';
|
||||
import 'package:url_launcher/url_launcher.dart';
|
||||
import '../../../common/widgets/username_color_generator.dart';
|
||||
import '../../../common/widgets/color_darkener.dart';
|
||||
import '../../../../domain/services/obfuscate_service.dart';
|
||||
import '../../../../domain/services/block_service.dart';
|
||||
import '../../../../domain/services/contact_service.dart';
|
||||
import '../../../../domain/services/call_service.dart';
|
||||
|
||||
class ContactModal extends StatefulWidget {
|
||||
final Contact contact;
|
||||
final Function onEdit;
|
||||
final Function onToggleFavorite;
|
||||
final bool isFavorite;
|
||||
|
||||
const ContactModal({
|
||||
super.key,
|
||||
required this.contact,
|
||||
required this.onEdit,
|
||||
required this.onToggleFavorite,
|
||||
required this.isFavorite,
|
||||
});
|
||||
|
||||
@override
|
||||
_ContactModalState createState() => _ContactModalState();
|
||||
}
|
||||
|
||||
class _ContactModalState extends State<ContactModal> {
|
||||
late String phoneNumber;
|
||||
bool isBlocked = false;
|
||||
final ObfuscateService _obfuscateService = ObfuscateService();
|
||||
final CallService _callService = CallService();
|
||||
final ContactService _contactService = ContactService();
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
phoneNumber = widget.contact.phones.isNotEmpty
|
||||
? widget.contact.phones.first.number
|
||||
: 'No phone number';
|
||||
_checkIfBlocked();
|
||||
}
|
||||
|
||||
Future<void> _checkIfBlocked() async {
|
||||
if (phoneNumber != 'No phone number') {
|
||||
bool blocked = await BlockService().isNumberBlocked(phoneNumber);
|
||||
if (mounted) {
|
||||
setState(() {
|
||||
isBlocked = blocked;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> _toggleBlockState() async {
|
||||
if (phoneNumber == 'No phone number') {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('No phone number to block or unblock')),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (isBlocked) {
|
||||
await BlockService().unblockNumber(phoneNumber);
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('$phoneNumber has been unblocked')),
|
||||
);
|
||||
}
|
||||
} else {
|
||||
await BlockService().blockNumber(phoneNumber);
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('$phoneNumber has been blocked')),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
if (phoneNumber != 'No phone number' && mounted) {
|
||||
_checkIfBlocked();
|
||||
}
|
||||
|
||||
if (mounted) {
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
}
|
||||
|
||||
void _launchSms(String phoneNumber) async {
|
||||
final uri = Uri(scheme: 'sms', path: phoneNumber);
|
||||
if (await canLaunchUrl(uri)) {
|
||||
await launchUrl(uri);
|
||||
} else {
|
||||
debugPrint('Could not launch SMS to $phoneNumber');
|
||||
}
|
||||
}
|
||||
|
||||
void _launchEmail(String email) async {
|
||||
final uri = Uri(scheme: 'mailto', path: email);
|
||||
if (await canLaunchUrl(uri)) {
|
||||
await launchUrl(uri);
|
||||
} else {
|
||||
debugPrint('Could not launch email to $email');
|
||||
}
|
||||
}
|
||||
|
||||
void _deleteContact() async {
|
||||
final bool shouldDelete = await showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('Delete Contact'),
|
||||
content: Text(
|
||||
'Are you sure you want to delete ${_obfuscateService.obfuscateData(widget.contact.displayName)}?'),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(false),
|
||||
child: const Text('Cancel'),
|
||||
),
|
||||
TextButton(
|
||||
onPressed: () => Navigator.of(context).pop(true),
|
||||
child: const Text('Delete'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
|
||||
if (shouldDelete && mounted) {
|
||||
try {
|
||||
// Delete the contact
|
||||
await FlutterContacts.deleteContact(widget.contact);
|
||||
|
||||
// Show success message
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text(
|
||||
'${_obfuscateService.obfuscateData(widget.contact.displayName)} deleted')),
|
||||
);
|
||||
|
||||
// Close the modal
|
||||
Navigator.of(context).pop();
|
||||
}
|
||||
} catch (e) {
|
||||
// Handle errors and show a failure message
|
||||
if (mounted) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content:
|
||||
Text('Failed to delete ${widget.contact.displayName}: $e')),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void _shareContactAsQRCode() {
|
||||
// Use the ContactService to show the QR code for the contact's vCard
|
||||
_contactService.showContactQRCodeDialog(context, widget.contact);
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
String email = widget.contact.emails.isNotEmpty
|
||||
? _obfuscateService.obfuscateData(widget.contact.emails.first.address)
|
||||
: 'No email';
|
||||
|
||||
final avatarColor = generateColorFromName(widget.contact.displayName);
|
||||
|
||||
return GestureDetector(
|
||||
onTap: () => Navigator.of(context).pop(),
|
||||
child: Container(
|
||||
color: Colors.black.withOpacity(0.5),
|
||||
child: GestureDetector(
|
||||
onTap: () {},
|
||||
child: FractionallySizedBox(
|
||||
heightFactor: 0.8,
|
||||
child: Container(
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey[900],
|
||||
borderRadius:
|
||||
const BorderRadius.vertical(top: Radius.circular(20)),
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
// Modal Handle and Three-Dot Menu
|
||||
Stack(
|
||||
children: [
|
||||
Align(
|
||||
alignment: Alignment.center,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(vertical: 10),
|
||||
child: Container(
|
||||
width: 50,
|
||||
height: 5,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.grey.shade300,
|
||||
borderRadius: BorderRadius.circular(5),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
Align(
|
||||
alignment: Alignment.topRight,
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.only(top: 10, right: 10),
|
||||
child: PopupMenuButton<String>(
|
||||
icon: const Icon(Icons.more_vert,
|
||||
color: Colors.white),
|
||||
onSelected: (String choice) {
|
||||
if (choice == 'delete') {
|
||||
_deleteContact();
|
||||
} else if (choice == 'share') {
|
||||
_shareContactAsQRCode();
|
||||
}
|
||||
// Handle other choices if needed
|
||||
},
|
||||
itemBuilder: (BuildContext context) {
|
||||
return [
|
||||
const PopupMenuItem<String>(
|
||||
value: 'delete',
|
||||
child: Text('Delete'),
|
||||
),
|
||||
const PopupMenuItem<String>(
|
||||
value: 'share',
|
||||
child: Text('Share (via QR code)'),
|
||||
),
|
||||
];
|
||||
},
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
// Contact Profile
|
||||
Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
children: [
|
||||
widget.contact.thumbnail != null && widget.contact.thumbnail!.isNotEmpty
|
||||
? ClipOval(
|
||||
child: Image.memory(
|
||||
widget.contact.thumbnail!,
|
||||
fit: BoxFit.cover,
|
||||
width: 100,
|
||||
height: 100,
|
||||
),
|
||||
)
|
||||
: CircleAvatar(
|
||||
backgroundColor: avatarColor,
|
||||
radius: 50,
|
||||
child: Text(
|
||||
widget.contact.displayName.isNotEmpty
|
||||
? widget.contact.displayName[0].toUpperCase()
|
||||
: '?',
|
||||
style: TextStyle(
|
||||
color: darken(avatarColor),
|
||||
fontSize: 40,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Text(
|
||||
_obfuscateService
|
||||
.obfuscateData(widget.contact.displayName),
|
||||
style: const TextStyle(
|
||||
fontSize: 24,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const Divider(color: Colors.grey),
|
||||
// Contact Actions
|
||||
Expanded(
|
||||
child: SingleChildScrollView(
|
||||
child: Column(
|
||||
children: [
|
||||
ListTile(
|
||||
leading: const Icon(Icons.phone, color: Colors.green),
|
||||
title: Text(
|
||||
_obfuscateService.obfuscateData(phoneNumber),
|
||||
style: const TextStyle(color: Colors.white),
|
||||
),
|
||||
onTap: () async {
|
||||
if (widget.contact.phones.isNotEmpty) {
|
||||
await _callService.makeGsmCall(context,
|
||||
phoneNumber: phoneNumber);
|
||||
}
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.message, color: Colors.blue),
|
||||
title: Text(
|
||||
_obfuscateService.obfuscateData(phoneNumber),
|
||||
style: const TextStyle(color: Colors.white),
|
||||
),
|
||||
onTap: () {
|
||||
if (widget.contact.phones.isNotEmpty) {
|
||||
_launchSms(phoneNumber);
|
||||
}
|
||||
},
|
||||
),
|
||||
ListTile(
|
||||
leading: const Icon(Icons.email, color: Colors.orange),
|
||||
title: Text(
|
||||
email,
|
||||
style: const TextStyle(color: Colors.white),
|
||||
),
|
||||
onTap: () {
|
||||
if (widget.contact.emails.isNotEmpty) {
|
||||
_launchEmail(email);
|
||||
}
|
||||
},
|
||||
),
|
||||
const Divider(color: Colors.grey),
|
||||
// Favorite, Edit, and Block/Unblock Buttons
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 16.0),
|
||||
child: Column(
|
||||
children: [
|
||||
// Favorite button
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: () {
|
||||
// First close the modal to avoid unmounted widget issues
|
||||
Navigator.of(context).pop();
|
||||
// Then toggle the favorite status
|
||||
widget.onToggleFavorite();
|
||||
},
|
||||
icon: Icon(widget.isFavorite
|
||||
? Icons.star
|
||||
: Icons.star_border),
|
||||
label: Text(
|
||||
widget.isFavorite ? 'Unfavorite' : 'Favorite'),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
// Edit button
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: () => widget.onEdit(),
|
||||
icon: const Icon(Icons.edit),
|
||||
label: const Text('Edit Contact'),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
// Block/Unblock button
|
||||
SizedBox(
|
||||
width: double.infinity,
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: _toggleBlockState,
|
||||
icon: Icon(
|
||||
isBlocked ? Icons.block : Icons.block_flipped),
|
||||
label: Text(isBlocked ? 'Unblock' : 'Block'),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -1,37 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import '../contacts/contact_state.dart';
|
||||
import '../contacts/widgets/alphabet_scroll_page.dart';
|
||||
|
||||
class FavoritesPage extends StatelessWidget {
|
||||
const FavoritesPage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
final contactState = ContactState.of(context);
|
||||
|
||||
if (contactState.loading) {
|
||||
return const Scaffold(
|
||||
backgroundColor: Colors.black,
|
||||
body: Center(child: CircularProgressIndicator()),
|
||||
);
|
||||
}
|
||||
|
||||
final favorites = contactState.favoriteContacts;
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.black,
|
||||
body: favorites.isEmpty
|
||||
? const Center(
|
||||
child: Text(
|
||||
'No favorites yet.\nStar your contacts to add them here.',
|
||||
style: TextStyle(color: Colors.white60),
|
||||
textAlign: TextAlign.center,
|
||||
),
|
||||
)
|
||||
: AlphabetScrollPage(
|
||||
scrollOffset: contactState.scrollOffset,
|
||||
contacts: favorites,
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -1,102 +0,0 @@
|
||||
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'),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -1,32 +0,0 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
/// Calling settings configuration page
|
||||
class SettingsCallPage extends StatelessWidget {
|
||||
const SettingsCallPage({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
backgroundColor: Colors.black,
|
||||
appBar: AppBar(
|
||||
title: const Text('Call Settings'),
|
||||
),
|
||||
body: ListView(
|
||||
children: const [
|
||||
ListTile(
|
||||
title: Text('Call Forwarding', style: TextStyle(color: Colors.white)),
|
||||
subtitle: Text('Manage call forwarding options', style: TextStyle(color: Colors.grey)),
|
||||
),
|
||||
ListTile(
|
||||
title: Text('Call Waiting', style: TextStyle(color: Colors.white)),
|
||||
subtitle: Text('Enable or disable call waiting', style: TextStyle(color: Colors.grey)),
|
||||
),
|
||||
ListTile(
|
||||
title: Text('Caller ID', style: TextStyle(color: Colors.white)),
|
||||
subtitle: Text('Manage your caller ID settings', style: TextStyle(color: Colors.grey)),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
52
dialer/lib/services/block_service.dart
Normal file
52
dialer/lib/services/block_service.dart
Normal file
@ -0,0 +1,52 @@
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
class BlockService {
|
||||
static final BlockService _instance = BlockService._internal();
|
||||
|
||||
factory BlockService() {
|
||||
return _instance;
|
||||
}
|
||||
|
||||
BlockService._internal();
|
||||
|
||||
// Function to add a number to the blocked list
|
||||
Future<void> blockNumber(String number) async {
|
||||
if (number.isEmpty) return;
|
||||
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
List<String> blockedNumbers = prefs.getStringList('blockedNumbers') ?? [];
|
||||
|
||||
if (!blockedNumbers.contains(number)) {
|
||||
blockedNumbers.add(number);
|
||||
await prefs.setStringList('blockedNumbers', blockedNumbers);
|
||||
print('$number has been blocked');
|
||||
} else {
|
||||
print('$number is already blocked');
|
||||
}
|
||||
}
|
||||
|
||||
// Function to remove a number from the blocked list
|
||||
Future<void> unblockNumber(String number) async {
|
||||
if (number.isEmpty) return;
|
||||
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
List<String> blockedNumbers = prefs.getStringList('blockedNumbers') ?? [];
|
||||
|
||||
if (blockedNumbers.contains(number)) {
|
||||
blockedNumbers.remove(number);
|
||||
await prefs.setStringList('blockedNumbers', blockedNumbers);
|
||||
print('$number has been unblocked');
|
||||
} else {
|
||||
print('$number is not blocked');
|
||||
}
|
||||
}
|
||||
|
||||
// Check if a number is blocked
|
||||
Future<bool> isNumberBlocked(String number) async {
|
||||
if (number.isEmpty) return false;
|
||||
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
List<String> blockedNumbers = prefs.getStringList('blockedNumbers') ?? [];
|
||||
return blockedNumbers.contains(number);
|
||||
}
|
||||
}
|
26
dialer/lib/services/call_service.dart
Normal file
26
dialer/lib/services/call_service.dart
Normal file
@ -0,0 +1,26 @@
|
||||
import 'package:flutter/services.dart';
|
||||
|
||||
// Service to manage call-related operations
|
||||
class CallService {
|
||||
static const MethodChannel _channel = MethodChannel('call_service');
|
||||
|
||||
// Function to make a GSM call
|
||||
Future<void> makeGsmCall(String phoneNumber) async {
|
||||
try {
|
||||
await _channel.invokeMethod('makeGsmCall', {"phoneNumber": phoneNumber});
|
||||
} catch (e) {
|
||||
print("Error making call: $e");
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
|
||||
// Function to hang up the current call
|
||||
Future<void> hangUpCall() async {
|
||||
try {
|
||||
await _channel.invokeMethod('hangUpCall');
|
||||
} catch (e) {
|
||||
print("Error hanging up call: $e");
|
||||
rethrow;
|
||||
}
|
||||
}
|
||||
}
|
51
dialer/lib/services/contact_service.dart
Normal file
51
dialer/lib/services/contact_service.dart
Normal file
@ -0,0 +1,51 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_contacts/flutter_contacts.dart';
|
||||
import 'package:qr_flutter/qr_flutter.dart';
|
||||
|
||||
// Service to manage contact-related operations
|
||||
class ContactService {
|
||||
Future<List<Contact>> fetchContacts() async {
|
||||
if (await FlutterContacts.requestPermission()) {
|
||||
return await FlutterContacts.getContacts(
|
||||
withProperties: true,
|
||||
withThumbnail: true,
|
||||
withAccounts: true,
|
||||
withGroups: true,
|
||||
withPhoto: true);
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
||||
Future<List<Contact>> fetchFavoriteContacts() async {
|
||||
List<Contact> contacts = await fetchContacts();
|
||||
return contacts.where((contact) => contact.isStarred).toList();
|
||||
}
|
||||
|
||||
Future<void> addNewContact(Contact contact) async {
|
||||
await FlutterContacts.insertContact(contact);
|
||||
}
|
||||
|
||||
// Function to show an AlertDialog with a QR code for the contact's vCard
|
||||
void showContactQRCodeDialog(BuildContext context, Contact contact) {
|
||||
showDialog(
|
||||
barrierColor: Colors.white24,
|
||||
context: context,
|
||||
barrierDismissible: true,
|
||||
builder: (BuildContext context) {
|
||||
return AlertDialog(
|
||||
backgroundColor: Colors.black,
|
||||
content: SizedBox(
|
||||
width: 200,
|
||||
height: 220,
|
||||
child: QrImageView(
|
||||
data: contact.toVCard(), // Generate vCard QR code
|
||||
version: QrVersions.auto,
|
||||
backgroundColor: Colors.white, // Make sure QR code is visible on black background
|
||||
size: 200.0,
|
||||
),
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
@ -10,7 +10,7 @@ class AsymmetricCryptoService {
|
||||
final String _aliasPrefix = 'icing_';
|
||||
final Uuid _uuid = Uuid();
|
||||
|
||||
/// Generates an ED25519 key pair with a unique alias and stores its metadata.
|
||||
/// Generates an ECDSA P-256 key pair with a unique alias and stores its metadata.
|
||||
Future<String> generateKeyPair({String? label}) async {
|
||||
try {
|
||||
// Generate a unique identifier for the key
|
@ -1,7 +1,7 @@
|
||||
// lib/services/obfuscate_service.dart
|
||||
import 'package:dialer/presentation/common/widgets/color_darkener.dart';
|
||||
import 'package:dialer/widgets/color_darkener.dart';
|
||||
|
||||
import '../../core/config/app_config.dart';
|
||||
import '../../globals.dart' as globals;
|
||||
import 'dart:ui';
|
||||
import 'dart:typed_data';
|
||||
import 'package:flutter/material.dart';
|
||||
@ -20,7 +20,7 @@ class ObfuscateService {
|
||||
|
||||
// Public method to obfuscate data
|
||||
String obfuscateData(String data) {
|
||||
if (AppConfig.isStealthMode) {
|
||||
if (globals.isStealthMode) {
|
||||
return _obfuscateData(data);
|
||||
} else {
|
||||
return data;
|
||||
@ -61,7 +61,7 @@ class ObfuscatedAvatar extends StatelessWidget {
|
||||
if (imageBytes != null && imageBytes!.isNotEmpty) {
|
||||
return ClipOval(
|
||||
child: ImageFiltered(
|
||||
imageFilter: AppConfig.isStealthMode
|
||||
imageFilter: globals.isStealthMode
|
||||
? ImageFilter.blur(sigmaX: 10, sigmaY: 10)
|
||||
: ImageFilter.blur(sigmaX: 0, sigmaY: 0),
|
||||
child: Image.memory(
|
10
dialer/lib/widgets/color_darkener.dart
Normal file
10
dialer/lib/widgets/color_darkener.dart
Normal file
@ -0,0 +1,10 @@
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
Color darken(Color color, [double amount = .1]) {
|
||||
assert(amount >= 0 && amount <= 1);
|
||||
|
||||
final hsl = HSLColor.fromColor(color);
|
||||
final hslDark = hsl.withLightness((hsl.lightness - amount).clamp(0.0, 1.0));
|
||||
|
||||
return hslDark.toColor();
|
||||
}
|
12
dialer/lib/widgets/username_color_generator.dart
Normal file
12
dialer/lib/widgets/username_color_generator.dart
Normal file
@ -0,0 +1,12 @@
|
||||
import 'dart:math';
|
||||
import 'package:flutter/material.dart';
|
||||
|
||||
Color generateColorFromName(String name) {
|
||||
final random = Random(name.hashCode);
|
||||
return Color.fromARGB(
|
||||
255,
|
||||
random.nextInt(256),
|
||||
random.nextInt(256),
|
||||
random.nextInt(256),
|
||||
);
|
||||
}
|
@ -55,6 +55,7 @@ dependencies:
|
||||
encrypt: ^5.0.3
|
||||
uuid: ^4.5.1
|
||||
provider: ^6.1.2
|
||||
package_info_plus: ^8.3.0
|
||||
|
||||
intl: any
|
||||
|
||||
|
@ -27,13 +27,14 @@ The protocol definition will include as completed:
|
||||
- Handshakes
|
||||
- Real-time data-stream encryption (and decryption)
|
||||
- Encrypted stream compression
|
||||
- Transmission over audio stream (at least one modulation type)
|
||||
- First steps in FEC (Forward Error Correction): detecting half of transmission errors
|
||||
- Transmission over audio stream
|
||||
- Minimal error correction in audio-based transmission
|
||||
- Error handling and user prevention
|
||||
|
||||
And should include prototype or scratches functionalities, among which:
|
||||
- Embedded silent data transmission (such as DTMF)
|
||||
- Embedded silent data transmission (silently transmit light data during an encrypted phone call)
|
||||
- On-the-fly key exchange (does not require prior key exchange, sacrifying some security)
|
||||
- Stronger FEC: detecting >80%, correcting 20% of transmission errors
|
||||
- Strong error correction
|
||||
|
||||
#### The Icing dialer (based on Icing kotlin library, an Icing protocol implementation)
|
||||
|
||||
@ -127,15 +128,16 @@ 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,
|
||||
but also that the call is coming from Jeff's phone and not an impersonator.
|
||||
|
||||
Elise, 42 years-old, is a journalist covering sensitive topics.
|
||||
Her work draws attention from people who want to know what she's saying - and to whom.
|
||||
Forced to stay discreet, with unreliable signal and a likely monitored phone line,
|
||||
she uses Icing dialer to make secure calls without exposing herself.
|
||||
Elise is a 42 years-old extreme reporter.
|
||||
After interviewing Russians opposition's leader, the FSB is looking to interview her.
|
||||
She tries to stay discreet and hidden, but those measures constrains her to barely receive cellular network.
|
||||
She suspects her phone line to be monitored, so the best she can do to call safely, is to use her Icing dialer.
|
||||
|
||||
Paul, a 22 years-old developer, is enjoying its vacations abroad.
|
||||
Paul, a 22 years-old developer working for a big company, decides to go to China for vacations.
|
||||
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 his country.
|
||||
With Icing dialer, he can call his collegues and help fix the problem, completely safe.
|
||||
qualified to fix it. Paul doesn't have WiFi and his phone plan only covers voice calls in China.
|
||||
With Icing dialer, he can call his collegues and help fix the
|
||||
problem, safe from potential Chinese spies.
|
||||
|
||||
## Evaluation Criteria
|
||||
### Protocol and lib
|
||||
|
@ -1,79 +0,0 @@
|
||||
# client_state.py
|
||||
from queue import Queue
|
||||
from session import NoiseXKSession
|
||||
import time
|
||||
|
||||
class ClientState:
|
||||
def __init__(self, client_id):
|
||||
self.client_id = client_id
|
||||
self.command_queue = Queue()
|
||||
self.initiator = None
|
||||
self.keypair = None
|
||||
self.peer_pubkey = None
|
||||
self.session = None
|
||||
self.handshake_in_progress = False
|
||||
self.handshake_start_time = None
|
||||
self.call_active = False
|
||||
|
||||
def process_command(self, client):
|
||||
"""Process commands from the queue."""
|
||||
if not self.command_queue.empty():
|
||||
print(f"Client {self.client_id} processing command queue, size: {self.command_queue.qsize()}")
|
||||
command = self.command_queue.get()
|
||||
if command == "handshake":
|
||||
try:
|
||||
print(f"Client {self.client_id} starting handshake, initiator: {self.initiator}")
|
||||
self.session = NoiseXKSession(self.keypair, self.peer_pubkey)
|
||||
self.session.handshake(client.sock, self.initiator)
|
||||
print(f"Client {self.client_id} handshake complete")
|
||||
client.send("HANDSHAKE_DONE")
|
||||
except Exception as e:
|
||||
print(f"Client {self.client_id} handshake failed: {e}")
|
||||
client.state_changed.emit("CALL_END", "", self.client_id)
|
||||
finally:
|
||||
self.handshake_in_progress = False
|
||||
self.handshake_start_time = None
|
||||
|
||||
def start_handshake(self, initiator, keypair, peer_pubkey):
|
||||
"""Queue handshake command."""
|
||||
self.initiator = initiator
|
||||
self.keypair = keypair
|
||||
self.peer_pubkey = peer_pubkey
|
||||
print(f"Client {self.client_id} queuing handshake, initiator: {initiator}")
|
||||
self.handshake_in_progress = True
|
||||
self.handshake_start_time = time.time()
|
||||
self.command_queue.put("handshake")
|
||||
|
||||
def handle_data(self, client, data):
|
||||
"""Handle received data (control or audio)."""
|
||||
try:
|
||||
decoded_data = data.decode('utf-8').strip()
|
||||
print(f"Client {self.client_id} received raw: {decoded_data}")
|
||||
if decoded_data in ["RINGING", "CALL_END", "CALL_DROPPED", "IN_CALL", "HANDSHAKE", "HANDSHAKE_DONE"]:
|
||||
client.state_changed.emit(decoded_data, decoded_data, self.client_id)
|
||||
if decoded_data == "HANDSHAKE":
|
||||
self.handshake_in_progress = True
|
||||
elif decoded_data == "HANDSHAKE_DONE":
|
||||
self.call_active = True
|
||||
else:
|
||||
print(f"Client {self.client_id} ignored unexpected text message: {decoded_data}")
|
||||
except UnicodeDecodeError:
|
||||
if self.call_active and self.session:
|
||||
try:
|
||||
print(f"Client {self.client_id} received audio packet, length={len(data)}")
|
||||
decrypted_data = self.session.decrypt(data)
|
||||
print(f"Client {self.client_id} decrypted audio packet, length={len(decrypted_data)}")
|
||||
client.data_received.emit(decrypted_data, self.client_id)
|
||||
except Exception as e:
|
||||
print(f"Client {self.client_id} failed to process audio packet: {e}")
|
||||
else:
|
||||
print(f"Client {self.client_id} ignored non-text message: {data.hex()}")
|
||||
|
||||
def check_handshake_timeout(self, client):
|
||||
"""Check for handshake timeout."""
|
||||
if self.handshake_in_progress and self.handshake_start_time:
|
||||
if time.time() - self.handshake_start_time > 30:
|
||||
print(f"Client {self.client_id} handshake timeout after 30s")
|
||||
client.state_changed.emit("CALL_END", "", self.client_id)
|
||||
self.handshake_in_progress = False
|
||||
self.handshake_start_time = None
|
@ -1,215 +0,0 @@
|
||||
import sys
|
||||
from PyQt5.QtWidgets import (
|
||||
QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
|
||||
QPushButton, QLabel, QFrame, QSizePolicy, QStyle
|
||||
)
|
||||
from PyQt5.QtCore import Qt, QSize
|
||||
from PyQt5.QtGui import QFont
|
||||
from phone_manager import PhoneManager
|
||||
from waveform_widget import WaveformWidget
|
||||
from phone_state import PhoneState
|
||||
|
||||
class PhoneUI(QMainWindow):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.setWindowTitle("Enhanced Dual Phone Interface")
|
||||
self.setGeometry(100, 100, 900, 750)
|
||||
self.setStyleSheet("""
|
||||
QMainWindow { background-color: #333333; }
|
||||
QLabel { color: #E0E0E0; font-size: 14px; }
|
||||
QPushButton {
|
||||
background-color: #0078D4; color: white; border: none;
|
||||
padding: 10px 15px; border-radius: 5px; font-size: 14px;
|
||||
min-height: 30px;
|
||||
}
|
||||
QPushButton:hover { background-color: #005A9E; }
|
||||
QPushButton:pressed { background-color: #003C6B; }
|
||||
QPushButton#settingsButton { background-color: #555555; }
|
||||
QPushButton#settingsButton:hover { background-color: #777777; }
|
||||
QFrame#phoneDisplay {
|
||||
background-color: #1E1E1E; border: 2px solid #0078D4;
|
||||
border-radius: 10px;
|
||||
}
|
||||
QLabel#phoneTitleLabel {
|
||||
font-size: 18px; font-weight: bold; padding-bottom: 5px;
|
||||
color: #FFFFFF;
|
||||
}
|
||||
QLabel#mainTitleLabel {
|
||||
font-size: 24px; font-weight: bold; color: #00A2E8;
|
||||
padding: 15px;
|
||||
}
|
||||
QWidget#phoneWidget {
|
||||
border: 1px solid #4A4A4A; border-radius: 8px;
|
||||
padding: 10px; background-color: #3A3A3A;
|
||||
}
|
||||
""")
|
||||
|
||||
self.manager = PhoneManager()
|
||||
self.manager.initialize_phones()
|
||||
|
||||
# Main widget and layout
|
||||
main_widget = QWidget()
|
||||
self.setCentralWidget(main_widget)
|
||||
main_layout = QVBoxLayout()
|
||||
main_layout.setSpacing(20)
|
||||
main_layout.setContentsMargins(20, 20, 20, 20)
|
||||
main_layout.setAlignment(Qt.AlignCenter)
|
||||
main_widget.setLayout(main_layout)
|
||||
|
||||
# App Title
|
||||
app_title_label = QLabel("Dual Phone Control Panel")
|
||||
app_title_label.setObjectName("mainTitleLabel")
|
||||
app_title_label.setAlignment(Qt.AlignCenter)
|
||||
main_layout.addWidget(app_title_label)
|
||||
|
||||
# Phone displays layout
|
||||
phone_controls_layout = QHBoxLayout()
|
||||
phone_controls_layout.setSpacing(50)
|
||||
phone_controls_layout.setAlignment(Qt.AlignCenter)
|
||||
main_layout.addLayout(phone_controls_layout)
|
||||
|
||||
# Setup UI for phones
|
||||
for phone in self.manager.phones:
|
||||
phone_container_widget, phone_display_frame, phone_button, waveform_widget, sent_waveform_widget, phone_status_label = self._create_phone_ui(
|
||||
f"Phone {phone['id']+1}", lambda checked, pid=phone['id']: self.manager.phone_action(pid, self)
|
||||
)
|
||||
phone['button'] = phone_button
|
||||
phone['waveform'] = waveform_widget
|
||||
phone['sent_waveform'] = sent_waveform_widget
|
||||
phone['status_label'] = phone_status_label
|
||||
phone_controls_layout.addWidget(phone_container_widget)
|
||||
phone['client'].data_received.connect(lambda data, cid=phone['id']: self.manager.update_waveform(cid, data))
|
||||
phone['client'].state_changed.connect(lambda state, num, cid=phone['id']: self.set_phone_state(cid, state, num))
|
||||
phone['client'].start()
|
||||
|
||||
# Spacer
|
||||
main_layout.addStretch(1)
|
||||
|
||||
# Settings Button
|
||||
self.settings_button = QPushButton("Settings")
|
||||
self.settings_button.setObjectName("settingsButton")
|
||||
self.settings_button.setFixedWidth(180)
|
||||
self.settings_button.setIcon(self.style().standardIcon(QStyle.SP_FileDialogDetailedView))
|
||||
self.settings_button.setIconSize(QSize(20, 20))
|
||||
self.settings_button.clicked.connect(self.settings_action)
|
||||
settings_layout = QHBoxLayout()
|
||||
settings_layout.addStretch()
|
||||
settings_layout.addWidget(self.settings_button)
|
||||
settings_layout.addStretch()
|
||||
main_layout.addLayout(settings_layout)
|
||||
|
||||
# Initialize UI
|
||||
for phone in self.manager.phones:
|
||||
self.update_phone_ui(phone['id'])
|
||||
|
||||
def _create_phone_ui(self, title, action_slot):
|
||||
phone_container_widget = QWidget()
|
||||
phone_container_widget.setObjectName("phoneWidget")
|
||||
phone_layout = QVBoxLayout()
|
||||
phone_layout.setAlignment(Qt.AlignCenter)
|
||||
phone_layout.setSpacing(15)
|
||||
phone_container_widget.setLayout(phone_layout)
|
||||
|
||||
phone_title_label = QLabel(title)
|
||||
phone_title_label.setObjectName("phoneTitleLabel")
|
||||
phone_title_label.setAlignment(Qt.AlignCenter)
|
||||
phone_layout.addWidget(phone_title_label)
|
||||
|
||||
phone_display_frame = QFrame()
|
||||
phone_display_frame.setObjectName("phoneDisplay")
|
||||
phone_display_frame.setFixedSize(250, 350)
|
||||
phone_display_frame.setSizePolicy(QSizePolicy.Fixed, QSizePolicy.Fixed)
|
||||
|
||||
display_content_layout = QVBoxLayout(phone_display_frame)
|
||||
display_content_layout.setAlignment(Qt.AlignCenter)
|
||||
phone_status_label = QLabel("Idle")
|
||||
phone_status_label.setAlignment(Qt.AlignCenter)
|
||||
phone_status_label.setFont(QFont("Arial", 16))
|
||||
display_content_layout.addWidget(phone_status_label)
|
||||
phone_layout.addWidget(phone_display_frame, alignment=Qt.AlignCenter)
|
||||
|
||||
phone_button = QPushButton()
|
||||
phone_button.setFixedWidth(120)
|
||||
phone_button.setIconSize(QSize(20, 20))
|
||||
phone_button.clicked.connect(action_slot)
|
||||
phone_layout.addWidget(phone_button, alignment=Qt.AlignCenter)
|
||||
|
||||
# Received waveform
|
||||
waveform_label = QLabel(f"{title} Received Audio")
|
||||
waveform_label.setAlignment(Qt.AlignCenter)
|
||||
waveform_label.setStyleSheet("font-size: 14px; color: #E0E0E0;")
|
||||
phone_layout.addWidget(waveform_label)
|
||||
waveform_widget = WaveformWidget(dynamic=False)
|
||||
phone_layout.addWidget(waveform_widget, alignment=Qt.AlignCenter)
|
||||
|
||||
# Sent waveform
|
||||
sent_waveform_label = QLabel(f"{title} Sent Audio")
|
||||
sent_waveform_label.setAlignment(Qt.AlignCenter)
|
||||
sent_waveform_label.setStyleSheet("font-size: 14px; color: #E0E0E0;")
|
||||
phone_layout.addWidget(sent_waveform_label)
|
||||
sent_waveform_widget = WaveformWidget(dynamic=False)
|
||||
phone_layout.addWidget(sent_waveform_widget, alignment=Qt.AlignCenter)
|
||||
|
||||
return phone_container_widget, phone_display_frame, phone_button, waveform_widget, sent_waveform_widget, phone_status_label
|
||||
|
||||
def update_phone_ui(self, phone_id):
|
||||
phone = self.manager.phones[phone_id]
|
||||
other_phone = self.manager.phones[1 - phone_id]
|
||||
state = phone['state']
|
||||
phone_number = other_phone['number'] if state != PhoneState.IDLE else ""
|
||||
button = phone['button']
|
||||
status_label = phone['status_label']
|
||||
|
||||
if state == PhoneState.IDLE:
|
||||
button.setText("Call")
|
||||
button.setIcon(self.style().standardIcon(QStyle.SP_MediaPlay))
|
||||
status_label.setText("Idle")
|
||||
button.setStyleSheet("background-color: #0078D4;")
|
||||
elif state == PhoneState.CALLING:
|
||||
button.setText("Cancel")
|
||||
button.setIcon(self.style().standardIcon(QStyle.SP_MediaStop))
|
||||
status_label.setText(f"Calling {phone_number}...")
|
||||
button.setStyleSheet("background-color: #E81123;")
|
||||
elif state == PhoneState.IN_CALL:
|
||||
button.setText("Hang Up")
|
||||
button.setIcon(self.style().standardIcon(QStyle.SP_DialogCancelButton))
|
||||
status_label.setText(f"In Call with {phone_number}")
|
||||
button.setStyleSheet("background-color: #E81123;")
|
||||
elif state == PhoneState.RINGING:
|
||||
button.setText("Answer")
|
||||
button.setIcon(self.style().standardIcon(QStyle.SP_DialogApplyButton))
|
||||
status_label.setText(f"Incoming Call from {phone_number}")
|
||||
button.setStyleSheet("background-color: #107C10;")
|
||||
|
||||
def set_phone_state(self, client_id, state_str, number):
|
||||
state = self.manager.map_state(state_str)
|
||||
phone = self.manager.phones[client_id]
|
||||
other_phone = self.manager.phones[1 - client_id]
|
||||
print(f"Setting state for Phone {client_id + 1}: {state}, number: {number}, is_initiator: {phone['is_initiator']}")
|
||||
phone['state'] = state
|
||||
if state == PhoneState.IN_CALL:
|
||||
print(f"Phone {client_id + 1} confirmed in IN_CALL state")
|
||||
if number == "IN_CALL" and phone['is_initiator']:
|
||||
print(f"Phone {client_id + 1} (initiator) starting handshake")
|
||||
phone['client'].send("HANDSHAKE")
|
||||
phone['client'].start_handshake(initiator=True, keypair=phone['keypair'], peer_pubkey=other_phone['public_key'])
|
||||
elif number == "HANDSHAKE" and not phone['is_initiator']:
|
||||
print(f"Phone {client_id + 1} (responder) starting handshake")
|
||||
phone['client'].start_handshake(initiator=False, keypair=phone['keypair'], peer_pubkey=other_phone['public_key'])
|
||||
elif number == "HANDSHAKE_DONE":
|
||||
self.manager.start_audio(client_id, parent=self) # Pass self as parent
|
||||
self.update_phone_ui(client_id)
|
||||
|
||||
def settings_action(self):
|
||||
print("Settings clicked")
|
||||
|
||||
def closeEvent(self, event):
|
||||
for phone in self.manager.phones:
|
||||
phone['client'].stop()
|
||||
event.accept()
|
||||
|
||||
if __name__ == "__main__":
|
||||
app = QApplication(sys.argv)
|
||||
window = PhoneUI()
|
||||
window.show()
|
||||
sys.exit(app.exec_())
|
@ -1,110 +0,0 @@
|
||||
import socket
|
||||
import time
|
||||
import select
|
||||
from PyQt5.QtCore import QThread, pyqtSignal
|
||||
from client_state import ClientState
|
||||
|
||||
class PhoneClient(QThread):
|
||||
data_received = pyqtSignal(bytes, int)
|
||||
state_changed = pyqtSignal(str, str, int)
|
||||
|
||||
def __init__(self, client_id):
|
||||
super().__init__()
|
||||
self.host = "localhost"
|
||||
self.port = 12345
|
||||
self.client_id = client_id
|
||||
self.sock = None
|
||||
self.running = True
|
||||
self.state = ClientState(client_id)
|
||||
|
||||
def connect_socket(self):
|
||||
retries = 3
|
||||
for attempt in range(retries):
|
||||
try:
|
||||
self.sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
self.sock.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1)
|
||||
self.sock.settimeout(120)
|
||||
self.sock.connect((self.host, self.port))
|
||||
print(f"Client {self.client_id} connected to {self.host}:{self.port}")
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"Client {self.client_id} connection attempt {attempt + 1} failed: {e}")
|
||||
if attempt < retries - 1:
|
||||
time.sleep(1)
|
||||
self.sock = None
|
||||
return False
|
||||
|
||||
def run(self):
|
||||
while self.running:
|
||||
if not self.sock:
|
||||
if not self.connect_socket():
|
||||
print(f"Client {self.client_id} failed to connect after retries")
|
||||
self.state_changed.emit("CALL_END", "", self.client_id)
|
||||
break
|
||||
try:
|
||||
while self.running:
|
||||
self.state.process_command(self)
|
||||
self.state.check_handshake_timeout(self)
|
||||
if not self.state.handshake_in_progress:
|
||||
if self.sock is None:
|
||||
print(f"Client {self.client_id} socket is None, exiting inner loop")
|
||||
break
|
||||
readable, _, _ = select.select([self.sock], [], [], 0.01)
|
||||
if readable:
|
||||
try:
|
||||
if self.sock is None:
|
||||
print(f"Client {self.client_id} socket is None before recv, exiting")
|
||||
break
|
||||
data = self.sock.recv(1024)
|
||||
if not data:
|
||||
print(f"Client {self.client_id} disconnected")
|
||||
self.state_changed.emit("CALL_END", "", self.client_id)
|
||||
break
|
||||
self.state.handle_data(self, data)
|
||||
except socket.error as e:
|
||||
print(f"Client {self.client_id} socket error: {e}")
|
||||
self.state_changed.emit("CALL_END", "", self.client_id)
|
||||
break
|
||||
else:
|
||||
self.msleep(20)
|
||||
print(f"Client {self.client_id} yielding during handshake")
|
||||
self.msleep(1)
|
||||
except Exception as e:
|
||||
print(f"Client {self.client_id} unexpected error in run loop: {e}")
|
||||
self.state_changed.emit("CALL_END", "", self.client_id)
|
||||
break
|
||||
finally:
|
||||
if self.sock:
|
||||
try:
|
||||
self.sock.close()
|
||||
except Exception as e:
|
||||
print(f"Client {self.client_id} error closing socket: {e}")
|
||||
self.sock = None
|
||||
|
||||
def send(self, message):
|
||||
if self.sock and self.running:
|
||||
try:
|
||||
if isinstance(message, str):
|
||||
data = message.encode('utf-8')
|
||||
self.sock.send(data)
|
||||
print(f"Client {self.client_id} sent: {message}, length={len(data)}")
|
||||
else:
|
||||
self.sock.send(message)
|
||||
print(f"Client {self.client_id} sent binary data, length={len(message)}")
|
||||
except socket.error as e:
|
||||
print(f"Client {self.client_id} send error: {e}")
|
||||
self.state_changed.emit("CALL_END", "", self.client_id)
|
||||
|
||||
def stop(self):
|
||||
self.running = False
|
||||
if self.sock:
|
||||
try:
|
||||
self.sock.close()
|
||||
except Exception as e:
|
||||
print(f"Client {self.client_id} error closing socket in stop: {e}")
|
||||
self.sock = None
|
||||
self.quit()
|
||||
self.wait(1000)
|
||||
|
||||
def start_handshake(self, initiator, keypair, peer_pubkey):
|
||||
self.state.start_handshake(initiator, keypair, peer_pubkey)
|
@ -1,98 +0,0 @@
|
||||
import secrets
|
||||
from PyQt5.QtCore import QTimer
|
||||
from phone_client import PhoneClient
|
||||
from session import NoiseXKSession
|
||||
from phone_state import PhoneState # Added import
|
||||
|
||||
class PhoneManager:
|
||||
def __init__(self):
|
||||
self.phones = []
|
||||
self.handshake_done_count = 0
|
||||
|
||||
def initialize_phones(self):
|
||||
for i in range(2):
|
||||
client = PhoneClient(i)
|
||||
keypair = NoiseXKSession.generate_keypair()
|
||||
phone = {
|
||||
'id': i,
|
||||
'client': client,
|
||||
'state': PhoneState.IDLE,
|
||||
'number': "123-4567" if i == 0 else "987-6543",
|
||||
'audio_timer': None,
|
||||
'keypair': keypair,
|
||||
'public_key': keypair.public,
|
||||
'is_initiator': False
|
||||
}
|
||||
self.phones.append(phone)
|
||||
|
||||
self.phones[0]['peer_public_key'] = self.phones[1]['public_key']
|
||||
self.phones[1]['peer_public_key'] = self.phones[0]['public_key']
|
||||
|
||||
def phone_action(self, phone_id, ui_manager):
|
||||
phone = self.phones[phone_id]
|
||||
other_phone = self.phones[1 - phone_id]
|
||||
print(f"Phone {phone_id + 1} Action, current state: {phone['state']}, is_initiator: {phone['is_initiator']}")
|
||||
|
||||
if phone['state'] == PhoneState.IDLE:
|
||||
phone['state'] = PhoneState.CALLING
|
||||
other_phone['state'] = PhoneState.RINGING
|
||||
phone['is_initiator'] = True
|
||||
other_phone['is_initiator'] = False
|
||||
phone['client'].send("RINGING")
|
||||
elif phone['state'] == PhoneState.RINGING:
|
||||
phone['state'] = other_phone['state'] = PhoneState.IN_CALL
|
||||
phone['client'].send("IN_CALL")
|
||||
elif phone['state'] in [PhoneState.IN_CALL, PhoneState.CALLING]:
|
||||
if not phone['client'].state.handshake_in_progress and phone['state'] != PhoneState.CALLING:
|
||||
phone['state'] = other_phone['state'] = PhoneState.IDLE
|
||||
phone['client'].send("CALL_END")
|
||||
for p in [phone, other_phone]:
|
||||
if p['audio_timer']:
|
||||
p['audio_timer'].stop()
|
||||
else:
|
||||
print(f"Phone {phone_id + 1} cannot hang up during handshake or call setup")
|
||||
|
||||
ui_manager.update_phone_ui(phone_id)
|
||||
ui_manager.update_phone_ui(1 - phone_id)
|
||||
|
||||
def send_audio(self, phone_id):
|
||||
phone = self.phones[phone_id]
|
||||
if phone['state'] == PhoneState.IN_CALL and phone['client'].state.session and phone['client'].sock:
|
||||
mock_audio = secrets.token_bytes(16)
|
||||
try:
|
||||
self.update_sent_waveform(phone_id, mock_audio)
|
||||
phone['client'].state.session.send(phone['client'].sock, mock_audio)
|
||||
print(f"Client {phone_id} sent encrypted audio packet, length=32")
|
||||
except Exception as e:
|
||||
print(f"Client {phone_id} failed to send audio: {e}")
|
||||
|
||||
def start_audio(self, client_id, parent=None):
|
||||
self.handshake_done_count += 1
|
||||
print(f"HANDSHAKE_DONE received for client {client_id}, count: {self.handshake_done_count}")
|
||||
if self.handshake_done_count == 2:
|
||||
for phone in self.phones:
|
||||
if phone['state'] == PhoneState.IN_CALL:
|
||||
if not phone['audio_timer'] or not phone['audio_timer'].isActive():
|
||||
phone['audio_timer'] = QTimer(parent) # Parent to PhoneUI
|
||||
phone['audio_timer'].timeout.connect(lambda pid=phone['id']: self.send_audio(pid))
|
||||
phone['audio_timer'].start(100)
|
||||
self.handshake_done_count = 0
|
||||
|
||||
def update_waveform(self, client_id, data):
|
||||
self.phones[client_id]['waveform'].set_data(data)
|
||||
|
||||
def update_sent_waveform(self, client_id, data):
|
||||
self.phones[client_id]['sent_waveform'].set_data(data)
|
||||
|
||||
def map_state(self, state_str):
|
||||
if state_str == "RINGING":
|
||||
return PhoneState.RINGING
|
||||
elif state_str in ["CALL_END", "CALL_DROPPED"]:
|
||||
return PhoneState.IDLE
|
||||
elif state_str == "IN_CALL":
|
||||
return PhoneState.IN_CALL
|
||||
elif state_str == "HANDSHAKE":
|
||||
return PhoneState.IN_CALL
|
||||
elif state_str == "HANDSHAKE_DONE":
|
||||
return PhoneState.IN_CALL
|
||||
return PhoneState.IDLE
|
@ -1,5 +0,0 @@
|
||||
class PhoneState:
|
||||
IDLE = 0
|
||||
CALLING = 1
|
||||
IN_CALL = 2
|
||||
RINGING = 3
|
@ -1,196 +0,0 @@
|
||||
import socket
|
||||
import logging
|
||||
from dissononce.processing.impl.handshakestate import HandshakeState
|
||||
from dissononce.processing.impl.symmetricstate import SymmetricState
|
||||
from dissononce.processing.impl.cipherstate import CipherState
|
||||
from dissononce.processing.handshakepatterns.interactive.XK import XKHandshakePattern
|
||||
from dissononce.cipher.chachapoly import ChaChaPolyCipher
|
||||
from dissononce.dh.x25519.x25519 import X25519DH
|
||||
from dissononce.dh.keypair import KeyPair
|
||||
from dissononce.dh.x25519.public import PublicKey
|
||||
from dissononce.hash.sha256 import SHA256Hash
|
||||
|
||||
# Configure root logger for debug output
|
||||
logging.basicConfig(level=logging.DEBUG, format="%(message)s")
|
||||
|
||||
class NoiseXKSession:
|
||||
@staticmethod
|
||||
def generate_keypair() -> KeyPair:
|
||||
"""
|
||||
Generate a static X25519 KeyPair.
|
||||
Returns:
|
||||
KeyPair object with .private and .public attributes.
|
||||
"""
|
||||
return X25519DH().generate_keypair()
|
||||
|
||||
def __init__(self, local_kp: KeyPair, peer_pubkey: PublicKey):
|
||||
"""
|
||||
Initialize with our KeyPair and the peer's PublicKey.
|
||||
"""
|
||||
self.local_kp: KeyPair = local_kp
|
||||
self.peer_pubkey: PublicKey = peer_pubkey
|
||||
|
||||
# Build the Noise handshake state (X25519 DH, ChaChaPoly cipher, SHA256 hash)
|
||||
cipher = ChaChaPolyCipher()
|
||||
dh = X25519DH()
|
||||
hshash = SHA256Hash()
|
||||
symmetric = SymmetricState(CipherState(cipher), hshash)
|
||||
self._hs = HandshakeState(symmetric, dh)
|
||||
|
||||
self._send_cs = None # type: CipherState
|
||||
self._recv_cs = None
|
||||
|
||||
def handshake(self, sock: socket.socket, initiator: bool) -> None:
|
||||
"""
|
||||
Perform the XK handshake over the socket. Branches on initiator/responder
|
||||
so that each side reads or writes in the correct message order.
|
||||
On completion, self._send_cs and self._recv_cs hold the two CipherStates.
|
||||
"""
|
||||
logging.debug(f"[handshake] start (initiator={initiator})")
|
||||
# initialize with our KeyPair and their PublicKey
|
||||
if initiator:
|
||||
# initiator knows peer’s static out-of-band
|
||||
self._hs.initialize(
|
||||
XKHandshakePattern(),
|
||||
True,
|
||||
b'',
|
||||
s=self.local_kp,
|
||||
rs=self.peer_pubkey
|
||||
)
|
||||
else:
|
||||
logging.debug("[handshake] responder initializing without rs")
|
||||
# responder must NOT supply rs here
|
||||
self._hs.initialize(
|
||||
XKHandshakePattern(),
|
||||
False,
|
||||
b'',
|
||||
s=self.local_kp
|
||||
)
|
||||
|
||||
cs_pair = None
|
||||
if initiator:
|
||||
# 1) -> e
|
||||
buf1 = bytearray()
|
||||
cs_pair = self._hs.write_message(b'', buf1)
|
||||
logging.debug(f"[-> e] {buf1.hex()}")
|
||||
self._send_all(sock, buf1)
|
||||
|
||||
# 2) <- e, es, s, ss
|
||||
msg2 = self._recv_all(sock)
|
||||
logging.debug(f"[<- msg2] {msg2.hex()}")
|
||||
self._hs.read_message(msg2, bytearray())
|
||||
|
||||
# 3) -> se (final)
|
||||
buf3 = bytearray()
|
||||
cs_pair = self._hs.write_message(b'', buf3)
|
||||
logging.debug(f"[-> se] {buf3.hex()}")
|
||||
self._send_all(sock, buf3)
|
||||
else:
|
||||
# 1) <- e
|
||||
msg1 = self._recv_all(sock)
|
||||
logging.debug(f"[<- e] {msg1.hex()}")
|
||||
self._hs.read_message(msg1, bytearray())
|
||||
|
||||
# 2) -> e, es, s, ss
|
||||
buf2 = bytearray()
|
||||
cs_pair = self._hs.write_message(b'', buf2)
|
||||
logging.debug(f"[-> msg2] {buf2.hex()}")
|
||||
self._send_all(sock, buf2)
|
||||
|
||||
# 3) <- se (final)
|
||||
msg3 = self._recv_all(sock)
|
||||
logging.debug(f"[<- se] {msg3.hex()}")
|
||||
cs_pair = self._hs.read_message(msg3, bytearray())
|
||||
|
||||
# on the final step, we must get exactly two CipherStates
|
||||
if not cs_pair or len(cs_pair) != 2:
|
||||
raise RuntimeError("Handshake did not complete properly")
|
||||
cs0, cs1 = cs_pair
|
||||
# the library returns (cs_encrypt_for_initiator, cs_decrypt_for_initiator)
|
||||
if initiator:
|
||||
# initiator: cs0 encrypts, cs1 decrypts
|
||||
self._send_cs, self._recv_cs = cs0, cs1
|
||||
else:
|
||||
# responder must swap
|
||||
self._send_cs, self._recv_cs = cs1, cs0
|
||||
|
||||
# dump the raw symmetric keys & nonces (if available)
|
||||
self._dump_cipherstate("HANDSHAKE→ SEND", self._send_cs)
|
||||
self._dump_cipherstate("HANDSHAKE→ RECV", self._recv_cs)
|
||||
|
||||
def send(self, sock: socket.socket, plaintext: bytes) -> None:
|
||||
"""
|
||||
Encrypt and send a message.
|
||||
"""
|
||||
if self._send_cs is None:
|
||||
raise RuntimeError("Handshake not complete")
|
||||
ct = self._send_cs.encrypt_with_ad(b'', plaintext)
|
||||
logging.debug(f"[ENCRYPT] {ct.hex()}")
|
||||
# self._dump_cipherstate("SEND→ after encrypt", self._send_cs)
|
||||
self._send_all(sock, ct)
|
||||
|
||||
def receive(self, sock: socket.socket) -> bytes:
|
||||
"""
|
||||
Receive and decrypt a message.
|
||||
"""
|
||||
if self._recv_cs is None:
|
||||
raise RuntimeError("Handshake not complete")
|
||||
ct = self._recv_all(sock)
|
||||
logging.debug(f"[CIPHERTEXT] {ct.hex()}")
|
||||
# self._dump_cipherstate("RECV→ before decrypt", self._recv_cs)
|
||||
pt = self._recv_cs.decrypt_with_ad(b'', ct)
|
||||
logging.debug(f"[DECRYPT] {pt!r}")
|
||||
return pt
|
||||
|
||||
def decrypt(self, ciphertext: bytes) -> bytes:
|
||||
"""
|
||||
Decrypt a ciphertext received as bytes.
|
||||
"""
|
||||
if self._recv_cs is None:
|
||||
raise RuntimeError("Handshake not complete")
|
||||
# Remove 2-byte length prefix if present
|
||||
if len(ciphertext) >= 2 and int.from_bytes(ciphertext[:2], 'big') == len(ciphertext) - 2:
|
||||
logging.debug(f"[DECRYPT] Stripping 2-byte length prefix from {len(ciphertext)}-byte input")
|
||||
ciphertext = ciphertext[2:]
|
||||
logging.debug(f"[CIPHERTEXT] {ciphertext.hex()}")
|
||||
# self._dump_cipherstate("DECRYPT→ before decrypt", self._recv_cs)
|
||||
pt = self._recv_cs.decrypt_with_ad(b'', ciphertext)
|
||||
logging.debug(f"[DECRYPT] {pt!r}")
|
||||
return pt
|
||||
|
||||
def _send_all(self, sock: socket.socket, data: bytes) -> None:
|
||||
# Length-prefix (2 bytes big-endian) + data
|
||||
length = len(data).to_bytes(2, 'big')
|
||||
logging.debug(f"[SEND] length={length.hex()}, data={data.hex()}")
|
||||
sock.sendall(length + data)
|
||||
|
||||
def _recv_all(self, sock: socket.socket) -> bytes:
|
||||
# Read 2-byte length prefix, then the payload
|
||||
hdr = self._read_exact(sock, 2)
|
||||
length = int.from_bytes(hdr, 'big')
|
||||
logging.debug(f"[RECV] length={length} ({hdr.hex()})")
|
||||
data = self._read_exact(sock, length)
|
||||
logging.debug(f"[RECV] data={data.hex()}")
|
||||
return data
|
||||
|
||||
@staticmethod
|
||||
def _read_exact(sock: socket.socket, n: int) -> bytes:
|
||||
buf = bytearray()
|
||||
while len(buf) < n:
|
||||
chunk = sock.recv(n - len(buf))
|
||||
if not chunk:
|
||||
raise ConnectionError("Socket closed during read")
|
||||
buf.extend(chunk)
|
||||
return bytes(buf)
|
||||
|
||||
def _dump_cipherstate(self, label: str, cs: CipherState) -> None:
|
||||
"""
|
||||
Print the symmetric key (cs._k) and nonce counter (cs._n) for inspection.
|
||||
"""
|
||||
key = cs._key
|
||||
nonce = getattr(cs, "_n", None)
|
||||
if isinstance(key, (bytes, bytearray)):
|
||||
key_hex = key.hex()
|
||||
else:
|
||||
key_hex = repr(key)
|
||||
logging.debug(f"[{label}] key={key_hex}")
|
@ -1,46 +0,0 @@
|
||||
import random
|
||||
from PyQt5.QtWidgets import QWidget
|
||||
from PyQt5.QtCore import QTimer, QSize, QPointF
|
||||
from PyQt5.QtGui import QPainter, QColor, QPen, QLinearGradient, QBrush
|
||||
|
||||
class WaveformWidget(QWidget):
|
||||
def __init__(self, parent=None, dynamic=False):
|
||||
super().__init__(parent)
|
||||
self.dynamic = dynamic
|
||||
self.setMinimumSize(200, 80)
|
||||
self.setMaximumHeight(100)
|
||||
self.waveform_data = [random.randint(10, 90) for _ in range(50)]
|
||||
if self.dynamic:
|
||||
self.timer = QTimer(self)
|
||||
self.timer.timeout.connect(self.update_waveform)
|
||||
self.timer.start(100)
|
||||
|
||||
def update_waveform(self):
|
||||
self.waveform_data = self.waveform_data[1:] + [random.randint(10, 90)]
|
||||
self.update()
|
||||
|
||||
def set_data(self, data):
|
||||
amplitude = sum(byte for byte in data) % 90 + 10
|
||||
self.waveform_data = self.waveform_data[1:] + [amplitude]
|
||||
self.update()
|
||||
|
||||
def paintEvent(self, event):
|
||||
painter = QPainter(self)
|
||||
painter.setRenderHint(QPainter.Antialiasing)
|
||||
painter.fillRect(self.rect(), QColor("#2D2D2D"))
|
||||
gradient = QLinearGradient(0, 0, 0, self.height())
|
||||
gradient.setColorAt(0.0, QColor("#0078D4"))
|
||||
gradient.setColorAt(1.0, QColor("#50E6A4"))
|
||||
pen = QPen(QBrush(gradient), 2)
|
||||
painter.setPen(pen)
|
||||
bar_width = self.width() / len(self.waveform_data)
|
||||
max_h = self.height() - 10
|
||||
for i, val in enumerate(self.waveform_data):
|
||||
bar_height = (val / 100.0) * max_h
|
||||
x = i * bar_width
|
||||
y = (self.height() - bar_height) / 2
|
||||
painter.drawLine(QPointF(x + bar_width / 2, y), QPointF(x + bar_width / 2, y + bar_height))
|
||||
|
||||
def resizeEvent(self, event):
|
||||
super().resizeEvent(event)
|
||||
self.update()
|
@ -1,13 +0,0 @@
|
||||
simulator/
|
||||
├── gsm_simulator.py # gsm_simulator
|
||||
├── launch_gsm_simulator.sh # use to start docker and simulator, run in terminal
|
||||
|
||||
2 clients nect to gsm_simulator and simulate a call using noise protocol
|
||||
UI/
|
||||
├── main.py # UI setup and event handling
|
||||
├── phone_manager.py # Phone state, client init, audio logic
|
||||
├── phone_client.py # Socket communication and threading
|
||||
├── client_state.py # Client state and command processing
|
||||
├── session.py # Noise XK crypto session
|
||||
├── waveform_widget.py # Waveform UI component
|
||||
├── phone_state.py # State constants
|
@ -1,14 +0,0 @@
|
||||
# Use official Python image
|
||||
FROM python:3.9-slim
|
||||
|
||||
# Set working directory
|
||||
WORKDIR /app
|
||||
|
||||
# Copy the simulator script
|
||||
COPY gsm_simulator.py .
|
||||
|
||||
# Expose the port
|
||||
EXPOSE 12345
|
||||
|
||||
# Run the simulator
|
||||
CMD ["python", "gsm_simulator.py"]
|
@ -1,85 +0,0 @@
|
||||
import socket
|
||||
import threading
|
||||
import time
|
||||
|
||||
HOST = "0.0.0.0"
|
||||
PORT = 12345
|
||||
FRAME_SIZE = 1000
|
||||
FRAME_DELAY = 0.02
|
||||
|
||||
clients = []
|
||||
clients_lock = threading.Lock()
|
||||
|
||||
def handle_client(client_sock, client_id):
|
||||
print(f"Starting handle_client for Client {client_id}")
|
||||
try:
|
||||
while True:
|
||||
other_client = None
|
||||
with clients_lock:
|
||||
if len(clients) == 2 and client_id < len(clients):
|
||||
other_client = clients[1 - client_id]
|
||||
print(f"Client {client_id} waiting for data, other_client exists: {other_client is not None}")
|
||||
|
||||
try:
|
||||
data = client_sock.recv(1024)
|
||||
if not data:
|
||||
print(f"Client {client_id} disconnected or no data received")
|
||||
break
|
||||
if other_client:
|
||||
for i in range(0, len(data), FRAME_SIZE):
|
||||
frame = data[i:i + FRAME_SIZE]
|
||||
other_client.send(frame)
|
||||
time.sleep(FRAME_DELAY)
|
||||
print(f"Forwarded {len(data)} bytes from Client {client_id} to Client {1 - client_id}")
|
||||
except socket.error as e:
|
||||
print(f"Socket error with Client {client_id}: {e}")
|
||||
break
|
||||
finally:
|
||||
print(f"Closing connection for Client {client_id}")
|
||||
with clients_lock:
|
||||
if client_id < len(clients) and clients[client_id] == client_sock:
|
||||
clients.pop(client_id)
|
||||
print(f"Removed Client {client_id} from clients list")
|
||||
client_sock.close()
|
||||
|
||||
def start_simulator():
|
||||
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||
server.bind((HOST, PORT))
|
||||
server.listen(2)
|
||||
print(f"GSM Simulator listening on {HOST}:{PORT}...")
|
||||
|
||||
try:
|
||||
while True:
|
||||
client_sock, addr = server.accept()
|
||||
client_sock.setsockopt(socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1) # Keep connection alive
|
||||
with clients_lock:
|
||||
if len(clients) < 2:
|
||||
clients.append(client_sock)
|
||||
client_id = len(clients) - 1
|
||||
else:
|
||||
# Replace a closed socket or reject if both slots are active
|
||||
replaced = False
|
||||
for i in range(len(clients)):
|
||||
if clients[i].fileno() == -1: # Check if socket is closed
|
||||
clients[i] = client_sock
|
||||
client_id = i
|
||||
replaced = True
|
||||
break
|
||||
if not replaced:
|
||||
print(f"Rejecting new client from {addr}: max clients (2) reached")
|
||||
client_sock.close()
|
||||
continue
|
||||
print(f"Client {client_id} connected from {addr}")
|
||||
threading.Thread(target=handle_client, args=(client_sock, client_id), daemon=True).start()
|
||||
except KeyboardInterrupt:
|
||||
print("Shutting down simulator...")
|
||||
finally:
|
||||
with clients_lock:
|
||||
for client in clients:
|
||||
client.close()
|
||||
clients.clear()
|
||||
server.close()
|
||||
|
||||
if __name__ == "__main__":
|
||||
start_simulator()
|
@ -1,68 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Script to launch the GSM Simulator in Docker
|
||||
|
||||
# Variables
|
||||
IMAGE_NAME="gsm-simulator"
|
||||
CONTAINER_NAME="gsm-sim"
|
||||
PORT="12345"
|
||||
LOG_FILE="gsm_simulator.log"
|
||||
|
||||
# Check if Docker is installed
|
||||
if ! command -v docker &> /dev/null; then
|
||||
echo "Error: Docker is not installed. Please install Docker and try again."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Check if gsm_simulator.py exists
|
||||
if [ ! -f "gsm_simulator.py" ]; then
|
||||
echo "Error: gsm_simulator.py not found in the current directory."
|
||||
echo "Please ensure gsm_simulator.py is present and try again."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Create Dockerfile if it doesn't exist
|
||||
if [ ! -f "Dockerfile" ]; then
|
||||
echo "Creating Dockerfile..."
|
||||
cat <<EOF > Dockerfile
|
||||
FROM python:3.9-slim
|
||||
WORKDIR /app
|
||||
COPY gsm_simulator.py .
|
||||
EXPOSE 12345
|
||||
CMD ["python", "gsm_simulator.py"]
|
||||
EOF
|
||||
fi
|
||||
|
||||
# Ensure log file is writable
|
||||
touch $LOG_FILE
|
||||
chmod 666 $LOG_FILE
|
||||
|
||||
# Build the Docker image
|
||||
echo "Building Docker image: $IMAGE_NAME..."
|
||||
docker build -t $IMAGE_NAME .
|
||||
|
||||
# Check if the build was successful
|
||||
if [ $? -ne 0 ]; then
|
||||
echo "Error: Failed to build Docker image."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Stop and remove any existing container
|
||||
if [ "$(docker ps -q -f name=$CONTAINER_NAME)" ]; then
|
||||
echo "Stopping existing container: $CONTAINER_NAME..."
|
||||
docker stop $CONTAINER_NAME
|
||||
fi
|
||||
if [ "$(docker ps -aq -f name=$CONTAINER_NAME)" ]; then
|
||||
echo "Removing existing container: $CONTAINER_NAME..."
|
||||
docker rm $CONTAINER_NAME
|
||||
fi
|
||||
|
||||
# Clean up dangling images
|
||||
docker image prune -f
|
||||
|
||||
# Run the Docker container interactively
|
||||
echo "Launching GSM Simulator in Docker container: $CONTAINER_NAME..."
|
||||
docker run -it --rm -p $PORT:$PORT --name $CONTAINER_NAME $IMAGE_NAME | tee $LOG_FILE
|
||||
|
||||
# Note: Script will block here until container exits
|
||||
echo "GSM Simulator stopped. Logs saved to $LOG_FILE."
|
@ -1,24 +0,0 @@
|
||||
#external_caller.py
|
||||
import socket
|
||||
import time
|
||||
|
||||
|
||||
def connect():
|
||||
caller_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
caller_socket.connect(('localhost', 12345))
|
||||
caller_socket.send("CALLER".encode())
|
||||
print("Connected to GSM simulator as CALLER")
|
||||
time.sleep(2) # Wait 2 seconds for receiver to connect
|
||||
|
||||
for i in range(5):
|
||||
message = f"Audio packet {i + 1}"
|
||||
caller_socket.send(message.encode())
|
||||
print(f"Sent: {message}")
|
||||
time.sleep(1)
|
||||
|
||||
caller_socket.send("CALL_END".encode())
|
||||
print("Call ended.")
|
||||
caller_socket.close()
|
||||
|
||||
if __name__ == "__main__":
|
||||
connect()
|
@ -1,37 +0,0 @@
|
||||
#external_receiver.py
|
||||
import socket
|
||||
|
||||
def connect():
|
||||
receiver_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
receiver_socket.settimeout(15) # Increase timeout to 15 seconds
|
||||
receiver_socket.connect(('localhost', 12345))
|
||||
receiver_socket.send("RECEIVER".encode())
|
||||
print("Connected to GSM simulator as RECEIVER")
|
||||
|
||||
while True:
|
||||
try:
|
||||
data = receiver_socket.recv(1024).decode().strip()
|
||||
if not data:
|
||||
print("No data received. Connection closed.")
|
||||
break
|
||||
if data == "RINGING":
|
||||
print("Incoming call... ringing")
|
||||
elif data == "CALL_END":
|
||||
print("Call ended by caller.")
|
||||
break
|
||||
elif data == "CALL_DROPPED":
|
||||
print("Call dropped by network.")
|
||||
break
|
||||
else:
|
||||
print(f"Received: {data}")
|
||||
except socket.timeout:
|
||||
print("Timed out waiting for data.")
|
||||
break
|
||||
except Exception as e:
|
||||
print(f"Receiver error: {e}")
|
||||
break
|
||||
|
||||
receiver_socket.close()
|
||||
|
||||
if __name__ == "__main__":
|
||||
connect()
|
@ -1,86 +0,0 @@
|
||||
import socket
|
||||
import os
|
||||
import time
|
||||
import subprocess
|
||||
|
||||
# Configuration
|
||||
HOST = "localhost"
|
||||
PORT = 12345
|
||||
INPUT_FILE = "wav/input.wav"
|
||||
OUTPUT_FILE = "wav/received.wav"
|
||||
|
||||
|
||||
def encrypt_data(data):
|
||||
return data # Replace with your encryption protocol
|
||||
|
||||
|
||||
def decrypt_data(data):
|
||||
return data # Replace with your decryption protocol
|
||||
|
||||
|
||||
def run_protocol(send_mode=True):
|
||||
"""Connect to the simulator and send/receive data."""
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
sock.connect((HOST, PORT))
|
||||
print(f"Connected to simulator at {HOST}:{PORT}")
|
||||
|
||||
if send_mode:
|
||||
# Sender mode: Encode audio with toast
|
||||
os.system(f"toast -p -l {INPUT_FILE}") # Creates input.wav.gsm
|
||||
input_gsm_file = f"{INPUT_FILE}.gsm"
|
||||
if not os.path.exists(input_gsm_file):
|
||||
print(f"Error: {input_gsm_file} not created")
|
||||
sock.close()
|
||||
return
|
||||
with open(input_gsm_file, "rb") as f:
|
||||
voice_data = f.read()
|
||||
|
||||
encrypted_data = encrypt_data(voice_data)
|
||||
sock.send(encrypted_data)
|
||||
print(f"Sent {len(encrypted_data)} bytes")
|
||||
os.remove(input_gsm_file) # Clean up
|
||||
else:
|
||||
# Receiver mode: Wait for and receive data
|
||||
print("Waiting for data from sender...")
|
||||
received_data = b""
|
||||
sock.settimeout(5.0)
|
||||
try:
|
||||
while True:
|
||||
print("Calling recv()...")
|
||||
data = sock.recv(1024)
|
||||
print(f"Received {len(data)} bytes")
|
||||
if not data:
|
||||
print("Connection closed by sender or simulator")
|
||||
break
|
||||
received_data += data
|
||||
except socket.timeout:
|
||||
print("Timed out waiting for data")
|
||||
|
||||
if received_data:
|
||||
with open("received.gsm", "wb") as f:
|
||||
f.write(decrypt_data(received_data))
|
||||
print(f"Wrote {len(received_data)} bytes to received.gsm")
|
||||
# Decode with untoast, then convert to WAV with sox
|
||||
result = subprocess.run(["untoast", "received.gsm"], capture_output=True, text=True)
|
||||
print(f"untoast return code: {result.returncode}")
|
||||
print(f"untoast stderr: {result.stderr}")
|
||||
if result.returncode == 0:
|
||||
if os.path.exists("received"):
|
||||
# Convert raw PCM to WAV (8 kHz, mono, 16-bit)
|
||||
subprocess.run(["sox", "-t", "raw", "-r", "8000", "-e", "signed", "-b", "16", "-c", "1", "received",
|
||||
OUTPUT_FILE])
|
||||
os.remove("received")
|
||||
print(f"Received and saved {len(received_data)} bytes to {OUTPUT_FILE}")
|
||||
else:
|
||||
print("Error: 'received' file not created by untoast")
|
||||
else:
|
||||
print(f"untoast failed: {result.stderr}")
|
||||
else:
|
||||
print("No data received from simulator")
|
||||
|
||||
sock.close()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
mode = input("Enter 'send' to send data or 'receive' to receive data: ").strip().lower()
|
||||
run_protocol(send_mode=(mode == "send"))
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -1,805 +0,0 @@
|
||||
<mxfile host="Electron" agent="Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) draw.io/26.0.16 Chrome/132.0.6834.196 Electron/34.2.0 Safari/537.36" version="26.0.16" pages="4">
|
||||
<diagram id="C5RBs43oDa-KdzZeNtuy" name="Logique">
|
||||
<mxGraphModel dx="735" dy="407" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="827" pageHeight="1169" math="0" shadow="0">
|
||||
<root>
|
||||
<mxCell id="WIyWlLk6GJQsqaUBKTNV-0" />
|
||||
<mxCell id="WIyWlLk6GJQsqaUBKTNV-1" parent="WIyWlLk6GJQsqaUBKTNV-0" />
|
||||
<mxCell id="WIyWlLk6GJQsqaUBKTNV-2" value="" style="rounded=0;html=1;jettySize=auto;orthogonalLoop=1;fontSize=11;endArrow=block;endFill=0;endSize=8;strokeWidth=1;shadow=0;labelBackgroundColor=none;edgeStyle=orthogonalEdgeStyle;" parent="WIyWlLk6GJQsqaUBKTNV-1" source="WIyWlLk6GJQsqaUBKTNV-3" target="WIyWlLk6GJQsqaUBKTNV-6" edge="1">
|
||||
<mxGeometry relative="1" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="WIyWlLk6GJQsqaUBKTNV-3" value="Alice appelle Bob" style="rounded=1;whiteSpace=wrap;html=1;fontSize=12;glass=0;strokeWidth=3;shadow=0;strokeColor=light-dark(#000000,#370FFF);" parent="WIyWlLk6GJQsqaUBKTNV-1" vertex="1">
|
||||
<mxGeometry x="300" y="90" width="120" height="40" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="WIyWlLk6GJQsqaUBKTNV-4" value="Yes" style="rounded=0;html=1;jettySize=auto;orthogonalLoop=1;fontSize=11;endArrow=block;endFill=0;endSize=8;strokeWidth=1;shadow=0;labelBackgroundColor=none;edgeStyle=orthogonalEdgeStyle;" parent="WIyWlLk6GJQsqaUBKTNV-1" source="FXGPDhTRSO2FZSW48CnP-0" target="WIyWlLk6GJQsqaUBKTNV-10" edge="1">
|
||||
<mxGeometry y="20" relative="1" as="geometry">
|
||||
<mxPoint as="offset" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="WIyWlLk6GJQsqaUBKTNV-5" value="No" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;jettySize=auto;orthogonalLoop=1;fontSize=11;endArrow=block;endFill=0;endSize=8;strokeWidth=1;shadow=0;labelBackgroundColor=none;" parent="WIyWlLk6GJQsqaUBKTNV-1" source="WIyWlLk6GJQsqaUBKTNV-6" target="WIyWlLk6GJQsqaUBKTNV-7" edge="1">
|
||||
<mxGeometry y="10" relative="1" as="geometry">
|
||||
<mxPoint as="offset" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="WIyWlLk6GJQsqaUBKTNV-6" value="Bob répond ?" style="rhombus;whiteSpace=wrap;html=1;shadow=0;fontFamily=Helvetica;fontSize=12;align=center;strokeWidth=1;spacing=6;spacingTop=-4;" parent="WIyWlLk6GJQsqaUBKTNV-1" vertex="1">
|
||||
<mxGeometry x="310" y="180" width="100" height="80" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="WIyWlLk6GJQsqaUBKTNV-7" value="Rien ne se passe" style="rounded=1;whiteSpace=wrap;html=1;fontSize=12;glass=0;strokeWidth=1;shadow=0;" parent="WIyWlLk6GJQsqaUBKTNV-1" vertex="1">
|
||||
<mxGeometry x="460" y="200" width="120" height="40" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="WIyWlLk6GJQsqaUBKTNV-8" value="Négativement" style="rounded=0;html=1;jettySize=auto;orthogonalLoop=1;fontSize=11;endArrow=block;endFill=0;endSize=8;strokeWidth=1;shadow=0;labelBackgroundColor=none;edgeStyle=orthogonalEdgeStyle;" parent="WIyWlLk6GJQsqaUBKTNV-1" source="WIyWlLk6GJQsqaUBKTNV-10" target="WIyWlLk6GJQsqaUBKTNV-11" edge="1">
|
||||
<mxGeometry y="40" relative="1" as="geometry">
|
||||
<mxPoint as="offset" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="WIyWlLk6GJQsqaUBKTNV-9" value="Positivement" style="edgeStyle=orthogonalEdgeStyle;rounded=0;html=1;jettySize=auto;orthogonalLoop=1;fontSize=11;endArrow=block;endFill=0;endSize=8;strokeWidth=1;shadow=0;labelBackgroundColor=none;" parent="WIyWlLk6GJQsqaUBKTNV-1" source="WIyWlLk6GJQsqaUBKTNV-10" target="WIyWlLk6GJQsqaUBKTNV-12" edge="1">
|
||||
<mxGeometry y="10" relative="1" as="geometry">
|
||||
<mxPoint as="offset" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="WIyWlLk6GJQsqaUBKTNV-10" value="Bob ping..." style="whiteSpace=wrap;html=1;shadow=0;fontFamily=Helvetica;fontSize=12;align=center;strokeWidth=1;spacing=6;spacingTop=-4;shape=hexagon;perimeter=hexagonPerimeter2;fixedSize=1;" parent="WIyWlLk6GJQsqaUBKTNV-1" vertex="1">
|
||||
<mxGeometry x="310" y="390" width="100" height="80" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="WIyWlLk6GJQsqaUBKTNV-11" value="Protocole échoué<div>-</div><div>Passage en clair</div>" style="rounded=1;whiteSpace=wrap;html=1;fontSize=12;glass=0;strokeWidth=3;shadow=0;strokeColor=light-dark(#000000,#FF1414);" parent="WIyWlLk6GJQsqaUBKTNV-1" vertex="1">
|
||||
<mxGeometry x="300" y="520" width="120" height="50" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="FXGPDhTRSO2FZSW48CnP-7" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=0.5;entryY=0;entryDx=0;entryDy=0;endArrow=block;endFill=0;" parent="WIyWlLk6GJQsqaUBKTNV-1" source="WIyWlLk6GJQsqaUBKTNV-12" target="FXGPDhTRSO2FZSW48CnP-8" edge="1">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<mxPoint x="599.9999999999998" y="500" as="targetPoint" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="WIyWlLk6GJQsqaUBKTNV-12" value="Alice envoi son<span style="background-color: transparent; color: light-dark(rgb(0, 0, 0), rgb(255, 255, 255));">&nbsp;handshake a Bob</span>" style="rounded=1;whiteSpace=wrap;html=1;fontSize=12;glass=0;strokeWidth=1;shadow=0;" parent="WIyWlLk6GJQsqaUBKTNV-1" vertex="1">
|
||||
<mxGeometry x="540" y="410" width="120" height="40" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="FXGPDhTRSO2FZSW48CnP-1" value="" style="rounded=0;html=1;jettySize=auto;orthogonalLoop=1;fontSize=11;endArrow=block;endFill=0;endSize=8;strokeWidth=1;shadow=0;labelBackgroundColor=none;edgeStyle=orthogonalEdgeStyle;" parent="WIyWlLk6GJQsqaUBKTNV-1" source="WIyWlLk6GJQsqaUBKTNV-6" target="FXGPDhTRSO2FZSW48CnP-0" edge="1">
|
||||
<mxGeometry y="20" relative="1" as="geometry">
|
||||
<mxPoint as="offset" />
|
||||
<mxPoint x="360" y="260" as="sourcePoint" />
|
||||
<mxPoint x="360" y="390" as="targetPoint" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="FXGPDhTRSO2FZSW48CnP-0" value="Le dialer d'Alice envoi un PING a Bob" style="rounded=1;whiteSpace=wrap;html=1;fontSize=12;glass=0;strokeWidth=1;shadow=0;" parent="WIyWlLk6GJQsqaUBKTNV-1" vertex="1">
|
||||
<mxGeometry x="300" y="300" width="120" height="40" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="FXGPDhTRSO2FZSW48CnP-2" value="" style="endArrow=block;html=1;rounded=0;exitX=0;exitY=0.5;exitDx=0;exitDy=0;endFill=0;entryX=1;entryY=0.5;entryDx=0;entryDy=0;edgeStyle=orthogonalEdgeStyle;" parent="WIyWlLk6GJQsqaUBKTNV-1" source="WIyWlLk6GJQsqaUBKTNV-10" target="FXGPDhTRSO2FZSW48CnP-4" edge="1">
|
||||
<mxGeometry width="50" height="50" relative="1" as="geometry">
|
||||
<mxPoint x="480" y="450" as="sourcePoint" />
|
||||
<mxPoint x="190" y="430" as="targetPoint" />
|
||||
<Array as="points">
|
||||
<mxPoint x="280" y="430" />
|
||||
<mxPoint x="280" y="460" />
|
||||
</Array>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="FXGPDhTRSO2FZSW48CnP-3" value="Ne ping pas" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" parent="FXGPDhTRSO2FZSW48CnP-2" vertex="1" connectable="0">
|
||||
<mxGeometry x="-0.2695" relative="1" as="geometry">
|
||||
<mxPoint x="-16" y="-10" as="offset" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="FXGPDhTRSO2FZSW48CnP-5" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=0;entryY=0.5;entryDx=0;entryDy=0;" parent="WIyWlLk6GJQsqaUBKTNV-1" source="FXGPDhTRSO2FZSW48CnP-4" target="FXGPDhTRSO2FZSW48CnP-0" edge="1">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<Array as="points">
|
||||
<mxPoint x="130" y="320" />
|
||||
</Array>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="FXGPDhTRSO2FZSW48CnP-6" value="Reping" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" parent="FXGPDhTRSO2FZSW48CnP-5" vertex="1" connectable="0">
|
||||
<mxGeometry x="0.0817" relative="1" as="geometry">
|
||||
<mxPoint x="23" y="-10" as="offset" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="FXGPDhTRSO2FZSW48CnP-4" value="Attendre 1s ? 0.5s ?" style="rounded=1;whiteSpace=wrap;html=1;" parent="WIyWlLk6GJQsqaUBKTNV-1" vertex="1">
|
||||
<mxGeometry x="70" y="440" width="120" height="40" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="FXGPDhTRSO2FZSW48CnP-12" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0;exitY=0.5;exitDx=0;exitDy=0;entryX=1;entryY=0.5;entryDx=0;entryDy=0;endArrow=block;endFill=0;" parent="WIyWlLk6GJQsqaUBKTNV-1" source="FXGPDhTRSO2FZSW48CnP-8" target="FXGPDhTRSO2FZSW48CnP-11" edge="1">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<mxPoint x="530.0380952380951" y="585.0038095238094" as="sourcePoint" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="FXGPDhTRSO2FZSW48CnP-13" value="Non" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" parent="FXGPDhTRSO2FZSW48CnP-12" vertex="1" connectable="0">
|
||||
<mxGeometry x="-0.4964" y="-1" relative="1" as="geometry">
|
||||
<mxPoint as="offset" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="FXGPDhTRSO2FZSW48CnP-43" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=0.5;exitY=1;exitDx=0;exitDy=0;endArrow=block;endFill=0;strokeColor=light-dark(#000000,#FF0000);" parent="WIyWlLk6GJQsqaUBKTNV-1" source="FXGPDhTRSO2FZSW48CnP-8" target="FXGPDhTRSO2FZSW48CnP-27" edge="1">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<mxPoint x="600" y="800" as="targetPoint" />
|
||||
<Array as="points">
|
||||
<mxPoint x="600" y="660" />
|
||||
<mxPoint x="570" y="660" />
|
||||
<mxPoint x="570" y="700" />
|
||||
<mxPoint x="210" y="700" />
|
||||
</Array>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="FXGPDhTRSO2FZSW48CnP-44" value="Oui" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" parent="FXGPDhTRSO2FZSW48CnP-43" vertex="1" connectable="0">
|
||||
<mxGeometry x="-0.8049" y="1" relative="1" as="geometry">
|
||||
<mxPoint x="8" y="-25" as="offset" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="FXGPDhTRSO2FZSW48CnP-8" value="Bob reconnait la clé publique d'Alice ?" style="rhombus;whiteSpace=wrap;html=1;fontSize=10;" parent="WIyWlLk6GJQsqaUBKTNV-1" vertex="1">
|
||||
<mxGeometry x="540" y="545" width="120" height="75" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="FXGPDhTRSO2FZSW48CnP-16" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=block;endFill=0;entryX=0.5;entryY=1;entryDx=0;entryDy=0;" parent="WIyWlLk6GJQsqaUBKTNV-1" source="FXGPDhTRSO2FZSW48CnP-11" target="WIyWlLk6GJQsqaUBKTNV-11" edge="1">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<mxPoint x="360" y="600" as="targetPoint" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="FXGPDhTRSO2FZSW48CnP-17" value="Non" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" parent="FXGPDhTRSO2FZSW48CnP-16" vertex="1" connectable="0">
|
||||
<mxGeometry x="-0.1233" y="-2" relative="1" as="geometry">
|
||||
<mxPoint as="offset" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="FXGPDhTRSO2FZSW48CnP-20" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=block;endFill=0;" parent="WIyWlLk6GJQsqaUBKTNV-1" source="FXGPDhTRSO2FZSW48CnP-11" target="FXGPDhTRSO2FZSW48CnP-19" edge="1">
|
||||
<mxGeometry relative="1" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="FXGPDhTRSO2FZSW48CnP-21" value="Oui" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" parent="FXGPDhTRSO2FZSW48CnP-20" vertex="1" connectable="0">
|
||||
<mxGeometry x="-0.275" y="1" relative="1" as="geometry">
|
||||
<mxPoint as="offset" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="FXGPDhTRSO2FZSW48CnP-11" value="Bob accepte la clé d'Alice?" style="rhombus;whiteSpace=wrap;html=1;fontSize=10;" parent="WIyWlLk6GJQsqaUBKTNV-1" vertex="1">
|
||||
<mxGeometry x="354" y="620" width="120" height="75" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="FXGPDhTRSO2FZSW48CnP-23" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=block;endFill=0;" parent="WIyWlLk6GJQsqaUBKTNV-1" source="FXGPDhTRSO2FZSW48CnP-19" target="FXGPDhTRSO2FZSW48CnP-22" edge="1">
|
||||
<mxGeometry relative="1" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="FXGPDhTRSO2FZSW48CnP-19" value="Bob envoi sa clé publique en handshake" style="whiteSpace=wrap;html=1;fontSize=10;rounded=1;" parent="WIyWlLk6GJQsqaUBKTNV-1" vertex="1">
|
||||
<mxGeometry x="160" y="636.25" width="120" height="42.5" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="FXGPDhTRSO2FZSW48CnP-24" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=0;entryY=0.5;entryDx=0;entryDy=0;endArrow=block;endFill=0;" parent="WIyWlLk6GJQsqaUBKTNV-1" source="FXGPDhTRSO2FZSW48CnP-22" target="WIyWlLk6GJQsqaUBKTNV-11" edge="1">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<Array as="points">
|
||||
<mxPoint x="70" y="545" />
|
||||
</Array>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="FXGPDhTRSO2FZSW48CnP-25" value="Non" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" parent="FXGPDhTRSO2FZSW48CnP-24" vertex="1" connectable="0">
|
||||
<mxGeometry x="-0.7543" y="3" relative="1" as="geometry">
|
||||
<mxPoint as="offset" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="FXGPDhTRSO2FZSW48CnP-28" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;endArrow=block;endFill=0;" parent="WIyWlLk6GJQsqaUBKTNV-1" source="FXGPDhTRSO2FZSW48CnP-22" target="FXGPDhTRSO2FZSW48CnP-27" edge="1">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<Array as="points">
|
||||
<mxPoint x="70" y="750" />
|
||||
</Array>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="FXGPDhTRSO2FZSW48CnP-22" value="Alice accepte la clé publique de Bob<span style="background-color: transparent; color: light-dark(rgb(0, 0, 0), rgb(255, 255, 255));">&nbsp;?</span>" style="rhombus;whiteSpace=wrap;html=1;fontSize=10;rounded=0;" parent="WIyWlLk6GJQsqaUBKTNV-1" vertex="1">
|
||||
<mxGeometry x="30" y="617.5" width="80" height="80" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="FXGPDhTRSO2FZSW48CnP-47" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;" parent="WIyWlLk6GJQsqaUBKTNV-1" source="FXGPDhTRSO2FZSW48CnP-26" target="FXGPDhTRSO2FZSW48CnP-46" edge="1">
|
||||
<mxGeometry relative="1" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="FXGPDhTRSO2FZSW48CnP-26" value="Alice et Bob sont d'accord sur la clé symmétrique a utiliser" style="rounded=0;whiteSpace=wrap;html=1;" parent="WIyWlLk6GJQsqaUBKTNV-1" vertex="1">
|
||||
<mxGeometry x="340" y="820" width="120" height="60" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="FXGPDhTRSO2FZSW48CnP-31" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;exitX=1;exitY=0.5;exitDx=0;exitDy=0;entryX=0;entryY=0.5;entryDx=0;entryDy=0;endArrow=block;endFill=0;" parent="WIyWlLk6GJQsqaUBKTNV-1" source="FXGPDhTRSO2FZSW48CnP-27" target="FXGPDhTRSO2FZSW48CnP-30" edge="1">
|
||||
<mxGeometry relative="1" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="FXGPDhTRSO2FZSW48CnP-45" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=0;entryY=0.5;entryDx=0;entryDy=0;strokeColor=light-dark(#000000,#FF1616);endArrow=block;endFill=0;" parent="WIyWlLk6GJQsqaUBKTNV-1" source="FXGPDhTRSO2FZSW48CnP-27" target="FXGPDhTRSO2FZSW48CnP-26" edge="1">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<Array as="points">
|
||||
<mxPoint x="210" y="850" />
|
||||
</Array>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="FXGPDhTRSO2FZSW48CnP-27" value="Alice et Bob calculent le secret partagé de leur côté" style="whiteSpace=wrap;html=1;fontSize=10;rounded=1;" parent="WIyWlLk6GJQsqaUBKTNV-1" vertex="1">
|
||||
<mxGeometry x="150" y="720" width="120" height="50" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="FXGPDhTRSO2FZSW48CnP-33" value="" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=0.5;entryY=1;entryDx=0;entryDy=0;jumpStyle=sharp;" parent="WIyWlLk6GJQsqaUBKTNV-1" source="FXGPDhTRSO2FZSW48CnP-29" target="FXGPDhTRSO2FZSW48CnP-34" edge="1">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<mxPoint x="640" y="680" as="targetPoint" />
|
||||
<Array as="points">
|
||||
<mxPoint x="700" y="745" />
|
||||
</Array>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="FXGPDhTRSO2FZSW48CnP-36" value="Non" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" parent="FXGPDhTRSO2FZSW48CnP-33" vertex="1" connectable="0">
|
||||
<mxGeometry x="-0.1536" y="1" relative="1" as="geometry">
|
||||
<mxPoint as="offset" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="FXGPDhTRSO2FZSW48CnP-41" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=1;entryY=0.5;entryDx=0;entryDy=0;" parent="WIyWlLk6GJQsqaUBKTNV-1" source="FXGPDhTRSO2FZSW48CnP-29" target="FXGPDhTRSO2FZSW48CnP-26" edge="1">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<Array as="points">
|
||||
<mxPoint x="525" y="798" />
|
||||
<mxPoint x="510" y="798" />
|
||||
<mxPoint x="510" y="850" />
|
||||
</Array>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="FXGPDhTRSO2FZSW48CnP-42" value="Oui" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" parent="FXGPDhTRSO2FZSW48CnP-41" vertex="1" connectable="0">
|
||||
<mxGeometry x="-0.7774" y="1" relative="1" as="geometry">
|
||||
<mxPoint as="offset" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="FXGPDhTRSO2FZSW48CnP-29" value="Ils sont d'accord ?" style="rhombus;whiteSpace=wrap;html=1;" parent="WIyWlLk6GJQsqaUBKTNV-1" vertex="1">
|
||||
<mxGeometry x="460" y="715" width="130" height="60" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="FXGPDhTRSO2FZSW48CnP-32" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=0;entryY=0.5;entryDx=0;entryDy=0;" parent="WIyWlLk6GJQsqaUBKTNV-1" source="FXGPDhTRSO2FZSW48CnP-30" target="FXGPDhTRSO2FZSW48CnP-29" edge="1">
|
||||
<mxGeometry relative="1" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="FXGPDhTRSO2FZSW48CnP-30" value="Alice et Bob lisent à haute voix la phrase de sécurité" style="whiteSpace=wrap;html=1;fontSize=10;rounded=1;" parent="WIyWlLk6GJQsqaUBKTNV-1" vertex="1">
|
||||
<mxGeometry x="300" y="720" width="120" height="50" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="FXGPDhTRSO2FZSW48CnP-37" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=1;entryY=0.5;entryDx=0;entryDy=0;" parent="WIyWlLk6GJQsqaUBKTNV-1" source="FXGPDhTRSO2FZSW48CnP-34" target="WIyWlLk6GJQsqaUBKTNV-12" edge="1">
|
||||
<mxGeometry relative="1" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="FXGPDhTRSO2FZSW48CnP-38" value="Oui" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" parent="FXGPDhTRSO2FZSW48CnP-37" vertex="1" connectable="0">
|
||||
<mxGeometry x="-0.3086" y="-2" relative="1" as="geometry">
|
||||
<mxPoint as="offset" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="FXGPDhTRSO2FZSW48CnP-39" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=1;entryY=0.5;entryDx=0;entryDy=0;jumpStyle=sharp;jumpSize=8;" parent="WIyWlLk6GJQsqaUBKTNV-1" source="FXGPDhTRSO2FZSW48CnP-34" target="WIyWlLk6GJQsqaUBKTNV-11" edge="1">
|
||||
<mxGeometry relative="1" as="geometry">
|
||||
<Array as="points">
|
||||
<mxPoint x="530" y="690" />
|
||||
<mxPoint x="530" y="545" />
|
||||
</Array>
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="FXGPDhTRSO2FZSW48CnP-40" value="Non" style="edgeLabel;html=1;align=center;verticalAlign=middle;resizable=0;points=[];" parent="FXGPDhTRSO2FZSW48CnP-39" vertex="1" connectable="0">
|
||||
<mxGeometry x="-0.7617" relative="1" as="geometry">
|
||||
<mxPoint as="offset" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="FXGPDhTRSO2FZSW48CnP-34" value="Ré-essayer ?" style="rhombus;whiteSpace=wrap;html=1;" parent="WIyWlLk6GJQsqaUBKTNV-1" vertex="1">
|
||||
<mxGeometry x="660" y="650" width="80" height="80" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="FXGPDhTRSO2FZSW48CnP-49" style="edgeStyle=orthogonalEdgeStyle;rounded=0;orthogonalLoop=1;jettySize=auto;html=1;entryX=0.5;entryY=0;entryDx=0;entryDy=0;" parent="WIyWlLk6GJQsqaUBKTNV-1" source="FXGPDhTRSO2FZSW48CnP-46" target="FXGPDhTRSO2FZSW48CnP-48" edge="1">
|
||||
<mxGeometry relative="1" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="FXGPDhTRSO2FZSW48CnP-46" value="Alice et Bob utilisent la clé symmétrique pour chiffrer leurs transmissions" style="whiteSpace=wrap;html=1;rounded=0;" parent="WIyWlLk6GJQsqaUBKTNV-1" vertex="1">
|
||||
<mxGeometry x="340" y="920" width="120" height="60" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="FXGPDhTRSO2FZSW48CnP-48" value="" style="rhombus;whiteSpace=wrap;html=1;" parent="WIyWlLk6GJQsqaUBKTNV-1" vertex="1">
|
||||
<mxGeometry x="360" y="1020" width="80" height="80" as="geometry" />
|
||||
</mxCell>
|
||||
</root>
|
||||
</mxGraphModel>
|
||||
</diagram>
|
||||
<diagram id="c7L-flsM9ZWaCx455Pfy" name="Transport Layer - 0">
|
||||
<mxGraphModel dx="1434" dy="835" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="850" pageHeight="1100" math="0" shadow="0">
|
||||
<root>
|
||||
<mxCell id="0" />
|
||||
<mxCell id="1" parent="0" />
|
||||
</root>
|
||||
</mxGraphModel>
|
||||
</diagram>
|
||||
<diagram id="4Sb7mgJDpsadGym-U4wz" name="Protocol Layer - 0">
|
||||
<mxGraphModel dx="1434" dy="835" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="850" pageHeight="1100" math="0" shadow="0">
|
||||
<root>
|
||||
<mxCell id="0" />
|
||||
<mxCell id="1" parent="0" />
|
||||
<mxCell id="b_xV4iUWIxmdZCAYY4YR-1" value="" style="html=1;shadow=0;dashed=0;align=center;verticalAlign=middle;shape=mxgraph.arrows2.arrow;dy=0;dx=10;notch=0;" parent="1" vertex="1">
|
||||
<mxGeometry x="160" y="120" width="440" height="120" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="O_eM33N56VtHnDaMz1H4-1" value="ALICE" style="shape=umlLifeline;perimeter=lifelinePerimeter;whiteSpace=wrap;html=1;container=1;dropTarget=0;collapsible=0;recursiveResize=0;outlineConnect=0;portConstraint=eastwest;newEdgeStyle={"curved":0,"rounded":0};participant=umlEntity;strokeWidth=2;" parent="1" vertex="1">
|
||||
<mxGeometry x="120" y="40" width="40" height="3110" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="O_eM33N56VtHnDaMz1H4-2" value="BOB" style="shape=umlLifeline;perimeter=lifelinePerimeter;whiteSpace=wrap;html=1;container=1;dropTarget=0;collapsible=0;recursiveResize=0;outlineConnect=0;portConstraint=eastwest;newEdgeStyle={"curved":0,"rounded":0};participant=umlEntity;strokeWidth=2;" parent="1" vertex="1">
|
||||
<mxGeometry x="690" y="40" width="40" height="3110" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="b_xV4iUWIxmdZCAYY4YR-2" value="PING" style="text;html=1;align=center;verticalAlign=middle;resizable=0;points=[];autosize=1;strokeColor=none;fillColor=none;fontSize=23;" parent="1" vertex="1">
|
||||
<mxGeometry x="385" y="65" width="80" height="40" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="n3lF8vaYaHAhAfaeaFZn-1" value="Nonce" style="swimlane;whiteSpace=wrap;html=1;" parent="1" vertex="1">
|
||||
<mxGeometry x="180" y="130" width="105" height="80" as="geometry">
|
||||
<mxRectangle x="210" y="130" width="80" height="30" as="alternateBounds" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="n3lF8vaYaHAhAfaeaFZn-6" value="<div>sha256 (</div><div>numéro alice +</div><div>numéro bob +</div><div>timestamp +</div><div>random<br></div><div>) / ~2 (left part)<br></div>" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;fontSize=7;" parent="n3lF8vaYaHAhAfaeaFZn-1" vertex="1">
|
||||
<mxGeometry y="25" width="100" height="55" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="n3lF8vaYaHAhAfaeaFZn-2" value="Version" style="swimlane;whiteSpace=wrap;html=1;" parent="1" vertex="1">
|
||||
<mxGeometry x="305" y="130" width="58.75" height="80" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="pWkGvNQAXuiST1IiWYlx-1" value="(0-128)" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" parent="n3lF8vaYaHAhAfaeaFZn-2" vertex="1">
|
||||
<mxGeometry x="3.75" y="30" width="51.25" height="25" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="n3lF8vaYaHAhAfaeaFZn-4" value="Checksum" style="swimlane;whiteSpace=wrap;html=1;" parent="1" vertex="1">
|
||||
<mxGeometry x="455" y="130" width="90" height="80" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="n3lF8vaYaHAhAfaeaFZn-7" value="CRC-32" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" parent="n3lF8vaYaHAhAfaeaFZn-4" vertex="1">
|
||||
<mxGeometry x="15" y="25" width="60" height="30" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="XvZTtdEB18xY6m2a5fJO-1" value="" style="html=1;shadow=0;dashed=0;align=center;verticalAlign=middle;shape=mxgraph.arrows2.arrow;dy=0;dx=10;notch=0;rotation=-180;" parent="1" vertex="1">
|
||||
<mxGeometry x="280" y="280" width="410" height="190" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="XvZTtdEB18xY6m2a5fJO-2" value="Timestamp" style="swimlane;whiteSpace=wrap;html=1;" parent="1" vertex="1">
|
||||
<mxGeometry x="300" y="290" width="90" height="60" as="geometry">
|
||||
<mxRectangle x="210" y="130" width="80" height="30" as="alternateBounds" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="XvZTtdEB18xY6m2a5fJO-3" value="timestamp" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;fontSize=10;strokeWidth=1;" parent="XvZTtdEB18xY6m2a5fJO-2" vertex="1">
|
||||
<mxGeometry x="11.25" y="27.5" width="67.5" height="25" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="XvZTtdEB18xY6m2a5fJO-4" value="Version" style="swimlane;whiteSpace=wrap;html=1;" parent="1" vertex="1">
|
||||
<mxGeometry x="405.63" y="290" width="58.75" height="60" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="XvZTtdEB18xY6m2a5fJO-11" value="0" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" parent="XvZTtdEB18xY6m2a5fJO-4" vertex="1">
|
||||
<mxGeometry y="25" width="60" height="30" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="XvZTtdEB18xY6m2a5fJO-5" value="Checksum" style="swimlane;whiteSpace=wrap;html=1;" parent="1" vertex="1">
|
||||
<mxGeometry x="590" y="380" width="85" height="60" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="XvZTtdEB18xY6m2a5fJO-6" value="CRC-32" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" parent="XvZTtdEB18xY6m2a5fJO-5" vertex="1">
|
||||
<mxGeometry x="12.5" y="25" width="60" height="30" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="XvZTtdEB18xY6m2a5fJO-9" value="Answer" style="swimlane;whiteSpace=wrap;html=1;" parent="1" vertex="1">
|
||||
<mxGeometry x="482.5" y="290" width="57.5" height="60" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="XvZTtdEB18xY6m2a5fJO-10" value="<div>YES</div><div>NO<br></div>" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" parent="1" vertex="1">
|
||||
<mxGeometry x="484.38" y="315" width="53.75" height="30" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="XvZTtdEB18xY6m2a5fJO-13" value="HANDSHAKE" style="text;html=1;align=center;verticalAlign=middle;resizable=0;points=[];autosize=1;strokeColor=none;fillColor=none;fontSize=23;" parent="1" vertex="1">
|
||||
<mxGeometry x="350" y="510" width="170" height="40" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="XvZTtdEB18xY6m2a5fJO-14" value="" style="html=1;shadow=0;dashed=0;align=center;verticalAlign=middle;shape=mxgraph.arrows2.arrow;dy=0;dx=10;notch=0;" parent="1" vertex="1">
|
||||
<mxGeometry x="160" y="570" width="410" height="220" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="XvZTtdEB18xY6m2a5fJO-15" value="Clé éphémère" style="swimlane;whiteSpace=wrap;html=1;" parent="1" vertex="1">
|
||||
<mxGeometry x="170" y="580" width="105" height="80" as="geometry">
|
||||
<mxRectangle x="210" y="130" width="80" height="30" as="alternateBounds" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="pP7SjZfcCiBg3d1TCkzP-13" value="Clé (publique) générée aléatoirement" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;fontSize=11;" parent="XvZTtdEB18xY6m2a5fJO-15" vertex="1">
|
||||
<mxGeometry y="30" width="100" height="40" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="XvZTtdEB18xY6m2a5fJO-17" value="Signature" style="swimlane;whiteSpace=wrap;html=1;" parent="1" vertex="1">
|
||||
<mxGeometry x="285" y="580" width="105" height="80" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="pP7SjZfcCiBg3d1TCkzP-14" value="PubkeyFixe. sign(clé éphémère)" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" parent="XvZTtdEB18xY6m2a5fJO-17" vertex="1">
|
||||
<mxGeometry y="20" width="100" height="60" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="XvZTtdEB18xY6m2a5fJO-18" value="Checksum" style="swimlane;whiteSpace=wrap;html=1;" parent="1" vertex="1">
|
||||
<mxGeometry x="486.25" y="580" width="65" height="80" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="XvZTtdEB18xY6m2a5fJO-19" value="CRC-32" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" parent="XvZTtdEB18xY6m2a5fJO-18" vertex="1">
|
||||
<mxGeometry x="2.5" y="30" width="60" height="30" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="pP7SjZfcCiBg3d1TCkzP-15" value="PFS" style="swimlane;whiteSpace=wrap;html=1;" parent="1" vertex="1">
|
||||
<mxGeometry x="402.81" y="580" width="71.88" height="80" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="pP7SjZfcCiBg3d1TCkzP-16" value="hash( preuve de convo précédente)" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;fontSize=10;" parent="pP7SjZfcCiBg3d1TCkzP-15" vertex="1">
|
||||
<mxGeometry x="6.57" y="30" width="60" height="40" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="pP7SjZfcCiBg3d1TCkzP-17" value="" style="html=1;shadow=0;dashed=0;align=center;verticalAlign=middle;shape=mxgraph.arrows2.arrow;dy=0;dx=10;notch=0;rotation=-180;" parent="1" vertex="1">
|
||||
<mxGeometry x="285" y="830" width="410" height="180" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="pP7SjZfcCiBg3d1TCkzP-43" value="Clé éphémère" style="swimlane;whiteSpace=wrap;html=1;" parent="1" vertex="1">
|
||||
<mxGeometry x="305" y="840" width="105" height="80" as="geometry">
|
||||
<mxRectangle x="210" y="130" width="80" height="30" as="alternateBounds" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="pP7SjZfcCiBg3d1TCkzP-44" value="Clé (publique) générée aléatoirement" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;fontSize=11;" parent="pP7SjZfcCiBg3d1TCkzP-43" vertex="1">
|
||||
<mxGeometry y="30" width="100" height="40" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="pP7SjZfcCiBg3d1TCkzP-45" value="Signature" style="swimlane;whiteSpace=wrap;html=1;" parent="1" vertex="1">
|
||||
<mxGeometry x="420" y="840" width="105" height="80" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="pP7SjZfcCiBg3d1TCkzP-46" value="PubkeyFixe. sign(clé éphémère)" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" parent="pP7SjZfcCiBg3d1TCkzP-45" vertex="1">
|
||||
<mxGeometry y="20" width="100" height="60" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="pP7SjZfcCiBg3d1TCkzP-47" value="Checksum" style="swimlane;whiteSpace=wrap;html=1;" parent="1" vertex="1">
|
||||
<mxGeometry x="621.25" y="840" width="65" height="80" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="pP7SjZfcCiBg3d1TCkzP-48" value="CRC-32" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" parent="pP7SjZfcCiBg3d1TCkzP-47" vertex="1">
|
||||
<mxGeometry x="2.5" y="30" width="60" height="30" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="pP7SjZfcCiBg3d1TCkzP-49" value="PFS" style="swimlane;whiteSpace=wrap;html=1;" parent="1" vertex="1">
|
||||
<mxGeometry x="537.81" y="840" width="71.88" height="80" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="pP7SjZfcCiBg3d1TCkzP-50" value="hash( preuve de convo précédente )" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;fontSize=10;" parent="pP7SjZfcCiBg3d1TCkzP-49" vertex="1">
|
||||
<mxGeometry x="6.57" y="30" width="60" height="40" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="pP7SjZfcCiBg3d1TCkzP-54" value="Timestamp" style="swimlane;whiteSpace=wrap;html=1;" parent="1" vertex="1">
|
||||
<mxGeometry x="182.5" y="690" width="80" height="70" as="geometry">
|
||||
<mxRectangle x="210" y="130" width="80" height="30" as="alternateBounds" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="pP7SjZfcCiBg3d1TCkzP-55" value="timestamp" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;fontSize=10;strokeWidth=1;" parent="pP7SjZfcCiBg3d1TCkzP-54" vertex="1">
|
||||
<mxGeometry x="6.25" y="32.5" width="67.5" height="25" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="pP7SjZfcCiBg3d1TCkzP-56" value="Timestamp" style="swimlane;whiteSpace=wrap;html=1;" parent="1" vertex="1">
|
||||
<mxGeometry x="606.25" y="930" width="80" height="70" as="geometry">
|
||||
<mxRectangle x="210" y="130" width="80" height="30" as="alternateBounds" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="pP7SjZfcCiBg3d1TCkzP-57" value="timestamp" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;fontSize=10;strokeWidth=1;" parent="pP7SjZfcCiBg3d1TCkzP-56" vertex="1">
|
||||
<mxGeometry x="6.25" y="32.5" width="67.5" height="25" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="pP7SjZfcCiBg3d1TCkzP-58" value="" style="html=1;shadow=0;dashed=0;align=center;verticalAlign=middle;shape=mxgraph.arrows2.arrow;dy=0;dx=10;notch=0;" parent="1" vertex="1">
|
||||
<mxGeometry x="160" y="1160" width="450" height="200" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="pP7SjZfcCiBg3d1TCkzP-59" value="ENCRYPTED COMS" style="text;html=1;align=center;verticalAlign=middle;resizable=0;points=[];autosize=1;strokeColor=none;fillColor=none;fontSize=23;" parent="1" vertex="1">
|
||||
<mxGeometry x="305" y="1100" width="240" height="40" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="pP7SjZfcCiBg3d1TCkzP-60" value="129b" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" parent="1" vertex="1">
|
||||
<mxGeometry x="200" y="210" width="60" height="30" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="pP7SjZfcCiBg3d1TCkzP-61" value="7b" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" parent="1" vertex="1">
|
||||
<mxGeometry x="303.75" y="210" width="60" height="30" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="pP7SjZfcCiBg3d1TCkzP-62" value="32b" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" parent="1" vertex="1">
|
||||
<mxGeometry x="470" y="210" width="60" height="30" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="pP7SjZfcCiBg3d1TCkzP-63" value="= 172b" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" parent="1" vertex="1">
|
||||
<mxGeometry x="530" y="210" width="60" height="30" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="pP7SjZfcCiBg3d1TCkzP-66" value="32b" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" parent="1" vertex="1">
|
||||
<mxGeometry x="313" y="350" width="60" height="30" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="pP7SjZfcCiBg3d1TCkzP-67" value="7b" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" parent="1" vertex="1">
|
||||
<mxGeometry x="406.75" y="350" width="60" height="30" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="pP7SjZfcCiBg3d1TCkzP-68" value="1b" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" parent="1" vertex="1">
|
||||
<mxGeometry x="479.25" y="350" width="60" height="30" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="pP7SjZfcCiBg3d1TCkzP-69" value="32b" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" parent="1" vertex="1">
|
||||
<mxGeometry x="600.5" y="440" width="60" height="30" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="pP7SjZfcCiBg3d1TCkzP-70" value="= 76b" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" parent="1" vertex="1">
|
||||
<mxGeometry x="426.25" y="420" width="60" height="30" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="pP7SjZfcCiBg3d1TCkzP-71" value="264b" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" parent="1" vertex="1">
|
||||
<mxGeometry x="193" y="660" width="60" height="30" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="pP7SjZfcCiBg3d1TCkzP-72" value="512b" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" parent="1" vertex="1">
|
||||
<mxGeometry x="307.5" y="660" width="60" height="30" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="pP7SjZfcCiBg3d1TCkzP-73" value="256b" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" parent="1" vertex="1">
|
||||
<mxGeometry x="409.38" y="660" width="60" height="30" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="pP7SjZfcCiBg3d1TCkzP-74" value="32b" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" parent="1" vertex="1">
|
||||
<mxGeometry x="488.75" y="660" width="60" height="30" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="pP7SjZfcCiBg3d1TCkzP-75" value="32b" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" parent="1" vertex="1">
|
||||
<mxGeometry x="192.5" y="760" width="60" height="30" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="pP7SjZfcCiBg3d1TCkzP-76" value="=1096b" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" parent="1" vertex="1">
|
||||
<mxGeometry x="315" y="750" width="60" height="30" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="pP7SjZfcCiBg3d1TCkzP-77" value="=1096b" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" parent="1" vertex="1">
|
||||
<mxGeometry x="327.5" y="970" width="60" height="30" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="_H5URFloX_BVB2BL7kO6-1" value="CRC ?" style="swimlane;whiteSpace=wrap;html=1;fillColor=#008a00;fontColor=#ffffff;strokeColor=#005700;" parent="1" vertex="1">
|
||||
<mxGeometry x="375" y="1270" width="63.25" height="60" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="_H5URFloX_BVB2BL7kO6-2" value="CRC-32" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" parent="_H5URFloX_BVB2BL7kO6-1" vertex="1">
|
||||
<mxGeometry x="1.6199999999999992" y="25" width="60" height="30" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="_H5URFloX_BVB2BL7kO6-3" value="Flag" style="swimlane;whiteSpace=wrap;html=1;fillColor=#008a00;fontColor=#ffffff;strokeColor=#005700;" parent="1" vertex="1">
|
||||
<mxGeometry x="180" y="1170" width="65" height="60" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="_H5URFloX_BVB2BL7kO6-4" value="To determine" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" parent="_H5URFloX_BVB2BL7kO6-3" vertex="1">
|
||||
<mxGeometry x="2.5" y="25" width="60" height="30" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="_H5URFloX_BVB2BL7kO6-5" value="nbretry" style="swimlane;whiteSpace=wrap;html=1;" parent="1" vertex="1">
|
||||
<mxGeometry x="344.38" y="1170" width="65" height="60" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="_H5URFloX_BVB2BL7kO6-6" value="y" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" parent="_H5URFloX_BVB2BL7kO6-5" vertex="1">
|
||||
<mxGeometry x="2.5" y="25" width="60" height="30" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="_H5URFloX_BVB2BL7kO6-7" value="msg_len" style="swimlane;whiteSpace=wrap;html=1;fillColor=#008a00;fontColor=#ffffff;strokeColor=#005700;" parent="1" vertex="1">
|
||||
<mxGeometry x="262.5" y="1170" width="65" height="60" as="geometry">
|
||||
<mxRectangle x="262.5" y="1170" width="90" height="30" as="alternateBounds" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="_H5URFloX_BVB2BL7kO6-8" value="XXX" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" parent="_H5URFloX_BVB2BL7kO6-7" vertex="1">
|
||||
<mxGeometry x="2.5" y="25" width="60" height="30" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="_H5URFloX_BVB2BL7kO6-9" value="msg" style="swimlane;whiteSpace=wrap;html=1;fillColor=#0050ef;fontColor=#ffffff;strokeColor=#001DBC;" parent="1" vertex="1">
|
||||
<mxGeometry x="187.5" y="1270" width="65" height="60" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="_H5URFloX_BVB2BL7kO6-10" value="BBB" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" parent="_H5URFloX_BVB2BL7kO6-9" vertex="1">
|
||||
<mxGeometry x="2.5" y="30" width="60" height="30" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="_H5URFloX_BVB2BL7kO6-11" value="16b" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" parent="1" vertex="1">
|
||||
<mxGeometry x="180" y="1230" width="60" height="30" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="_H5URFloX_BVB2BL7kO6-12" value="8b" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" parent="1" vertex="1">
|
||||
<mxGeometry x="349.38" y="1230" width="55" height="30" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="_H5URFloX_BVB2BL7kO6-13" value="16b" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" parent="1" vertex="1">
|
||||
<mxGeometry x="267.5" y="1230" width="55" height="30" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="_H5URFloX_BVB2BL7kO6-14" value="96b" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" parent="1" vertex="1">
|
||||
<mxGeometry x="510" y="1230" width="55" height="30" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="_H5URFloX_BVB2BL7kO6-15" value="32b" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" parent="1" vertex="1">
|
||||
<mxGeometry x="379.12" y="1330" width="55" height="30" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="_H5URFloX_BVB2BL7kO6-16" value="= (180b ~ 212b) + yyy" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" parent="1" vertex="1">
|
||||
<mxGeometry x="465" y="1285" width="130" height="30" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="pWkGvNQAXuiST1IiWYlx-2" value="Cypher" style="swimlane;whiteSpace=wrap;html=1;" parent="1" vertex="1">
|
||||
<mxGeometry x="375" y="130" width="58.75" height="80" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="pWkGvNQAXuiST1IiWYlx-3" value="(0-16)" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" parent="pWkGvNQAXuiST1IiWYlx-2" vertex="1">
|
||||
<mxGeometry x="3.75" y="30" width="51.25" height="25" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="pWkGvNQAXuiST1IiWYlx-4" value="4b" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" parent="1" vertex="1">
|
||||
<mxGeometry x="375" y="210" width="55" height="30" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="pWkGvNQAXuiST1IiWYlx-5" value="Cypher" style="swimlane;whiteSpace=wrap;html=1;" parent="1" vertex="1">
|
||||
<mxGeometry x="600" y="290" width="58.75" height="60" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="pWkGvNQAXuiST1IiWYlx-6" value="(0-16)" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" parent="pWkGvNQAXuiST1IiWYlx-5" vertex="1">
|
||||
<mxGeometry x="3.75" y="30" width="51.25" height="25" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="pWkGvNQAXuiST1IiWYlx-7" value="4b" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" parent="1" vertex="1">
|
||||
<mxGeometry x="601.88" y="350" width="55" height="30" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="pWkGvNQAXuiST1IiWYlx-8" value="status" style="swimlane;whiteSpace=wrap;html=1;" parent="1" vertex="1">
|
||||
<mxGeometry x="425" y="1170" width="65" height="60" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="pWkGvNQAXuiST1IiWYlx-9" value="CRC ?" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" parent="pWkGvNQAXuiST1IiWYlx-8" vertex="1">
|
||||
<mxGeometry x="2.5" y="25" width="60" height="30" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="pWkGvNQAXuiST1IiWYlx-10" value="4b" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" parent="1" vertex="1">
|
||||
<mxGeometry x="428.75" y="1230" width="55" height="30" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="pWkGvNQAXuiST1IiWYlx-11" value="iv" style="swimlane;whiteSpace=wrap;html=1;" parent="1" vertex="1">
|
||||
<mxGeometry x="505" y="1170" width="65" height="60" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="pWkGvNQAXuiST1IiWYlx-12" value="random<div>(+Z)</div>" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" parent="pWkGvNQAXuiST1IiWYlx-11" vertex="1">
|
||||
<mxGeometry x="2.5" y="25" width="60" height="30" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="pWkGvNQAXuiST1IiWYlx-13" value="BBB b" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" parent="1" vertex="1">
|
||||
<mxGeometry x="193" y="1330" width="55" height="30" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="pWkGvNQAXuiST1IiWYlx-14" value="MAC" style="swimlane;whiteSpace=wrap;html=1;fillColor=#008a00;fontColor=#ffffff;strokeColor=#005700;" parent="1" vertex="1">
|
||||
<mxGeometry x="286.13" y="1270" width="63.25" height="60" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="pWkGvNQAXuiST1IiWYlx-15" value="AEAD" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" parent="pWkGvNQAXuiST1IiWYlx-14" vertex="1">
|
||||
<mxGeometry x="1.6199999999999992" y="25" width="60" height="30" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="pWkGvNQAXuiST1IiWYlx-16" value="128b" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" parent="1" vertex="1">
|
||||
<mxGeometry x="290.25" y="1330" width="55" height="30" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="pWkGvNQAXuiST1IiWYlx-17" value="Green = clear data" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;fillColor=#008a00;fontColor=#ffffff;strokeColor=#005700;" parent="1" vertex="1">
|
||||
<mxGeometry x="10" y="1170" width="110" height="30" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="pWkGvNQAXuiST1IiWYlx-18" value="<font style="color: light-dark(rgb(0, 0, 0), rgb(255, 255, 255));">White = additional data</font>" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;fillColor=none;strokeColor=light-dark(#6C8EBF,#FFFFFF);" parent="1" vertex="1">
|
||||
<mxGeometry y="1220" width="130" height="30" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="pWkGvNQAXuiST1IiWYlx-19" value="Blue = encrypted data" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;fillColor=#0050ef;fontColor=#ffffff;strokeColor=#001DBC;" parent="1" vertex="1">
|
||||
<mxGeometry x="10" y="1270" width="110" height="30" as="geometry" />
|
||||
</mxCell>
|
||||
</root>
|
||||
</mxGraphModel>
|
||||
</diagram>
|
||||
<diagram name="Protocol Layer - 1" id="_rkrwzJg5buKJxYS8faK">
|
||||
<mxGraphModel dx="1062" dy="1719" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="850" pageHeight="1100" math="0" shadow="0">
|
||||
<root>
|
||||
<mxCell id="0ZE-dMPOFneTTZtpNr96-0" />
|
||||
<mxCell id="0ZE-dMPOFneTTZtpNr96-1" parent="0ZE-dMPOFneTTZtpNr96-0" />
|
||||
<mxCell id="0ZE-dMPOFneTTZtpNr96-2" value="" style="html=1;shadow=0;dashed=0;align=center;verticalAlign=middle;shape=mxgraph.arrows2.arrow;dy=0;dx=10;notch=0;" vertex="1" parent="0ZE-dMPOFneTTZtpNr96-1">
|
||||
<mxGeometry x="160" y="120" width="260" height="120" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="0ZE-dMPOFneTTZtpNr96-3" value="<font style="color: rgb(255, 65, 27);"><b>ALICE&nbsp; (INITIATOR)</b></font>" style="shape=umlLifeline;perimeter=lifelinePerimeter;whiteSpace=wrap;html=1;container=1;dropTarget=0;collapsible=0;recursiveResize=0;outlineConnect=0;portConstraint=eastwest;newEdgeStyle={"curved":0,"rounded":0};participant=umlEntity;strokeWidth=2;" vertex="1" parent="0ZE-dMPOFneTTZtpNr96-1">
|
||||
<mxGeometry x="120" y="40" width="40" height="3110" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="0ZE-dMPOFneTTZtpNr96-4" value="<font>BOB</font>" style="shape=umlLifeline;perimeter=lifelinePerimeter;whiteSpace=wrap;html=1;container=1;dropTarget=0;collapsible=0;recursiveResize=0;outlineConnect=0;portConstraint=eastwest;newEdgeStyle={"curved":0,"rounded":0};participant=umlEntity;strokeWidth=2;" vertex="1" parent="0ZE-dMPOFneTTZtpNr96-1">
|
||||
<mxGeometry x="690" y="40" width="40" height="3110" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="0ZE-dMPOFneTTZtpNr96-5" value="PING" style="text;html=1;align=center;verticalAlign=middle;resizable=0;points=[];autosize=1;strokeColor=none;fillColor=none;fontSize=23;" vertex="1" parent="0ZE-dMPOFneTTZtpNr96-1">
|
||||
<mxGeometry x="385" y="65" width="80" height="40" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="0ZE-dMPOFneTTZtpNr96-8" value="Version" style="swimlane;whiteSpace=wrap;html=1;" vertex="1" parent="0ZE-dMPOFneTTZtpNr96-1">
|
||||
<mxGeometry x="171.25" y="130" width="58.75" height="80" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="0ZE-dMPOFneTTZtpNr96-9" value="(0-128)" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" vertex="1" parent="0ZE-dMPOFneTTZtpNr96-8">
|
||||
<mxGeometry x="3.75" y="30" width="51.25" height="25" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="0ZE-dMPOFneTTZtpNr96-12" value="" style="html=1;shadow=0;dashed=0;align=center;verticalAlign=middle;shape=mxgraph.arrows2.arrow;dy=0;dx=10;notch=0;rotation=-180;" vertex="1" parent="0ZE-dMPOFneTTZtpNr96-1">
|
||||
<mxGeometry x="350" y="280" width="340" height="190" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="0ZE-dMPOFneTTZtpNr96-13" value="Timestamp" style="swimlane;whiteSpace=wrap;html=1;" vertex="1" parent="0ZE-dMPOFneTTZtpNr96-1">
|
||||
<mxGeometry x="380" y="290" width="90" height="60" as="geometry">
|
||||
<mxRectangle x="210" y="130" width="80" height="30" as="alternateBounds" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="0ZE-dMPOFneTTZtpNr96-14" value="timestamp" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;fontSize=10;strokeWidth=1;" vertex="1" parent="0ZE-dMPOFneTTZtpNr96-13">
|
||||
<mxGeometry x="11.25" y="27.5" width="67.5" height="25" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="0ZE-dMPOFneTTZtpNr96-15" value="Version" style="swimlane;whiteSpace=wrap;html=1;" vertex="1" parent="0ZE-dMPOFneTTZtpNr96-1">
|
||||
<mxGeometry x="479.76" y="290" width="58.75" height="60" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="0ZE-dMPOFneTTZtpNr96-16" value="0" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" vertex="1" parent="0ZE-dMPOFneTTZtpNr96-15">
|
||||
<mxGeometry y="25" width="60" height="30" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="0ZE-dMPOFneTTZtpNr96-19" value="Answer" style="swimlane;whiteSpace=wrap;html=1;" vertex="1" parent="0ZE-dMPOFneTTZtpNr96-1">
|
||||
<mxGeometry x="548.76" y="290" width="57.5" height="60" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="0ZE-dMPOFneTTZtpNr96-20" value="<div>YES</div><div>NO<br></div>" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" vertex="1" parent="0ZE-dMPOFneTTZtpNr96-1">
|
||||
<mxGeometry x="533.76" y="315" width="53.75" height="30" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="0ZE-dMPOFneTTZtpNr96-21" value="HANDSHAKE" style="text;html=1;align=center;verticalAlign=middle;resizable=0;points=[];autosize=1;strokeColor=none;fillColor=none;fontSize=23;" vertex="1" parent="0ZE-dMPOFneTTZtpNr96-1">
|
||||
<mxGeometry x="350" y="510" width="170" height="40" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="0ZE-dMPOFneTTZtpNr96-22" value="" style="html=1;shadow=0;dashed=0;align=center;verticalAlign=middle;shape=mxgraph.arrows2.arrow;dy=0;dx=10;notch=0;" vertex="1" parent="0ZE-dMPOFneTTZtpNr96-1">
|
||||
<mxGeometry x="160" y="570" width="410" height="220" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="0ZE-dMPOFneTTZtpNr96-23" value="Clé éphémère" style="swimlane;whiteSpace=wrap;html=1;" vertex="1" parent="0ZE-dMPOFneTTZtpNr96-1">
|
||||
<mxGeometry x="170" y="580" width="105" height="80" as="geometry">
|
||||
<mxRectangle x="210" y="130" width="80" height="30" as="alternateBounds" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="0ZE-dMPOFneTTZtpNr96-24" value="Clé (publique) générée aléatoirement" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;fontSize=11;" vertex="1" parent="0ZE-dMPOFneTTZtpNr96-23">
|
||||
<mxGeometry y="30" width="100" height="40" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="0ZE-dMPOFneTTZtpNr96-25" value="" style="swimlane;whiteSpace=wrap;html=1;" vertex="1" parent="0ZE-dMPOFneTTZtpNr96-1">
|
||||
<mxGeometry x="285" y="580" width="105" height="80" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="0ZE-dMPOFneTTZtpNr96-29" value="" style="swimlane;whiteSpace=wrap;html=1;" vertex="1" parent="0ZE-dMPOFneTTZtpNr96-1">
|
||||
<mxGeometry x="402.81" y="580" width="71.88" height="80" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="0ZE-dMPOFneTTZtpNr96-31" value="" style="html=1;shadow=0;dashed=0;align=center;verticalAlign=middle;shape=mxgraph.arrows2.arrow;dy=0;dx=10;notch=0;rotation=-180;" vertex="1" parent="0ZE-dMPOFneTTZtpNr96-1">
|
||||
<mxGeometry x="285" y="830" width="410" height="180" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="0ZE-dMPOFneTTZtpNr96-32" value="Clé éphémère" style="swimlane;whiteSpace=wrap;html=1;" vertex="1" parent="0ZE-dMPOFneTTZtpNr96-1">
|
||||
<mxGeometry x="305" y="840" width="105" height="80" as="geometry">
|
||||
<mxRectangle x="210" y="130" width="80" height="30" as="alternateBounds" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="0ZE-dMPOFneTTZtpNr96-33" value="Clé (publique) générée aléatoirement" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;fontSize=11;" vertex="1" parent="0ZE-dMPOFneTTZtpNr96-32">
|
||||
<mxGeometry y="30" width="100" height="40" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="0ZE-dMPOFneTTZtpNr96-34" value="" style="swimlane;whiteSpace=wrap;html=1;startSize=23;" vertex="1" parent="0ZE-dMPOFneTTZtpNr96-1">
|
||||
<mxGeometry x="420" y="840" width="105" height="80" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="0ZE-dMPOFneTTZtpNr96-38" value="" style="swimlane;whiteSpace=wrap;html=1;" vertex="1" parent="0ZE-dMPOFneTTZtpNr96-1">
|
||||
<mxGeometry x="537.81" y="840" width="71.88" height="80" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="0ZE-dMPOFneTTZtpNr96-40" value="Timestamp" style="swimlane;whiteSpace=wrap;html=1;" vertex="1" parent="0ZE-dMPOFneTTZtpNr96-1">
|
||||
<mxGeometry x="182.5" y="690" width="80" height="70" as="geometry">
|
||||
<mxRectangle x="210" y="130" width="80" height="30" as="alternateBounds" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="0ZE-dMPOFneTTZtpNr96-41" value="timestamp" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;fontSize=10;strokeWidth=1;" vertex="1" parent="0ZE-dMPOFneTTZtpNr96-40">
|
||||
<mxGeometry x="6.25" y="32.5" width="67.5" height="25" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="0ZE-dMPOFneTTZtpNr96-42" value="Timestamp" style="swimlane;whiteSpace=wrap;html=1;" vertex="1" parent="0ZE-dMPOFneTTZtpNr96-1">
|
||||
<mxGeometry x="606.25" y="930" width="80" height="70" as="geometry">
|
||||
<mxRectangle x="210" y="130" width="80" height="30" as="alternateBounds" />
|
||||
</mxGeometry>
|
||||
</mxCell>
|
||||
<mxCell id="0ZE-dMPOFneTTZtpNr96-43" value="timestamp" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;fontSize=10;strokeWidth=1;" vertex="1" parent="0ZE-dMPOFneTTZtpNr96-42">
|
||||
<mxGeometry x="6.25" y="32.5" width="67.5" height="25" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="0ZE-dMPOFneTTZtpNr96-44" value="" style="html=1;shadow=0;dashed=0;align=center;verticalAlign=middle;shape=mxgraph.arrows2.arrow;dy=0;dx=10;notch=0;" vertex="1" parent="0ZE-dMPOFneTTZtpNr96-1">
|
||||
<mxGeometry x="160" y="1160" width="450" height="200" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="0ZE-dMPOFneTTZtpNr96-45" value="ENCRYPTED COMS" style="text;html=1;align=center;verticalAlign=middle;resizable=0;points=[];autosize=1;strokeColor=none;fillColor=none;fontSize=23;" vertex="1" parent="0ZE-dMPOFneTTZtpNr96-1">
|
||||
<mxGeometry x="305" y="1100" width="240" height="40" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="0ZE-dMPOFneTTZtpNr96-47" value="7b" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" vertex="1" parent="0ZE-dMPOFneTTZtpNr96-1">
|
||||
<mxGeometry x="170" y="210" width="60" height="30" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="0ZE-dMPOFneTTZtpNr96-49" value="= 48b" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" vertex="1" parent="0ZE-dMPOFneTTZtpNr96-1">
|
||||
<mxGeometry x="350" y="210" width="60" height="30" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="0ZE-dMPOFneTTZtpNr96-50" value="16b" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" vertex="1" parent="0ZE-dMPOFneTTZtpNr96-1">
|
||||
<mxGeometry x="393" y="350" width="60" height="30" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="0ZE-dMPOFneTTZtpNr96-51" value="7b" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" vertex="1" parent="0ZE-dMPOFneTTZtpNr96-1">
|
||||
<mxGeometry x="479.13" y="350" width="60" height="30" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="0ZE-dMPOFneTTZtpNr96-52" value="1b" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" vertex="1" parent="0ZE-dMPOFneTTZtpNr96-1">
|
||||
<mxGeometry x="545.51" y="350" width="60" height="30" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="0ZE-dMPOFneTTZtpNr96-54" value="= 32b" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" vertex="1" parent="0ZE-dMPOFneTTZtpNr96-1">
|
||||
<mxGeometry x="465" y="395" width="60" height="30" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="0ZE-dMPOFneTTZtpNr96-55" value="264b" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" vertex="1" parent="0ZE-dMPOFneTTZtpNr96-1">
|
||||
<mxGeometry x="193" y="660" width="60" height="30" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="0ZE-dMPOFneTTZtpNr96-56" value="512b" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" vertex="1" parent="0ZE-dMPOFneTTZtpNr96-1">
|
||||
<mxGeometry x="307.5" y="660" width="60" height="30" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="0ZE-dMPOFneTTZtpNr96-57" value="256b" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" vertex="1" parent="0ZE-dMPOFneTTZtpNr96-1">
|
||||
<mxGeometry x="409.38" y="660" width="60" height="30" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="0ZE-dMPOFneTTZtpNr96-59" value="16b" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" vertex="1" parent="0ZE-dMPOFneTTZtpNr96-1">
|
||||
<mxGeometry x="192.5" y="760" width="60" height="30" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="0ZE-dMPOFneTTZtpNr96-60" value="=1096b" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" vertex="1" parent="0ZE-dMPOFneTTZtpNr96-1">
|
||||
<mxGeometry x="315" y="750" width="60" height="30" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="0ZE-dMPOFneTTZtpNr96-61" value="=1096b" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" vertex="1" parent="0ZE-dMPOFneTTZtpNr96-1">
|
||||
<mxGeometry x="327.5" y="970" width="60" height="30" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="0ZE-dMPOFneTTZtpNr96-66" value="nbretry" style="swimlane;whiteSpace=wrap;html=1;" vertex="1" parent="0ZE-dMPOFneTTZtpNr96-1">
|
||||
<mxGeometry x="344.38" y="1170" width="65" height="60" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="0ZE-dMPOFneTTZtpNr96-67" value="y" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" vertex="1" parent="0ZE-dMPOFneTTZtpNr96-66">
|
||||
<mxGeometry x="2.5" y="25" width="60" height="30" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="0ZE-dMPOFneTTZtpNr96-70" value="msg" style="swimlane;whiteSpace=wrap;html=1;fillColor=#0050ef;fontColor=#ffffff;strokeColor=#001DBC;" vertex="1" parent="0ZE-dMPOFneTTZtpNr96-1">
|
||||
<mxGeometry x="187.5" y="1270" width="65" height="60" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="0ZE-dMPOFneTTZtpNr96-71" value="BBB" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" vertex="1" parent="0ZE-dMPOFneTTZtpNr96-70">
|
||||
<mxGeometry x="2.5" y="30" width="60" height="30" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="0ZE-dMPOFneTTZtpNr96-73" value="8b" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" vertex="1" parent="0ZE-dMPOFneTTZtpNr96-1">
|
||||
<mxGeometry x="349.38" y="1230" width="55" height="30" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="0ZE-dMPOFneTTZtpNr96-77" value="= (180b ~ 212b) + yyy" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" vertex="1" parent="0ZE-dMPOFneTTZtpNr96-1">
|
||||
<mxGeometry x="465" y="1285" width="130" height="30" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="0ZE-dMPOFneTTZtpNr96-78" value="Cypher" style="swimlane;whiteSpace=wrap;html=1;" vertex="1" parent="0ZE-dMPOFneTTZtpNr96-1">
|
||||
<mxGeometry x="240" y="130" width="58.75" height="80" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="0ZE-dMPOFneTTZtpNr96-79" value="(0-32)" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" vertex="1" parent="0ZE-dMPOFneTTZtpNr96-78">
|
||||
<mxGeometry x="3.75" y="30" width="51.25" height="25" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="0ZE-dMPOFneTTZtpNr96-80" value="5b" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" vertex="1" parent="0ZE-dMPOFneTTZtpNr96-1">
|
||||
<mxGeometry x="240" y="210" width="55" height="30" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="0ZE-dMPOFneTTZtpNr96-81" value="Cypher Offset" style="swimlane;whiteSpace=wrap;html=1;" vertex="1" parent="0ZE-dMPOFneTTZtpNr96-1">
|
||||
<mxGeometry x="616.88" y="290" width="58.75" height="60" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="0ZE-dMPOFneTTZtpNr96-82" value="(0-8)" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" vertex="1" parent="0ZE-dMPOFneTTZtpNr96-81">
|
||||
<mxGeometry x="3.75" y="30" width="51.25" height="25" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="0ZE-dMPOFneTTZtpNr96-83" value="4b" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" vertex="1" parent="0ZE-dMPOFneTTZtpNr96-1">
|
||||
<mxGeometry x="618.76" y="350" width="55" height="30" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="0ZE-dMPOFneTTZtpNr96-84" value="status" style="swimlane;whiteSpace=wrap;html=1;" vertex="1" parent="0ZE-dMPOFneTTZtpNr96-1">
|
||||
<mxGeometry x="425" y="1170" width="65" height="60" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="0ZE-dMPOFneTTZtpNr96-85" value="CRC ?" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" vertex="1" parent="0ZE-dMPOFneTTZtpNr96-84">
|
||||
<mxGeometry x="2.5" y="25" width="60" height="30" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="0ZE-dMPOFneTTZtpNr96-86" value="4b" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" vertex="1" parent="0ZE-dMPOFneTTZtpNr96-1">
|
||||
<mxGeometry x="428.75" y="1230" width="55" height="30" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="0ZE-dMPOFneTTZtpNr96-89" value="BBB b" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" vertex="1" parent="0ZE-dMPOFneTTZtpNr96-1">
|
||||
<mxGeometry x="193" y="1330" width="55" height="30" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="0ZE-dMPOFneTTZtpNr96-90" value="MAC" style="swimlane;whiteSpace=wrap;html=1;fillColor=#008a00;fontColor=#ffffff;strokeColor=#005700;" vertex="1" parent="0ZE-dMPOFneTTZtpNr96-1">
|
||||
<mxGeometry x="286.13" y="1270" width="63.25" height="60" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="0ZE-dMPOFneTTZtpNr96-91" value="AEAD" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" vertex="1" parent="0ZE-dMPOFneTTZtpNr96-90">
|
||||
<mxGeometry x="1.6199999999999992" y="25" width="60" height="30" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="0ZE-dMPOFneTTZtpNr96-92" value="128b" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" vertex="1" parent="0ZE-dMPOFneTTZtpNr96-1">
|
||||
<mxGeometry x="290.25" y="1330" width="55" height="30" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="0ZE-dMPOFneTTZtpNr96-93" value="Green = clear data" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;fillColor=#008a00;fontColor=#ffffff;strokeColor=#005700;" vertex="1" parent="0ZE-dMPOFneTTZtpNr96-1">
|
||||
<mxGeometry x="10" y="1170" width="110" height="30" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="0ZE-dMPOFneTTZtpNr96-94" value="<font style="color: light-dark(rgb(0, 0, 0), rgb(255, 255, 255));">White = additional data</font>" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;fillColor=none;strokeColor=light-dark(#6C8EBF,#FFFFFF);" vertex="1" parent="0ZE-dMPOFneTTZtpNr96-1">
|
||||
<mxGeometry y="1220" width="130" height="30" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="0ZE-dMPOFneTTZtpNr96-95" value="Blue = encrypted data" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;fillColor=#0050ef;fontColor=#ffffff;strokeColor=#001DBC;" vertex="1" parent="0ZE-dMPOFneTTZtpNr96-1">
|
||||
<mxGeometry x="10" y="1270" width="110" height="30" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="el23RTDFL3Ay36EOE6FN-0" value="Quality" style="swimlane;whiteSpace=wrap;html=1;" vertex="1" parent="0ZE-dMPOFneTTZtpNr96-1">
|
||||
<mxGeometry x="308.75" y="130" width="58.75" height="80" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="el23RTDFL3Ay36EOE6FN-1" value="(0-16)" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" vertex="1" parent="el23RTDFL3Ay36EOE6FN-0">
|
||||
<mxGeometry x="3.75" y="30" width="51.25" height="25" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="el23RTDFL3Ay36EOE6FN-2" value="4b" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" vertex="1" parent="0ZE-dMPOFneTTZtpNr96-1">
|
||||
<mxGeometry x="308.75" y="210" width="55" height="30" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="el23RTDFL3Ay36EOE6FN-3" value="4b" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" vertex="1" parent="0ZE-dMPOFneTTZtpNr96-1">
|
||||
<mxGeometry x="616.88" y="440" width="55" height="30" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="el23RTDFL3Ay36EOE6FN-4" value="Quality" style="swimlane;whiteSpace=wrap;html=1;" vertex="1" parent="0ZE-dMPOFneTTZtpNr96-1">
|
||||
<mxGeometry x="616.88" y="380" width="58.75" height="60" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="el23RTDFL3Ay36EOE6FN-5" value="(0-16)" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;" vertex="1" parent="el23RTDFL3Ay36EOE6FN-4">
|
||||
<mxGeometry x="3.75" y="30" width="51.25" height="25" as="geometry" />
|
||||
</mxCell>
|
||||
<mxCell id="1WmqBiAd3lf2sxgMsw2H-0" value="Best Case Scenario<div>Noise_XK_25519_ChaChaPoly_SHA256</div>" style="text;html=1;align=center;verticalAlign=middle;whiteSpace=wrap;rounded=0;fontSize=25;fontStyle=1" vertex="1" parent="0ZE-dMPOFneTTZtpNr96-1">
|
||||
<mxGeometry x="223" y="-20" width="405" height="30" as="geometry" />
|
||||
</mxCell>
|
||||
</root>
|
||||
</mxGraphModel>
|
||||
</diagram>
|
||||
</mxfile>
|
@ -1,91 +0,0 @@
|
||||
import argparse
|
||||
import threading
|
||||
import sys
|
||||
|
||||
from noise_xk.session import NoiseXKSession
|
||||
from noise_xk.transport import P2PTransport
|
||||
from dissononce.dh.x25519.public import PublicKey
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(prog="noise_xk")
|
||||
parser.add_argument(
|
||||
"--listen-port", type=int, required=True,
|
||||
help="Port on which to bind+listen"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--peer-host", type=str, default="127.0.0.1",
|
||||
help="Peer host to dial (default: 127.0.0.1)"
|
||||
)
|
||||
parser.add_argument(
|
||||
"--peer-port", type=int, required=True,
|
||||
help="Peer port to dial"
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
# 1. Generate static keypair and print our static public key
|
||||
kp = NoiseXKSession.generate_keypair()
|
||||
# kp.public is a PublicKey; .data holds raw bytes
|
||||
local_priv = kp.private # carried implicitly in NoiseXKSession
|
||||
local_pub = kp.public
|
||||
print(f"[My static pubkey:] {local_pub.data.hex()}")
|
||||
|
||||
# 2. Read peer pubkey from user input
|
||||
peer_pubkey = None
|
||||
while True:
|
||||
line = input(">>> ").strip()
|
||||
if line.startswith("peer_pubkey "):
|
||||
hexstr = line.split(None, 1)[1]
|
||||
try:
|
||||
raw = bytes.fromhex(hexstr)
|
||||
peer_pubkey = PublicKey(raw) # wrap raw bytes in PublicKey
|
||||
break
|
||||
except ValueError:
|
||||
print("Invalid hex; please retry.")
|
||||
else:
|
||||
print("Use: peer_pubkey <hex>")
|
||||
|
||||
# 3. Establish P2P connection (race listen vs. dial)
|
||||
transport = P2PTransport(
|
||||
listen_port=args.listen_port,
|
||||
peer_host=args.peer_host,
|
||||
peer_port=args.peer_port
|
||||
)
|
||||
print(
|
||||
f"Racing connect/listen on ports "
|
||||
f"{args.listen_port} ⇆ {args.peer_host}:{args.peer_port}…"
|
||||
)
|
||||
sock, initiator = transport.connect()
|
||||
print(f"Connected (initiator={initiator}); performing handshake…")
|
||||
|
||||
# 4. Perform Noise XK handshake
|
||||
session = NoiseXKSession(kp, peer_pubkey)
|
||||
session.handshake(sock, initiator)
|
||||
print("Handshake complete! You can now type messages.")
|
||||
|
||||
# 5. Reader thread for incoming messages
|
||||
def reader():
|
||||
while True:
|
||||
try:
|
||||
pt = session.receive(sock)
|
||||
print(f"\n< {pt.decode()}")
|
||||
except Exception as e:
|
||||
print(f"\n[Receive error ({type(e).__name__}): {e!r}]")
|
||||
break
|
||||
|
||||
thread = threading.Thread(target=reader, daemon=True)
|
||||
thread.start()
|
||||
|
||||
# 6. Main loop: send user input
|
||||
try:
|
||||
for line in sys.stdin:
|
||||
text = line.rstrip("\n")
|
||||
if not text:
|
||||
continue
|
||||
session.send(sock, text.encode())
|
||||
except KeyboardInterrupt:
|
||||
pass
|
||||
finally:
|
||||
sock.close()
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
@ -1,179 +0,0 @@
|
||||
# noise_xk/session.py
|
||||
|
||||
import socket
|
||||
import logging
|
||||
from dissononce.processing.impl.handshakestate import HandshakeState
|
||||
from dissononce.processing.impl.symmetricstate import SymmetricState
|
||||
from dissononce.processing.impl.cipherstate import CipherState
|
||||
from dissononce.processing.handshakepatterns.interactive.XK import XKHandshakePattern
|
||||
from dissononce.cipher.chachapoly import ChaChaPolyCipher
|
||||
from dissononce.dh.x25519.x25519 import X25519DH
|
||||
from dissononce.dh.keypair import KeyPair
|
||||
from dissononce.dh.x25519.public import PublicKey
|
||||
from dissononce.hash.sha256 import SHA256Hash
|
||||
|
||||
# Configure root logger for debug output
|
||||
logging.basicConfig(level=logging.DEBUG, format="%(message)s")
|
||||
|
||||
class NoiseXKSession:
|
||||
@staticmethod
|
||||
def generate_keypair() -> KeyPair:
|
||||
"""
|
||||
Generate a static X25519 KeyPair.
|
||||
Returns:
|
||||
KeyPair object with .private and .public attributes.
|
||||
"""
|
||||
return X25519DH().generate_keypair()
|
||||
|
||||
def __init__(self, local_kp: KeyPair, peer_pubkey: PublicKey):
|
||||
"""
|
||||
Initialize with our KeyPair and the peer's PublicKey.
|
||||
"""
|
||||
self.local_kp: KeyPair = local_kp
|
||||
self.peer_pubkey: PublicKey = peer_pubkey
|
||||
|
||||
# Build the Noise handshake state (X25519 DH, ChaChaPoly cipher, SHA256 hash)
|
||||
cipher = ChaChaPolyCipher()
|
||||
dh = X25519DH()
|
||||
hshash = SHA256Hash()
|
||||
symmetric = SymmetricState(CipherState(cipher), hshash)
|
||||
self._hs = HandshakeState(symmetric, dh)
|
||||
|
||||
self._send_cs = None # type: CipherState
|
||||
self._recv_cs = None
|
||||
|
||||
def handshake(self, sock: socket.socket, initiator: bool) -> None:
|
||||
"""
|
||||
Perform the XK handshake over the socket. Branches on initiator/responder
|
||||
so that each side reads or writes in the correct message order.
|
||||
On completion, self._send_cs and self._recv_cs hold the two CipherStates.
|
||||
"""
|
||||
|
||||
logging.debug(f"[handshake] start (initiator={initiator})")
|
||||
# initialize with our KeyPair and their PublicKey
|
||||
if initiator:
|
||||
# initiator knows peer’s static out-of-band
|
||||
self._hs.initialize(
|
||||
XKHandshakePattern(),
|
||||
True,
|
||||
b'',
|
||||
s=self.local_kp,
|
||||
rs=self.peer_pubkey
|
||||
)
|
||||
else:
|
||||
logging.debug("[handshake] responder initializing without rs")
|
||||
# responder must NOT supply rs here
|
||||
self._hs.initialize(
|
||||
XKHandshakePattern(),
|
||||
False,
|
||||
b'',
|
||||
s=self.local_kp
|
||||
)
|
||||
|
||||
cs_pair = None
|
||||
if initiator:
|
||||
# 1) -> e
|
||||
buf1 = bytearray()
|
||||
cs_pair = self._hs.write_message(b'', buf1)
|
||||
logging.debug(f"[-> e] {buf1.hex()}")
|
||||
self._send_all(sock, buf1)
|
||||
|
||||
# 2) <- e, es, s, ss
|
||||
msg2 = self._recv_all(sock)
|
||||
logging.debug(f"[<- msg2] {msg2.hex()}")
|
||||
self._hs.read_message(msg2, bytearray())
|
||||
|
||||
# 3) -> se (final)
|
||||
buf3 = bytearray()
|
||||
cs_pair = self._hs.write_message(b'', buf3)
|
||||
logging.debug(f"[-> se] {buf3.hex()}")
|
||||
self._send_all(sock, buf3)
|
||||
else:
|
||||
# 1) <- e
|
||||
msg1 = self._recv_all(sock)
|
||||
logging.debug(f"[<- e] {msg1.hex()}")
|
||||
self._hs.read_message(msg1, bytearray())
|
||||
|
||||
# 2) -> e, es, s, ss
|
||||
buf2 = bytearray()
|
||||
cs_pair = self._hs.write_message(b'', buf2)
|
||||
logging.debug(f"[-> msg2] {buf2.hex()}")
|
||||
self._send_all(sock, buf2)
|
||||
|
||||
# 3) <- se (final)
|
||||
msg3 = self._recv_all(sock)
|
||||
logging.debug(f"[<- se] {msg3.hex()}")
|
||||
cs_pair = self._hs.read_message(msg3, bytearray())
|
||||
|
||||
# on the final step, we must get exactly two CipherStates
|
||||
if not cs_pair or len(cs_pair) != 2:
|
||||
raise RuntimeError("Handshake did not complete properly")
|
||||
cs0, cs1 = cs_pair
|
||||
# the library returns (cs_encrypt_for_initiator, cs_decrypt_for_initiator)
|
||||
if initiator:
|
||||
# initiator: cs0 encrypts, cs1 decrypts
|
||||
self._send_cs, self._recv_cs = cs0, cs1
|
||||
else:
|
||||
# responder must swap
|
||||
self._send_cs, self._recv_cs = cs1, cs0
|
||||
|
||||
# dump the raw symmetric keys & nonces (if available)
|
||||
self._dump_cipherstate("HANDSHAKE→ SEND", self._send_cs)
|
||||
self._dump_cipherstate("HANDSHAKE→ RECV", self._recv_cs)
|
||||
|
||||
def send(self, sock: socket.socket, plaintext: bytes) -> None:
|
||||
"""
|
||||
Encrypt and send a message.
|
||||
"""
|
||||
if self._send_cs is None:
|
||||
raise RuntimeError("Handshake not complete")
|
||||
ct = self._send_cs.encrypt_with_ad(b'', plaintext)
|
||||
logging.debug(f"[ENCRYPT] {ct.hex()}")
|
||||
self._dump_cipherstate("SEND→ after encrypt", self._send_cs)
|
||||
self._send_all(sock, ct)
|
||||
|
||||
def receive(self, sock: socket.socket) -> bytes:
|
||||
"""
|
||||
Receive and decrypt a message.
|
||||
"""
|
||||
if self._recv_cs is None:
|
||||
raise RuntimeError("Handshake not complete")
|
||||
ct = self._recv_all(sock)
|
||||
logging.debug(f"[CIPHERTEXT] {ct.hex()}")
|
||||
self._dump_cipherstate("RECV→ before decrypt", self._recv_cs)
|
||||
pt = self._recv_cs.decrypt_with_ad(b'', ct)
|
||||
logging.debug(f"[DECRYPT] {pt!r}")
|
||||
return pt
|
||||
|
||||
def _send_all(self, sock: socket.socket, data: bytes) -> None:
|
||||
# Length-prefix (2 bytes big-endian) + data
|
||||
length = len(data).to_bytes(2, 'big')
|
||||
sock.sendall(length + data)
|
||||
|
||||
def _recv_all(self, sock: socket.socket) -> bytes:
|
||||
# Read 2-byte length prefix, then the payload
|
||||
hdr = self._read_exact(sock, 2)
|
||||
length = int.from_bytes(hdr, 'big')
|
||||
return self._read_exact(sock, length)
|
||||
|
||||
@staticmethod
|
||||
def _read_exact(sock: socket.socket, n: int) -> bytes:
|
||||
buf = bytearray()
|
||||
while len(buf) < n:
|
||||
chunk = sock.recv(n - len(buf))
|
||||
if not chunk:
|
||||
raise ConnectionError("Socket closed during read")
|
||||
buf.extend(chunk)
|
||||
return bytes(buf)
|
||||
|
||||
def _dump_cipherstate(self, label: str, cs: CipherState) -> None:
|
||||
"""
|
||||
Print the symmetric key (cs._k) and nonce counter (cs._n) for inspection.
|
||||
"""
|
||||
key = cs._key
|
||||
nonce = getattr(cs, "_n", None)
|
||||
if isinstance(key, (bytes, bytearray)):
|
||||
key_hex = key.hex()
|
||||
else:
|
||||
key_hex = repr(key)
|
||||
logging.debug(f"[{label}] key={key_hex}")
|
@ -1,99 +0,0 @@
|
||||
import socket
|
||||
import threading
|
||||
import time
|
||||
|
||||
class P2PTransport:
|
||||
def __init__(self, listen_port: int, peer_host: str, peer_port: int):
|
||||
"""
|
||||
Args:
|
||||
listen_port: port to bind() and accept()
|
||||
peer_host: host to dial()
|
||||
peer_port: port to dial()
|
||||
"""
|
||||
self.listen_port = listen_port
|
||||
self.peer_host = peer_host
|
||||
self.peer_port = peer_port
|
||||
|
||||
def connect(self) -> (socket.socket, bool):
|
||||
"""
|
||||
Race bind+listen vs. dial:
|
||||
- If dial succeeds first, return (sock, True) # we are initiator
|
||||
- If accept succeeds first, return (sock, False) # we are responder
|
||||
"""
|
||||
server = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
server.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
|
||||
server.bind(('0.0.0.0', self.listen_port))
|
||||
server.listen(1)
|
||||
|
||||
result = {}
|
||||
event = threading.Event()
|
||||
lock = threading.Lock()
|
||||
|
||||
def accept_thread():
|
||||
try:
|
||||
conn, _ = server.accept()
|
||||
with lock:
|
||||
if not event.is_set():
|
||||
result['sock'] = conn
|
||||
result['initiator'] = False
|
||||
event.set()
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
def dial_thread():
|
||||
sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
|
||||
sock.settimeout(1.0)
|
||||
while not event.is_set():
|
||||
try:
|
||||
sock.connect((self.peer_host, self.peer_port))
|
||||
with lock:
|
||||
if not event.is_set():
|
||||
result['sock'] = sock
|
||||
result['initiator'] = True
|
||||
event.set()
|
||||
return
|
||||
except (ConnectionRefusedError, socket.timeout):
|
||||
time.sleep(0.1)
|
||||
except Exception:
|
||||
break
|
||||
|
||||
t1 = threading.Thread(target=accept_thread, daemon=True)
|
||||
t2 = threading.Thread(target=dial_thread, daemon=True)
|
||||
t1.start()
|
||||
t2.start()
|
||||
|
||||
event.wait()
|
||||
sock, initiator = result['sock'], result['initiator']
|
||||
# close the listening socket—we’ve got our P2P link
|
||||
server.close()
|
||||
# ensure this socket is in blocking mode (no lingering timeouts)
|
||||
sock.settimeout(None)
|
||||
return sock, initiator
|
||||
|
||||
def send_packet(self, sock: socket.socket, data: bytes) -> None:
|
||||
"""
|
||||
Send a 2-byte big-endian length prefix followed by data.
|
||||
"""
|
||||
length = len(data).to_bytes(2, 'big')
|
||||
sock.sendall(length + data)
|
||||
|
||||
def recv_packet(self, sock: socket.socket) -> bytes:
|
||||
"""
|
||||
Receive a 2-byte length prefix, then that many payload bytes.
|
||||
"""
|
||||
hdr = self._read_exact(sock, 2)
|
||||
length = int.from_bytes(hdr, 'big')
|
||||
return self._read_exact(sock, length)
|
||||
|
||||
@staticmethod
|
||||
def _read_exact(sock: socket.socket, n: int) -> bytes:
|
||||
buf = bytearray()
|
||||
while len(buf) < n:
|
||||
chunk = sock.recv(n - len(buf))
|
||||
if not chunk:
|
||||
raise ConnectionError("Socket closed during read")
|
||||
buf.extend(chunk)
|
||||
return bytes(buf)
|
||||
|
||||
def close(self, sock: socket.socket) -> None:
|
||||
sock.close()
|
@ -1,7 +0,0 @@
|
||||
# System install
|
||||
Docker
|
||||
Python3
|
||||
|
||||
# Venv install
|
||||
PyQt5
|
||||
dissononce
|
Loading…
Reference in New Issue
Block a user