Compare commits

..

1 Commits

Author SHA1 Message Date
5975977a52 Trying to get audio (DON'T COMPILE)
All checks were successful
/ mirror (push) Successful in 4s
2025-01-17 14:40:02 +02:00
191 changed files with 5402 additions and 15888 deletions

View File

@ -1,31 +0,0 @@
on:
push:
paths:
- dialer/**
jobs:
build:
runs-on: debian
steps:
- uses: actions/checkout@v1
with:
subpath: dialer/
- uses: docker://git.gmoker.com/icing/flutter:main
- uses: actions/upload-artifact@v1
with:
name: icing-dialer-${{ gitea.ref_name }}-${{ gitea.run_id }}.apk
path: build/app/outputs/flutter-apk/app-release.apk
build-stealth:
runs-on: debian
steps:
- uses: actions/checkout@v1
with:
subpath: dialer/
- uses: docker://git.gmoker.com/icing/flutter:main
with:
args: "build apk --dart-define=STEALTH=true"
- uses: actions/upload-artifact@v1
with:
name: icing-dialer-stealth-${{ gitea.ref_name }}-${{ gitea.run_id }}.apk
path: build/app/outputs/flutter-apk/app-release.apk

View File

@ -6,10 +6,11 @@ on:
jobs:
deploy:
runs-on: debian
defaults:
run:
working-directory: website
steps:
- uses: actions/checkout@v1
with:
subpath: website/
- name: setup env
run: |
. ./.env || true
@ -28,8 +29,10 @@ jobs:
- uses: actions/kaniko@v1
with:
password: "${{ secrets.PKGRW }}"
dockerfile: website/Dockerfile
- uses: actions/k8sdeploy@v1
with:
kubeconfig: "${{ secrets.K8S }}"
registry_password: "${{ secrets.PKGRW }}"
workdir: website

3
.gitignore vendored
View File

@ -9,7 +9,6 @@
.history
.svn/
migrate_working_dir/
protocol_prototype/venv
# IntelliJ related
*.iml
@ -183,5 +182,3 @@ app.*.symbols
!/packages/flutter_tools/test/data/dart_dependencies_test/**/.packages
!/dev/ci/**/Gemfile.lock
!.vscode/settings.json
dialer/android/gradle.properties
.gitignore

View File

@ -1,7 +1,5 @@
# Icing
## Encrypting phone calls on an analog audio level
An Epitech Innovation Project
*By*
@ -10,12 +8,6 @@ An Epitech Innovation Project
---
The **docs** folder contains documentation about:
#### Epitech
- The Beta Test Plan
- The Delivrables
#### Icing
- The project
- A user manual
- Our automations

View File

@ -4,10 +4,8 @@ gradle-wrapper.jar
/gradlew
/gradlew.bat
/local.properties
/gradle.properties
GeneratedPluginRegistrant.java
gradle.properties
.cxx
# Remember to never publicly share your keystore.
# See https://flutter.dev/to/reference-keystore

View File

@ -6,25 +6,25 @@ plugins {
}
android {
namespace = "com.icing.dialer"
namespace = "com.example.dialer"
compileSdk = flutter.compileSdkVersion
ndkVersion = flutter.ndkVersion
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_17
jvmTarget = JavaVersion.VERSION_1_8
}
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId = "com.icing.dialer"
applicationId = "com.example.dialer"
// You can update the following values to match your application needs.
// For more information, see: https://flutter.dev/to/review-gradle-config.
minSdk = flutter.minSdkVersion
minSdk = 23
targetSdk = flutter.targetSdkVersion
versionCode = flutter.versionCode
versionName = flutter.versionName

View File

@ -5,6 +5,9 @@
<uses-permission android:name="android.permission.SEND_SMS" />
<uses-permission android:name="android.permission.READ_BLOCKED_NUMBERS" />
<uses-permission android:name="android.permission.WRITE_BLOCKED_NUMBERS" />
<uses-permission android:name="android.permission.RECORD_AUDIO" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
<uses-feature android:name="android.hardware.camera" android:required="false" />
<!-- The INTERNET permission is required for development. Specifically,
the Flutter tool needs it to communicate with the running application

View File

@ -1,27 +1,17 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.icing.dialer">
<uses-feature android:name="android.hardware.telephony" android:required="true" />
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.READ_CONTACTS"/>
<uses-permission android:name="android.permission.WRITE_CONTACTS"/>
<uses-permission android:name="android.permission.CALL_PHONE" />
<uses-permission android:name="android.permission.SEND_SMS" />
<uses-permission android:name="android.permission.READ_BLOCKED_NUMBERS" />
<uses-permission android:name="android.permission.WRITE_BLOCKED_NUMBERS" />
<uses-permission android:name="android.permission.READ_CALL_LOG"/>
<uses-permission android:name="android.permission.READ_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"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher"
android:enableOnBackInvokedCallback="true">
<activity
android:name=".activities.MainActivity"
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTop"
android:taskAffinity=""
@ -41,48 +31,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

View File

@ -0,0 +1,5 @@
package com.example.dialer
import io.flutter.embedding.android.FlutterActivity
class MainActivity: FlutterActivity()

View File

@ -1,155 +0,0 @@
package com.icing.dialer
import android.os.Build
import android.security.keystore.KeyGenParameterSpec
import android.security.keystore.KeyProperties
import android.util.Base64
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()
"signData" -> signData()
"getPublicKey" -> getPublicKey()
"deleteKeyPair" -> deleteKeyPair()
"keyPairExists" -> keyPairExists()
else -> result.notImplemented()
}
}
private fun generateEDKeyPair() {
val alias = call.argument<String>("alias")
if (alias == null) {
result.error("INVALID_ARGUMENT", "Alias is required", null)
return
}
try {
val keyStore = KeyStore.getInstance(ANDROID_KEYSTORE).apply { load(null) }
if (keyStore.containsAlias(alias)) {
result.error("KEY_EXISTS", "Key with alias \"$alias\" already exists.", null)
return
}
val keyPairGenerator = KeyPairGenerator.getInstance(
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)
.setUserAuthenticationRequired(false)
.build()
keyPairGenerator.initialize(parameterSpec)
keyPairGenerator.generateKeyPair()
result.success(null)
} catch (e: Exception) {
result.error("KEY_GENERATION_FAILED", e.message, null)
}
}
private fun signData() {
val alias = call.argument<String>("alias")
val data = call.argument<String>("data")
if (alias == null || data == null) {
result.error("INVALID_ARGUMENT", "Alias and data are required", null)
return
}
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")
signature.initSign(privateKey)
signature.update(data.toByteArray())
val signedBytes = signature.sign()
val signatureBase64 = Base64.encodeToString(signedBytes, Base64.DEFAULT)
result.success(signatureBase64)
} catch (e: Exception) {
result.error("SIGNING_FAILED", e.message, null)
}
}
private fun getPublicKey() {
val alias = call.argument<String>("alias")
if (alias == null) {
result.error("INVALID_ARGUMENT", "Alias is required", null)
return
}
try {
val keyStore = KeyStore.getInstance(ANDROID_KEYSTORE).apply { load(null) }
val certificate = keyStore.getCertificate(alias) ?: run {
result.error("CERTIFICATE_NOT_FOUND", "Certificate not found for alias \"$alias\".", null)
return
}
val publicKey = certificate.publicKey
val publicKeyBase64 = Base64.encodeToString(publicKey.encoded, Base64.DEFAULT)
result.success(publicKeyBase64)
} catch (e: Exception) {
result.error("PUBLIC_KEY_RETRIEVAL_FAILED", e.message, null)
}
}
private fun deleteKeyPair() {
val alias = call.argument<String>("alias")
if (alias == null) {
result.error("INVALID_ARGUMENT", "Alias is required", null)
return
}
try {
val keyStore = KeyStore.getInstance(ANDROID_KEYSTORE).apply { load(null) }
if (!keyStore.containsAlias(alias)) {
result.error("KEY_NOT_FOUND", "No key found with alias \"$alias\" to delete.", null)
return
}
keyStore.deleteEntry(alias)
result.success(null)
} catch (e: Exception) {
result.error("KEY_DELETION_FAILED", e.message, null)
}
}
private fun keyPairExists() {
val alias = call.argument<String>("alias")
if (alias == null) {
result.error("INVALID_ARGUMENT", "Alias is required", null)
return
}
try {
val keyStore = KeyStore.getInstance(ANDROID_KEYSTORE).apply { load(null) }
val exists = keyStore.containsAlias(alias)
result.success(exists)
} catch (e: Exception) {
result.error("KEY_CHECK_FAILED", e.message, null)
}
}
}

View File

@ -1,357 +0,0 @@
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.MethodChannel
class MainActivity : FlutterActivity() {
private val KEYSTORE_CHANNEL = "com.example.keystore"
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)
}
"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()
}
}
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, KEYSTORE_CHANNEL)
.setMethodCallHandler { call, result ->
KeystoreHelper(call, result).handleMethodCall()
}
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)
}
} 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")
}
}
}
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"
)
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))
val map = mutableMapOf<String, Any?>(
"number" to number,
"type" to type,
"date" to date,
"duration" to duration
)
logsList.add(map)
}
}
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")
}
}
}
}
}
}

View File

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

View File

@ -1,55 +0,0 @@
package com.icing.dialer.services
import android.content.Context
import android.net.Uri
import android.os.Build
import android.os.Bundle
import android.telecom.TelecomManager
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
}
} catch (e: Exception) {
Log.e(TAG, "Error making GSM call: ${e.message}", e)
false
}
}
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
}
}
}

View File

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

View File

@ -2,4 +2,4 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.10.2-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-all.zip

View File

@ -18,8 +18,8 @@ pluginManagement {
plugins {
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
id "com.android.application" version "8.3.2" apply false
id "org.jetbrains.kotlin.android" version "2.0.20" apply false
id "com.android.application" version "8.1.0" apply false
id "org.jetbrains.kotlin.android" version "1.8.22" apply false
}
include ":app"

View File

@ -2,9 +2,4 @@
IMG=git.gmoker.com/icing/flutter:main
if [ "$1" == '-s' ]; then
OPT+=(--dart-define=STEALTH=true)
fi
set -x
docker run --rm -v "$PWD:/app/" "$IMG" build apk "${OPT[@]}"
docker run --rm -v "$PWD:/app/" "$IMG" build apk

View File

@ -1,3 +0,0 @@
description: This file stores settings for Dart & Flutter DevTools.
documentation: https://docs.flutter.dev/tools/devtools/extensions#configure-extension-enablement-states
extensions:

View File

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

View File

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

View File

@ -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 [];
}
}
}

View File

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

View File

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

View File

@ -1,170 +0,0 @@
import 'dart:async';
import 'dart:convert';
import 'package:flutter/services.dart';
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
import 'package:uuid/uuid.dart';
class AsymmetricCryptoService {
static const MethodChannel _channel = MethodChannel('com.example.keystore');
final FlutterSecureStorage _secureStorage = FlutterSecureStorage();
final String _aliasPrefix = 'icing_';
final Uuid _uuid = Uuid();
/// Generates an ED25519 key pair with a unique alias and stores its metadata.
Future<String> generateKeyPair({String? label}) async {
try {
// Generate a unique identifier for the key
final String uuid = _uuid.v4();
final String alias = '$_aliasPrefix$uuid';
// Invoke native method to generate the key pair
await _channel.invokeMethod('generateKeyPair', {'alias': alias});
// Store key metadata securely
final Map<String, dynamic> keyMetadata = {
'alias': alias,
'label': label ?? 'Key $uuid',
'created_at': DateTime.now().toIso8601String(),
};
// Retrieve existing keys
final String? existingKeys = await _secureStorage.read(key: 'keys');
List<dynamic> keysList = existingKeys != null ? jsonDecode(existingKeys) : [];
// Add the new key
keysList.add(keyMetadata);
// Save updated keys list
await _secureStorage.write(key: 'keys', value: jsonEncode(keysList));
return alias;
} on PlatformException catch (e) {
throw Exception("Failed to generate key pair: ${e.message}");
}
}
/// Signs data using the specified key alias.
Future<String> signData(String alias, String data) async {
try {
final String signature = await _channel.invokeMethod('signData', {
'alias': alias,
'data': data,
});
return signature;
} on PlatformException catch (e) {
throw Exception("Failed to sign data with alias '$alias': ${e.message}");
}
}
/// Retrieves the public key for the specified alias.
Future<String> getPublicKey(String alias) async {
try {
final String publicKey = await _channel.invokeMethod('getPublicKey', {
'alias': alias,
});
return publicKey;
} on PlatformException catch (e) {
throw Exception("Failed to retrieve public key: ${e.message}");
}
}
/// Deletes the key pair associated with the specified alias and removes its metadata.
Future<void> deleteKeyPair(String alias) async {
try {
await _channel.invokeMethod('deleteKeyPair', {'alias': alias});
final String? existingKeys = await _secureStorage.read(key: 'keys');
if (existingKeys != null) {
List<dynamic> keysList = jsonDecode(existingKeys);
keysList.removeWhere((key) => key['alias'] == alias);
await _secureStorage.write(key: 'keys', value: jsonEncode(keysList));
}
} on PlatformException catch (e) {
throw Exception("Failed to delete key pair: ${e.message}");
}
}
/// Retrieves all stored key metadata.
Future<List<Map<String, dynamic>>> getAllKeys() async {
try {
final String? existingKeys = await _secureStorage.read(key: 'keys');
if (existingKeys == null) {
print("No keys found");
return [];
}
List<dynamic> keysList = jsonDecode(existingKeys);
return keysList.cast<Map<String, dynamic>>();
} catch (e) {
throw Exception("Failed to retrieve keys: $e");
}
}
/// Checks if a key pair exists for the given alias.
Future<bool> keyPairExists(String alias) async {
try {
final bool exists = await _channel.invokeMethod('keyPairExists', {'alias': alias});
return exists;
} on PlatformException catch (e) {
throw Exception("Failed to check key pair existence: ${e.message}");
}
}
/// Initializes the default key pair if it doesn't exist.
Future<void> initializeDefaultKeyPair() async {
const String defaultAlias = 'icing_default';
final List<Map<String, dynamic>> keys = await getAllKeys();
// Check if the key exists in metadata
final bool defaultKeyExists = keys.any((key) => key['alias'] == defaultAlias);
if (!defaultKeyExists) {
await _channel.invokeMethod('generateKeyPair', {'alias': defaultAlias});
final Map<String, dynamic> keyMetadata = {
'alias': defaultAlias,
'label': 'Default Key',
'created_at': DateTime.now().toIso8601String(),
};
keys.add(keyMetadata);
await _secureStorage.write(key: 'keys', value: jsonEncode(keys));
}
}
/// Updates the label of a key with the specified alias.
///
/// [alias]: The unique alias of the key to update.
/// [newLabel]: The new label to assign to the key.
///
/// Throws an exception if the key is not found or the update fails.
Future<void> updateKeyLabel(String alias, String newLabel) async {
try {
// Retrieve existing keys
final String? existingKeys = await _secureStorage.read(key: 'keys');
if (existingKeys == null) {
throw Exception("No keys found to update.");
}
List<dynamic> keysList = jsonDecode(existingKeys);
// Find the key with the specified alias
bool keyFound = false;
for (var key in keysList) {
if (key['alias'] == alias) {
key['label'] = newLabel;
keyFound = true;
break;
}
}
if (!keyFound) {
throw Exception("Key with alias \"$alias\" not found.");
}
// Save the updated keys list
await _secureStorage.write(key: 'keys', value: jsonEncode(keysList));
} catch (e) {
throw Exception("Failed to update key label: $e");
}
}
}

View File

@ -1,91 +0,0 @@
// lib/services/obfuscate_service.dart
import 'package:dialer/presentation/common/widgets/color_darkener.dart';
import '../../core/config/app_config.dart';
import 'dart:ui';
import 'dart:typed_data';
import 'package:flutter/material.dart';
class ObfuscateService {
// Private constructor
ObfuscateService._privateConstructor();
// Singleton instance
static final ObfuscateService _instance = ObfuscateService._privateConstructor();
// Factory constructor to return the same instance
factory ObfuscateService() {
return _instance;
}
// Public method to obfuscate data
String obfuscateData(String data) {
if (AppConfig.isStealthMode) {
return _obfuscateData(data);
} else {
return data;
}
}
// Private helper method for obfuscation logic
String _obfuscateData(String data) {
if (data.isNotEmpty) {
// Ensure the string has at least two characters to obfuscate
if (data.length == 1) {
return '${data[0]}';
} else {
return '${data[0]}...${data[data.length - 1]}';
}
}
return '';
}
}
class ObfuscatedAvatar extends StatelessWidget {
final Uint8List? imageBytes;
final double radius;
final Color backgroundColor;
final String? fallbackInitial;
const ObfuscatedAvatar({
Key? key,
required this.imageBytes,
this.radius = 25,
this.backgroundColor = Colors.grey,
this.fallbackInitial,
}) : super(key: key);
@override
Widget build(BuildContext context) {
if (imageBytes != null && imageBytes!.isNotEmpty) {
return ClipOval(
child: ImageFiltered(
imageFilter: AppConfig.isStealthMode
? ImageFilter.blur(sigmaX: 10, sigmaY: 10)
: ImageFilter.blur(sigmaX: 0, sigmaY: 0),
child: Image.memory(
imageBytes!,
fit: BoxFit.cover,
width: radius * 2,
height: radius * 2,
),
),
);
} else {
return CircleAvatar(
radius: radius,
backgroundColor: backgroundColor,
child: Text(
fallbackInitial != null && fallbackInitial!.isNotEmpty
? fallbackInitial![0].toUpperCase()
: '?',
style: TextStyle(
color: darken(backgroundColor),
fontSize: radius,
),
),
);
}
}
}

View File

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

View File

@ -0,0 +1,338 @@
import 'package:flutter/material.dart';
import 'package:flutter_contacts/flutter_contacts.dart';
import 'package:url_launcher/url_launcher.dart';
import '../../widgets/contact_service.dart';
import '../contacts/widgets/add_contact_button.dart';
import '../../services/audio_handler.dart';
import '../../widgets/listen_replay_button.dart';
import '../../widgets/encrypt_audio_button.dart';
import 'package:permission_handler/permission_handler.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();
// Initialize AudioHandler
final AudioHandler _audioHandler = AudioHandler();
@override
void initState() {
super.initState();
_requestPermissions();
_fetchContacts();
}
Future<void> _requestPermissions() async {
var statuses = await [
Permission.microphone,
Permission.contacts,
Permission.storage,
].request();
if (statuses[Permission.microphone] != PermissionStatus.granted ||
statuses[Permission.contacts] != PermissionStatus.granted ||
statuses[Permission.storage] != PermissionStatus.granted) {
// Handle permission denial
// For simplicity, just print a message
print('Permissions not granted');
}
}
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
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');
}
}
// 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
void dispose() {
_audioHandler.stopListening();
super.dispose();
}
@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(
contact.displayName,
style:
const TextStyle(color: Colors.white),
),
subtitle: Text(
phoneNumber,
style:
const TextStyle(color: Colors.grey),
),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
// Call button
IconButton(
icon: Icon(Icons.phone,
color: Colors.green[300],
size: 20),
onPressed: () {
_launchPhoneDialer(phoneNumber);
},
),
// Message button
IconButton(
icon: Icon(Icons.message,
color: Colors.blue[300],
size: 20),
onPressed: () {
_launchSms(phoneNumber);
},
),
],
),
onTap: () {
// Handle contact selection if needed
},
);
}).toList()
: [
Center(
child: Text('No contacts found',
style:
TextStyle(color: Colors.white)))
],
),
),
],
),
),
),
// 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);
},
),
),
// New Buttons: Listen/Replay and Encrypt
Positioned(
bottom: 80.0, // Adjust as needed to position above AddContactButton
left: 0,
right: 0,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ListenReplayButton(audioHandler: _audioHandler),
const SizedBox(width: 20),
EncryptAudioButton(audioHandler: _audioHandler),
],
),
),
],
),
);
}
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,
),
),
);
}
}

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

View File

@ -1,6 +1,6 @@
import 'package:flutter/material.dart';
import 'package:flutter_contacts/flutter_contacts.dart';
import '../../../domain/services/contact_service.dart';
import '../../widgets/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');
} finally {
if (mounted) {
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');
} 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(() {});
});
}
}
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;

View File

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

View File

@ -0,0 +1,221 @@
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 '../../../widgets/color_darkener.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;
@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: TextStyle(
fontSize: 28,
fontWeight: FontWeight.bold,
color: Colors.white,
),
),
),
// Contact Entries
...contactsForLetter.map((contact) {
String phoneNumber = contact.phones.isNotEmpty
? contact.phones.first.number
: 'No phone number';
Color avatarColor =
generateColorFromName(contact.displayName);
return ListTile(
leading: (contact.thumbnail != null &&
contact.thumbnail!.isNotEmpty)
? CircleAvatar(
backgroundImage:
MemoryImage(contact.thumbnail!),
)
: CircleAvatar(
backgroundColor: avatarColor,
child: Text(
contact.displayName.isNotEmpty
? contact.displayName[0].toUpperCase()
: '?',
style: TextStyle(
color: darken(avatarColor, 0.4)),
),
),
title: Text(contact.displayName,
style: TextStyle(color: Colors.white)),
subtitle: Text(phoneNumber,
style: 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();
}
}

View File

@ -0,0 +1,341 @@
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 '../../../widgets/block_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;
@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 ${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('${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')),
);
}
}
}
@override
Widget build(BuildContext context) {
String email = widget.contact.emails.isNotEmpty
? 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();
}
},
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: [
CircleAvatar(
radius: 50,
backgroundImage: (widget.contact.thumbnail != null &&
widget.contact.thumbnail!.isNotEmpty)
? MemoryImage(widget.contact.thumbnail!)
: null,
backgroundColor:
generateColorFromName(widget.contact.displayName),
child: (widget.contact.thumbnail == null ||
widget.contact.thumbnail!.isEmpty)
? Text(
widget.contact.displayName.isNotEmpty
? widget.contact.displayName[0]
.toUpperCase()
: '?',
style: const TextStyle(
fontSize: 40, color: Colors.white),
)
: null,
),
const SizedBox(height: 10),
Text(
widget.contact.displayName,
style: const TextStyle(
fontSize: 24, fontWeight: FontWeight.bold),
),
],
),
),
const Divider(),
// Contact Actions
ListTile(
leading: const Icon(Icons.phone, color: Colors.green),
title: Text(phoneNumber),
onTap: () {
if (widget.contact.phones.isNotEmpty) {
_launchPhoneDialer(phoneNumber);
}
},
),
ListTile(
leading: const Icon(Icons.message, color: Colors.blue),
title: Text(phoneNumber),
onTap: () {
if (widget.contact.phones.isNotEmpty) {
_launchSms(phoneNumber);
}
},
),
ListTile(
leading: const Icon(Icons.email, color: Colors.orange),
title: Text(email),
onTap: () {
if (widget.contact.emails.isNotEmpty) {
_launchEmail(email);
}
},
),
const Divider(),
// Favorite, Edit, and Block/Unblock Buttons
Padding(
padding: const EdgeInsets.symmetric(horizontal: 16.0),
child: Column(
children: [
// Favorite button
SizedBox(
width: double
.infinity, // This makes the button take full width
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), // Space between buttons
// Edit button
SizedBox(
width: double
.infinity, // This makes the button take full width
child: ElevatedButton.icon(
onPressed: () => widget.onEdit(),
icon: const Icon(Icons.edit),
label: const Text('Edit Contact'),
),
),
const SizedBox(height: 10), // Space between buttons
// Block/Unblock button
SizedBox(
width: double
.infinity, // This makes the button take full width
child: ElevatedButton.icon(
onPressed: _toggleBlockState,
icon: Icon(
isBlocked ? Icons.block : Icons.block_flipped),
label: Text(isBlocked ? 'Unblock' : 'Block'),
),
),
],
),
),
const SizedBox(height: 16),
],
),
),
),
),
),
);
}
}

View File

@ -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:qr_flutter/qr_flutter.dart';
class QRCodeButton extends StatelessWidget {
final List<Contact> contacts;
@ -23,12 +23,31 @@ class QRCodeButton extends StatelessWidget {
@override
Widget build(BuildContext context) {
return IconButton(
icon: Icon(Icons.qr_code, color: selfContact != null ? Colors.blue : Colors.grey),
onPressed: selfContact != null
? () {
// Use the ContactService to show the QR code
ContactService().showContactQRCodeDialog(context, selfContact!);
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: selfContact!.toVCard(),
version: QrVersions.auto,
backgroundColor: Colors.white, // Ensure QR code is visible on black background
size: 200.0,
),
),
);
},
);
}
: null,
);

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

View File

@ -0,0 +1,439 @@
import 'package:flutter/material.dart';
import 'package:flutter_contacts/flutter_contacts.dart';
import 'package:intl/intl.dart';
import 'package:url_launcher/url_launcher.dart';
import 'package:dialer/features/contacts/contact_state.dart';
import 'package:dialer/widgets/username_color_generator.dart';
import 'package:dialer/widgets/color_darkener.dart';
class History {
final Contact contact;
final DateTime date;
final String callType; // 'incoming' or 'outgoing'
final String callStatus; // 'missed' or 'answered'
final int attempts;
History(
this.contact,
this.date,
this.callType,
this.callStatus,
this.attempts,
);
}
class HistoryPage extends StatefulWidget {
const HistoryPage({Key? key}) : super(key: key);
@override
_HistoryPageState createState() => _HistoryPageState();
}
class _HistoryPageState extends State<HistoryPage> with SingleTickerProviderStateMixin {
List<History> histories = [];
bool loading = true;
int? _expandedIndex;
@override
void didChangeDependencies() {
super.didChangeDependencies();
if (loading) {
_buildHistories();
}
}
Future<void> _buildHistories() async {
final contactState = ContactState.of(context);
if (contactState.loading) {
// Wait for contacts to be loaded
await Future.doWhile(() async {
await Future.delayed(const Duration(milliseconds: 100));
return contactState.loading;
});
}
List<Contact> contacts = contactState.contacts;
if (contacts.isEmpty) {
setState(() {
loading = false;
});
return;
}
setState(() {
histories = List.generate(
contacts.length >= 10 ? 10 : contacts.length,
(index) => History(
contacts[index],
DateTime.now().subtract(Duration(hours: (index + 1) * 2)),
index % 2 == 0 ? 'outgoing' : 'incoming',
index % 3 == 0 ? 'missed' : 'answered',
index % 3 + 1,
),
);
loading = false;
});
}
List _buildGroupedList(List<History> historyList) {
// Sort histories by date (most recent first)
historyList.sort((a, b) => b.date.compareTo(a.date));
final now = DateTime.now();
final today = DateTime(now.year, now.month, now.day);
final yesterday = today.subtract(const Duration(days: 1));
List<History> todayHistories = [];
List<History> yesterdayHistories = [];
List<History> olderHistories = [];
for (var history in historyList) {
final callDate = DateTime(history.date.year, history.date.month, history.date.day);
if (callDate == today) {
todayHistories.add(history);
} else if (callDate == yesterday) {
yesterdayHistories.add(history);
} else {
olderHistories.add(history);
}
}
// Combine them with headers
final items = <dynamic>[];
if (todayHistories.isNotEmpty) {
items.add('Today');
items.addAll(todayHistories);
}
if (yesterdayHistories.isNotEmpty) {
items.add('Yesterday');
items.addAll(yesterdayHistories);
}
if (olderHistories.isNotEmpty) {
items.add('Older');
items.addAll(olderHistories);
}
return items;
}
@override
Widget build(BuildContext context) {
final contactState = ContactState.of(context);
if (loading || contactState.loading) {
return Scaffold(
backgroundColor: Colors.black,
body: const Center(
child: CircularProgressIndicator(),
),
);
}
if (histories.isEmpty) {
return Scaffold(
backgroundColor: Colors.black,
body: const Center(
child: Text(
'No call history available.',
style: TextStyle(color: Colors.white),
),
),
);
}
// Filter missed calls
List<History> missedCalls = histories.where((h) => h.callStatus == 'missed').toList();
final allItems = _buildGroupedList(histories);
final missedItems = _buildGroupedList(missedCalls);
return DefaultTabController(
length: 2,
child: Scaffold(
backgroundColor: Colors.black,
appBar: PreferredSize(
preferredSize: const Size.fromHeight(kToolbarHeight),
child: Container(
color: Colors.black,
child: const TabBar(
tabs: [
Tab(text: 'All Calls'),
Tab(text: 'Missed Calls'),
],
indicatorColor: Colors.white,
),
),
),
body: TabBarView(
children: [
// All Calls
_buildListView(allItems),
// Missed Calls
_buildListView(missedItems),
],
),
),
);
}
Widget _buildListView(List items) {
return ListView.builder(
itemCount: items.length,
itemBuilder: (context, index) {
final item = items[index];
if (item is String) {
// This is a header item
return Container(
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16),
color: Colors.grey[900],
child: Text(
item,
style: const TextStyle(
color: Colors.white70,
fontWeight: FontWeight.bold,
),
),
);
} else if (item is History) {
final history = item;
final contact = history.contact;
final isExpanded = _expandedIndex == index;
// Generate the avatar color
Color avatarColor = generateColorFromName(contact.displayName);
return Column(
children: [
ListTile(
leading: (contact.thumbnail != null && contact.thumbnail!.isNotEmpty)
? CircleAvatar(
backgroundImage: MemoryImage(contact.thumbnail!),
)
: CircleAvatar(
backgroundColor: avatarColor,
child: Text(
contact.displayName.isNotEmpty
? contact.displayName[0].toUpperCase()
: '?',
style: TextStyle(color: darken(avatarColor, 0.4)),
),
),
title: Text(
contact.displayName,
style: const TextStyle(color: Colors.white),
),
subtitle: Text(
'${history.callType} - ${history.callStatus} - ${DateFormat('MMM dd, hh:mm a').format(history.date)}',
style: const TextStyle(color: Colors.grey),
),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
Text(
'${history.attempts}x',
style: const TextStyle(color: Colors.white),
),
IconButton(
icon: const Icon(Icons.phone, color: Colors.green),
onPressed: () async {
if (contact.phones.isNotEmpty) {
final Uri callUri =
Uri(scheme: 'tel', path: contact.phones.first.number);
if (await canLaunchUrl(callUri)) {
await launchUrl(callUri);
} else {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Could not launch call')),
);
}
} else {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Contact has no phone number')),
);
}
},
),
],
),
onTap: () {
setState(() {
_expandedIndex = isExpanded ? null : index;
});
},
),
if (isExpanded)
Container(
color: Colors.grey[850],
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
TextButton.icon(
onPressed: () async {
if (history.contact.phones.isNotEmpty) {
final Uri smsUri =
Uri(scheme: 'sms', path: history.contact.phones.first.number);
if (await canLaunchUrl(smsUri)) {
await launchUrl(smsUri);
} else {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Could not send message')),
);
}
} else {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Contact has no phone number')),
);
}
},
icon: const Icon(Icons.message, color: Colors.white),
label: const Text('Message', style: TextStyle(color: Colors.white)),
),
TextButton.icon(
onPressed: () {
// Navigate to Call Details page
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => CallDetailsPage(history: history),
),
);
},
icon: const Icon(Icons.info, color: Colors.white),
label: const Text('Details', style: TextStyle(color: Colors.white)),
),
TextButton.icon(
onPressed: () {
// Implement block number functionality
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Number blocked (functionality not implemented)'),
),
);
},
icon: const Icon(Icons.block, color: Colors.white),
label: const Text('Block', style: TextStyle(color: Colors.white)),
),
],
),
),
],
);
}
return const SizedBox.shrink();
},
);
}
}
class CallDetailsPage extends StatelessWidget {
final History history;
const CallDetailsPage({Key? key, required this.history}) : super(key: key);
@override
Widget build(BuildContext context) {
final contact = history.contact;
return Scaffold(
backgroundColor: Colors.black,
appBar: AppBar(
title: const Text('Call Details'),
backgroundColor: Colors.black,
),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
// Display Contact Name and Thumbnail
Row(
children: [
(contact.thumbnail != null && contact.thumbnail!.isNotEmpty)
? CircleAvatar(
backgroundImage: MemoryImage(contact.thumbnail!),
radius: 30,
)
: CircleAvatar(
backgroundColor: Colors.grey[700],
radius: 30,
child: Text(
contact.displayName.isNotEmpty
? contact.displayName[0].toUpperCase()
: '?',
style: const TextStyle(color: Colors.white),
),
),
const SizedBox(width: 16),
Expanded(
child: Text(
contact.displayName,
style: const TextStyle(color: Colors.white, fontSize: 24),
),
),
],
),
const SizedBox(height: 24),
// Display call type, status, date, attempts
DetailRow(
label: 'Call Type:',
value: history.callType,
),
DetailRow(
label: 'Call Status:',
value: history.callStatus,
),
DetailRow(
label: 'Date:',
value: DateFormat('MMM dd, yyyy - hh:mm a').format(history.date),
),
DetailRow(
label: 'Attempts:',
value: '${history.attempts}',
),
const SizedBox(height: 24),
// If you have more details like duration, contact number, etc.
if (contact.phones.isNotEmpty)
DetailRow(
label: 'Number:',
value: contact.phones.first.number,
),
],
),
),
);
}
}
class DetailRow extends StatelessWidget {
final String label;
final String value;
const DetailRow({Key? key, required this.label, required this.value}) : super(key: key);
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: Row(
children: [
Text(
label,
style: const TextStyle(color: Colors.white70, fontWeight: FontWeight.bold),
),
const SizedBox(width: 8),
Expanded(
child: Text(
value,
style: const TextStyle(color: Colors.white),
textAlign: TextAlign.right,
),
),
],
),
);
}
}

View File

@ -0,0 +1,235 @@
import 'package:flutter/material.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/features/settings/settings.dart';
import '../../widgets/contact_service.dart';
class _MyHomePageState extends State<MyHomePage>
with SingleTickerProviderStateMixin {
late TabController _tabController;
List<Contact> _allContacts = [];
List<Contact> _contactSuggestions = [];
final ContactService _contactService = ContactService();
@override
void initState() {
super.initState();
// Set the TabController length to 3
_tabController = TabController(length: 3, vsync: this, initialIndex: 1);
_tabController.addListener(_handleTabIndex);
_fetchContacts();
}
void _fetchContacts() async {
_allContacts = await _contactService.fetchContacts();
setState(() {});
}
void _onSearchChanged(String query) {
print("Search query: $query");
setState(() {
if (query.isEmpty) {
_contactSuggestions = List.from(_allContacts);
} else {
_contactSuggestions = _allContacts.where((contact) {
return contact.displayName
.toLowerCase()
.contains(query.toLowerCase());
}).toList();
}
});
}
@override
void dispose() {
_tabController.removeListener(_handleTabIndex);
_tabController.dispose();
super.dispose();
}
void _handleTabIndex() {
setState(() {});
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.black,
body: Column(
children: [
// Persistent Search Bar
Padding(
padding: const EdgeInsets.only(
top: 24.0,
bottom: 10.0,
left: 16.0,
right: 16.0,
),
child: Row(
children: [
Expanded(
child: Container(
decoration: BoxDecoration(
color: const Color.fromARGB(255, 30, 30, 30),
borderRadius: BorderRadius.circular(12.0),
border: Border(
top: BorderSide(color: Colors.grey.shade800, width: 1),
left: BorderSide(color: Colors.grey.shade800, width: 1),
right: BorderSide(color: Colors.grey.shade800, width: 1),
bottom:
BorderSide(color: Colors.grey.shade800, width: 2),
),
),
child: SearchAnchor(
builder:
(BuildContext context, SearchController controller) {
return SearchBar(
controller: controller,
padding:
MaterialStateProperty.all<EdgeInsetsGeometry>(
const EdgeInsets.only(
top: 6.0,
bottom: 6.0,
left: 16.0,
right: 16.0,
),
),
onTap: () {
controller.openView();
_onSearchChanged('');
},
backgroundColor: MaterialStateProperty.all(
const Color.fromARGB(255, 30, 30, 30)),
hintText: 'Search contacts',
hintStyle: MaterialStateProperty.all(
const TextStyle(color: Colors.grey, fontSize: 16.0),
),
leading: const Icon(
Icons.search,
color: Colors.grey,
size: 24.0,
),
shape:
MaterialStateProperty.all<RoundedRectangleBorder>(
RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12.0),
),
),
);
},
viewOnChanged: (query) {
_onSearchChanged(query);
},
suggestionsBuilder:
(BuildContext context, SearchController controller) {
return _contactSuggestions.map((contact) {
return ListTile(
key: ValueKey(contact.id),
title: Text(contact.displayName,
style: const TextStyle(color: Colors.white)),
onTap: () {
controller.closeView(contact.displayName);
},
);
}).toList();
},
),
),
),
// 3-dot menu
PopupMenuButton<String>(
icon: const Icon(Icons.more_vert, color: Colors.white),
itemBuilder: (BuildContext context) => [
const PopupMenuItem<String>(
value: 'settings',
child: Text('Settings'),
),
],
onSelected: (String value) {
if (value == 'settings') {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const SettingsPage()),
);
}
},
),
],
),
),
// Main content with TabBarView
Expanded(
child: Stack(
children: [
TabBarView(
controller: _tabController,
children: const [
FavoritesPage(),
HistoryPage(),
ContactPage(),
],
),
Positioned(
right: 20,
bottom: 20,
child: FloatingActionButton(
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const CompositionPage(),
),
);
},
backgroundColor: Colors.blue,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(45),
),
child: const Icon(Icons.dialpad, color: Colors.white),
),
),
],
),
),
],
),
bottomNavigationBar: Container(
color: Colors.black,
child: TabBar(
controller: _tabController,
tabs: [
Tab(
icon: Icon(_tabController.index == 0
? Icons.star
: Icons.star_border)),
Tab(
icon: Icon(_tabController.index == 1
? Icons.access_time_filled
: Icons.access_time_outlined)),
Tab(
icon: Icon(_tabController.index == 2
? Icons.contacts
: Icons.contacts_outlined)),
],
labelColor: Colors.white,
unselectedLabelColor: const Color.fromARGB(255, 158, 158, 158),
indicatorSize: TabBarIndicatorSize.label,
indicatorColor: Colors.white,
),
),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key});
@override
_MyHomePageState createState() => _MyHomePageState();
}

View File

@ -77,7 +77,7 @@ class _SettingsCallPageState extends State<SettingsCallPage> {
},
child: const Text('Beep'),
),
// ...existing options...
// Add more ringtone options
],
);
},

View File

@ -0,0 +1,65 @@
import 'package:flutter/material.dart';
import 'key_storage.dart';
class DeleteKeyPairPage extends StatelessWidget {
const DeleteKeyPairPage({super.key});
Future<void> _deleteKeyPair(BuildContext context) async {
final keyStorage = KeyStorage();
await keyStorage.deleteKeys();
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('The key pair has been deleted.'),
),
);
Navigator.pop(context);
}
void _showConfirmationDialog(BuildContext context) {
showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: const Text('Confirm Deletion'),
content: const Text(
'Are you sure you want to delete the key pair? This action is irreversible.'),
actions: [
TextButton(
child: const Text('Cancel'),
onPressed: () {
Navigator.of(context).pop();
},
),
TextButton(
child: const Text('Delete'),
onPressed: () {
Navigator.of(context).pop();
_deleteKeyPair(context);
},
),
],
);
},
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.black,
appBar: AppBar(
title: const Text('Delete a Key Pair'),
),
body: Center(
child: ElevatedButton(
onPressed: () {
_showConfirmationDialog(context);
},
child: const Text('Delete Key Pair'),
),
),
);
}
}

View File

@ -0,0 +1,138 @@
import 'package:flutter/material.dart';
import 'dart:typed_data';
import 'dart:convert';
import 'package:pointycastle/export.dart' as crypto;
import 'package:file_picker/file_picker.dart';
import 'dart:io';
import 'key_storage.dart';
class ExportPrivateKeyPage extends StatefulWidget {
const ExportPrivateKeyPage({super.key});
@override
_ExportPrivateKeyPageState createState() => _ExportPrivateKeyPageState();
}
class _ExportPrivateKeyPageState extends State<ExportPrivateKeyPage> {
final TextEditingController _passwordController = TextEditingController();
Future<void> _exportPrivateKey() async {
final keyStorage = KeyStorage();
final privateKeyPem = await keyStorage.getPrivateKey();
if (privateKeyPem == null) {
// Show error message if there's no key
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('No private key found to export.'),
),
);
return;
}
final password = _passwordController.text;
if (password.isEmpty) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Please enter a password.'),
),
);
return;
}
final encryptedData = _encryptPrivateKey(privateKeyPem, password);
final outputFile = await FilePicker.platform.saveFile(
dialogTitle: 'Save encrypted private key',
fileName: 'private_key_encrypted.aes',
);
if (outputFile != null) {
try {
final file = File(outputFile);
await file.writeAsBytes(encryptedData);
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('Key Exported'),
content: const Text('The encrypted private key has been exported successfully.'),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('OK'),
),
],
),
);
} catch (e) {
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('Failed to write file: $e'),
),
);
}
}
}
Uint8List _encryptPrivateKey(String privateKey, String password) {
// Derive a key from the password using PBKDF2
final derivator = crypto.PBKDF2KeyDerivator(
crypto.HMac(crypto.SHA256Digest(), 64),
);
final salt = Uint8List.fromList(utf8.encode('some_salt')); // In production, use a random salt and store it securely
derivator.init(crypto.Pbkdf2Parameters(salt, 1000, 32));
final key = derivator.process(Uint8List.fromList(utf8.encode(password)));
// Initialize AES-CBC cipher with PKCS7 padding
final iv = Uint8List(16); // zero IV for example, in production use random IV and store it
final params = crypto.PaddedBlockCipherParameters<crypto.ParametersWithIV<crypto.KeyParameter>, Null>(
crypto.ParametersWithIV<crypto.KeyParameter>(crypto.KeyParameter(key), iv),
null,
);
final cipher = crypto.PaddedBlockCipher('AES/CBC/PKCS7');
cipher.init(true, params);
final input = Uint8List.fromList(utf8.encode(privateKey));
final output = cipher.process(input);
return output;
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.black,
appBar: AppBar(
title: const Text('Export Private Key'),
),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
const Text(
'Enter a password to encrypt the private key:',
style: TextStyle(color: Colors.white),
),
TextField(
controller: _passwordController,
obscureText: true,
style: const TextStyle(color: Colors.white),
decoration: const InputDecoration(
hintText: 'Password',
hintStyle: TextStyle(color: Colors.grey),
),
),
const SizedBox(height: 20),
ElevatedButton(
onPressed: _exportPrivateKey,
child: const Text('Export Encrypted Private Key'),
),
],
),
),
);
}
}

View File

@ -0,0 +1,122 @@
import 'package:flutter/material.dart';
import 'package:pointycastle/export.dart' as crypto;
import 'dart:math';
import 'dart:convert';
import 'dart:typed_data';
import 'package:asn1lib/asn1lib.dart';
import 'key_storage.dart';
class GenerateNewKeyPairPage extends StatelessWidget {
const GenerateNewKeyPairPage({super.key});
Future<Map<String, String>> _generateKeyPair() async {
final keyParams = crypto.RSAKeyGeneratorParameters(
BigInt.parse('65537'),
2048,
64,
);
final secureRandom = crypto.FortunaRandom();
final random = Random.secure();
final seeds = List<int>.generate(32, (_) => random.nextInt(256));
secureRandom.seed(crypto.KeyParameter(Uint8List.fromList(seeds)));
final rngParams = crypto.ParametersWithRandom(keyParams, secureRandom);
final keyGenerator = crypto.RSAKeyGenerator();
keyGenerator.init(rngParams);
final pair = keyGenerator.generateKeyPair();
final publicKey = pair.publicKey as crypto.RSAPublicKey;
final privateKey = pair.privateKey as crypto.RSAPrivateKey;
final publicKeyPem = _encodePublicKeyToPemPKCS1(publicKey);
final privateKeyPem = _encodePrivateKeyToPemPKCS1(privateKey);
// Save keys securely
final keyStorage = KeyStorage();
await keyStorage.saveKeys(publicKey: publicKeyPem, privateKey: privateKeyPem);
return {'publicKey': publicKeyPem, 'privateKey': privateKeyPem};
}
String _encodePublicKeyToPemPKCS1(crypto.RSAPublicKey publicKey) {
final bytes = _encodePublicKeyToDer(publicKey);
return _formatPem(bytes, 'RSA PUBLIC KEY');
}
String _encodePrivateKeyToPemPKCS1(crypto.RSAPrivateKey privateKey) {
final bytes = _encodePrivateKeyToDer(privateKey);
return _formatPem(bytes, 'RSA PRIVATE KEY');
}
Uint8List _encodePublicKeyToDer(crypto.RSAPublicKey publicKey) {
final algorithmSeq = ASN1Sequence();
// Create the OID directly with the arcs
algorithmSeq.add(ASN1ObjectIdentifier([1, 2, 840, 113549, 1, 1, 1]));
algorithmSeq.add(ASN1Null());
final publicKeySeq = ASN1Sequence();
publicKeySeq.add(ASN1Integer(publicKey.modulus!));
publicKeySeq.add(ASN1Integer(publicKey.exponent!));
final publicKeyBitString = ASN1BitString(Uint8List.fromList(publicKeySeq.encodedBytes));
final topLevelSeq = ASN1Sequence();
topLevelSeq.add(algorithmSeq);
topLevelSeq.add(publicKeyBitString);
return Uint8List.fromList(topLevelSeq.encodedBytes);
}
Uint8List _encodePrivateKeyToDer(crypto.RSAPrivateKey privateKey) {
final privateKeySeq = ASN1Sequence();
privateKeySeq.add(ASN1Integer(BigInt.from(0))); // Version
privateKeySeq.add(ASN1Integer(privateKey.n!));
privateKeySeq.add(ASN1Integer(privateKey.exponent!));
privateKeySeq.add(ASN1Integer(privateKey.d!));
privateKeySeq.add(ASN1Integer(privateKey.p!));
privateKeySeq.add(ASN1Integer(privateKey.q!));
privateKeySeq.add(ASN1Integer(privateKey.d! % (privateKey.p! - BigInt.one)));
privateKeySeq.add(ASN1Integer(privateKey.d! % (privateKey.q! - BigInt.one)));
privateKeySeq.add(ASN1Integer(privateKey.q!.modInverse(privateKey.p!)));
return Uint8List.fromList(privateKeySeq.encodedBytes);
}
String _formatPem(Uint8List bytes, String label) {
final base64Data = base64Encode(bytes);
final chunks = RegExp('.{1,64}').allMatches(base64Data).map((m) => m.group(0)!);
return '-----BEGIN $label-----\n${chunks.join('\n')}\n-----END $label-----';
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.black,
appBar: AppBar(
title: const Text('Generate a New Key Pair'),
),
body: Center(
child: ElevatedButton(
onPressed: () async {
await _generateKeyPair();
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('Keys Generated'),
content: const Text('The new key pair has been generated and stored securely.'),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('OK'),
),
],
),
);
},
child: const Text('Generate a New Key Pair'),
),
),
);
}
}

View File

@ -0,0 +1,28 @@
// key_storage.dart
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
class KeyStorage {
static const _publicKeyKey = 'public_key';
static const _privateKeyKey = 'private_key';
final FlutterSecureStorage _storage = const FlutterSecureStorage();
Future<void> saveKeys({required String publicKey, required String privateKey}) async {
await _storage.write(key: _publicKeyKey, value: publicKey);
await _storage.write(key: _privateKeyKey, value: privateKey);
}
Future<String?> getPublicKey() async {
return await _storage.read(key: _publicKeyKey);
}
Future<String?> getPrivateKey() async {
return await _storage.read(key: _privateKeyKey);
}
Future<void> deleteKeys() async {
await _storage.delete(key: _publicKeyKey);
await _storage.delete(key: _privateKeyKey);
}
}

View File

@ -0,0 +1,80 @@
import 'package:flutter/material.dart';
import 'show_public_key_qr.dart';
import 'show_public_key_text.dart';
import 'generate_new_key_pair.dart';
import 'export_private_key.dart';
import 'delete_key_pair.dart';
class KeyManagementPage extends StatelessWidget {
const KeyManagementPage({super.key});
void _navigateToOption(BuildContext context, String option) {
switch (option) {
case 'Display public key as text':
Navigator.push(
context,
MaterialPageRoute(builder: (context) => const DisplayPublicKeyTextPage()),
);
break;
case 'Display public key as QR code':
Navigator.push(
context,
MaterialPageRoute(builder: (context) => const DisplayPublicKeyQRCodePage()),
);
break;
case 'Generate a new key pair':
Navigator.push(
context,
MaterialPageRoute(builder: (context) => const GenerateNewKeyPairPage()),
);
break;
case 'Export private key to password-encrypted file (AES 256)':
Navigator.push(
context,
MaterialPageRoute(builder: (context) => const ExportPrivateKeyPage()),
);
break;
case 'Delete a key pair':
Navigator.push(
context,
MaterialPageRoute(builder: (context) => const DeleteKeyPairPage()),
);
break;
default:
break;
}
}
@override
Widget build(BuildContext context) {
final keyManagementOptions = [
'Display public key as text',
'Display public key as QR code',
'Generate a new key pair',
'Export private key to password-encrypted file (AES 256)',
'Delete a key pair',
];
return Scaffold(
backgroundColor: Colors.black,
appBar: AppBar(
title: const Text('Key Management'),
),
body: ListView.builder(
itemCount: keyManagementOptions.length,
itemBuilder: (context, index) {
return ListTile(
title: Text(
keyManagementOptions[index],
style: const TextStyle(color: Colors.white),
),
trailing: const Icon(Icons.arrow_forward_ios, color: Colors.white),
onTap: () {
_navigateToOption(context, keyManagementOptions[index]);
},
);
},
),
);
}
}

View File

@ -0,0 +1,49 @@
import 'package:flutter/material.dart';
import 'package:pretty_qr_code/pretty_qr_code.dart';
import 'key_storage.dart';
class DisplayPublicKeyQRCodePage extends StatelessWidget {
const DisplayPublicKeyQRCodePage({super.key});
Future<String?> _loadPublicKey() async {
final keyStorage = KeyStorage();
return keyStorage.getPublicKey();
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.black,
appBar: AppBar(
title: const Text('Public Key in QR Code'),
),
body: FutureBuilder<String?>(
future: _loadPublicKey(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(child: CircularProgressIndicator());
}
final publicKey = snapshot.data;
if (publicKey == null) {
return const Center(
child: Text(
'No public key found.',
style: TextStyle(color: Colors.white),
),
);
}
return Center(
child: PrettyQr(
data: publicKey,
size: 250,
roundEdges: true,
elementColor: Colors.white,
),
);
},
),
);
}
}

View File

@ -0,0 +1,50 @@
import 'package:flutter/material.dart';
import 'key_storage.dart';
class DisplayPublicKeyTextPage extends StatelessWidget {
const DisplayPublicKeyTextPage({super.key});
Future<String?> _loadPublicKey() async {
final keyStorage = KeyStorage();
return await keyStorage.getPublicKey();
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.black,
appBar: AppBar(
title: const Text('Public Key as Text'),
),
body: FutureBuilder<String?>(
future: _loadPublicKey(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Center(child: CircularProgressIndicator());
}
final publicKey = snapshot.data;
if (publicKey == null) {
return const Center(
child: Text(
'No public key found.',
style: TextStyle(color: Colors.white),
),
);
}
return Center(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: SelectableText(
publicKey,
style: const TextStyle(color: Colors.white),
textAlign: TextAlign.center,
),
),
);
},
),
);
}
}

View File

@ -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/sim/settings_accounts.dart';
import 'package:dialer/features/settings/key/manage_keys_page.dart';
import 'package:dialer/features/settings/blocked/settings_blocked.dart';
class SettingsPage extends StatelessWidget {
const SettingsPage({super.key});
@ -14,10 +17,16 @@ class SettingsPage extends StatelessWidget {
MaterialPageRoute(builder: (context) => const SettingsCallPage()),
);
break;
case 'Sim settings':
Navigator.push(
context,
MaterialPageRoute(builder: (context) => const SettingsAccountsPage()),
);
break;
case 'Key management':
Navigator.push(
context,
MaterialPageRoute(builder: (context) => ManageKeysPage()),
MaterialPageRoute(builder: (context) => const KeyManagementPage()),
);
break;
case 'Blocked numbers':
@ -37,6 +46,7 @@ class SettingsPage extends StatelessWidget {
Widget build(BuildContext context) {
final settingsOptions = [
'Calling settings',
'Page of telephone accounts',
'Key management',
'Blocked numbers'
];

View File

@ -0,0 +1,91 @@
import 'package:flutter/material.dart';
import 'package:mobile_number/mobile_number.dart';
class ChooseSimPage extends StatefulWidget {
const ChooseSimPage({Key? key}) : super(key: key);
@override
_ChooseSimPageState createState() => _ChooseSimPageState();
}
class _ChooseSimPageState extends State<ChooseSimPage> {
List<SimCard> _simCards = [];
int? _selectedSimIndex;
@override
void initState() {
super.initState();
_fetchSimCards();
}
Future<void> _fetchSimCards() async {
try {
bool permissionsGranted = await MobileNumber.hasPhonePermission;
if (!permissionsGranted) {
await MobileNumber.requestPhonePermission;
permissionsGranted = await MobileNumber.hasPhonePermission;
}
if (permissionsGranted) {
List<SimCard>? simCards = await MobileNumber.getSimCards;
if (simCards != null && mounted) {
setState(() {
_simCards = simCards;
_selectedSimIndex = 0;
});
} else {
print('No SIM cards found');
}
} else {
print('Phone permission denied');
}
} catch (e) {
print('Failed to get SIM cards: $e');
}
}
void _onSimSelected(int? index) {
if (index != null) {
setState(() {
_selectedSimIndex = index;
});
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.black,
appBar: AppBar(
title: const Text('Choose SIM'),
),
body: _simCards.isEmpty
? const Center(
child: Text(
'No SIM cards found',
style: TextStyle(color: Colors.white),
),
)
: ListView.builder(
itemCount: _simCards.length,
itemBuilder: (context, index) {
final sim = _simCards[index];
return ListTile(
title: Text(
'SIM ${index + 1}',
style: const TextStyle(color: Colors.white),
),
subtitle: Text(
'Operator: ${sim.carrierName ?? 'N/A'}',
style: const TextStyle(color: Colors.grey),
),
trailing: Radio<int>(
value: index,
groupValue: _selectedSimIndex,
onChanged: _onSimSelected,
),
);
},
),
);
}
}

View File

@ -0,0 +1,57 @@
import 'package:flutter/material.dart';
import 'choose_sim.dart';
import 'sim_parameters.dart';
class SettingsAccountsPage extends StatelessWidget {
const SettingsAccountsPage({Key? key}) : super(key: key);
void _navigateToAccountOption(BuildContext context, String option) {
switch (option) {
case 'Chose SIM card':
Navigator.push(
context,
MaterialPageRoute(builder: (context) => const ChooseSimPage()),
);
break;
case 'SIM card parameters':
Navigator.push(
context,
MaterialPageRoute(builder: (context) => const SimParametersPage()),
);
break;
default:
break;
}
}
@override
Widget build(BuildContext context) {
final accountOptions = [
'Chose SIM card',
'SIM card parameters',
];
return Scaffold(
backgroundColor: Colors.black,
appBar: AppBar(
title: const Text('Sim settings'),
),
body: ListView.builder(
itemCount: accountOptions.length,
itemBuilder: (context, index) {
return ListTile(
title: Text(
accountOptions[index],
style: const TextStyle(color: Colors.white),
),
trailing:
const Icon(Icons.arrow_forward_ios, color: Colors.white),
onTap: () {
_navigateToAccountOption(context, accountOptions[index]);
},
);
},
),
);
}
}

View File

@ -0,0 +1,89 @@
import 'package:flutter/material.dart';
import 'package:mobile_number/mobile_number.dart';
class SimParametersPage extends StatefulWidget {
const SimParametersPage({Key? key}) : super(key: key);
@override
_SimParametersPageState createState() => _SimParametersPageState();
}
class _SimParametersPageState extends State<SimParametersPage> {
List<SimCard> _simCards = [];
@override
void initState() {
super.initState();
_fetchSimParameters();
}
Future<void> _fetchSimParameters() async {
try {
bool permissionsGranted = await MobileNumber.hasPhonePermission;
if (!permissionsGranted) {
await MobileNumber.requestPhonePermission;
// Check again if permission is granted
permissionsGranted = await MobileNumber.hasPhonePermission;
}
if (permissionsGranted) {
List<SimCard>? simCards = await MobileNumber.getSimCards;
if (simCards != null && mounted) {
setState(() {
_simCards = simCards;
});
} else {
print('No SIM cards found');
}
} else {
print('Phone permission denied');
}
} catch (e) {
print('Failed to get SIM cards: $e');
}
}
Widget _buildSimInfo(SimCard sim, int index) {
return Card(
color: Colors.grey[850],
child: ListTile(
title: Text(
'SIM ${index + 1}',
style: const TextStyle(color: Colors.white),
),
subtitle: Text(
'''
Carrier Name: ${sim.carrierName ?? 'N/A'}
Country Iso: ${sim.countryIso ?? 'N/A'}
Display Name: ${sim.displayName ?? 'N/A'}
Slot Index: ${sim.slotIndex ?? 'N/A'}
Number: ${sim.number ?? 'N/A'}
''',
style: const TextStyle(color: Colors.grey),
),
),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.black,
appBar: AppBar(
title: const Text('SIM Parameters'),
),
body: _simCards.isEmpty
? const Center(
child: Text(
'No SIM cards found',
style: TextStyle(color: Colors.white),
),
)
: ListView.builder(
itemCount: _simCards.length,
itemBuilder: (context, index) {
return _buildSimInfo(_simCards[index], index);
},
),
);
}
}

View File

@ -1,7 +0,0 @@
// 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;

View File

@ -1,105 +1,23 @@
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:provider/provider.dart';
import 'package:dialer/features/contacts/contact_state.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();
// 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();
runApp(
MultiProvider(
providers: [
Provider<AsymmetricCryptoService>(
create: (_) => cryptoService,
),
],
child: const DialerApp(),
),
);
void main() {
runApp(const MyApp());
}
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 MyApp extends StatelessWidget {
const MyApp({super.key});
@override
Widget build(BuildContext context) {
return ContactState(
child: MaterialApp(
title: 'Dialer App',
navigatorKey: CallService.navigatorKey,
theme: ThemeData(
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()),
},
brightness: Brightness.dark
),
home: SafeArea(child: MyHomePage()),
)
);
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,672 +0,0 @@
import 'dart:async';
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 '../contacts/widgets/contact_modal.dart';
class History {
final Contact contact;
final DateTime date;
final String callType; // 'incoming' or 'outgoing'
final String callStatus; // 'missed' or 'answered'
final int attempts;
History(
this.contact,
this.date,
this.callType,
this.callStatus,
this.attempts,
);
}
class HistoryPage extends StatefulWidget {
const HistoryPage({Key? key}) : super(key: key);
@override
_HistoryPageState createState() => _HistoryPageState();
}
class _HistoryPageState extends State<HistoryPage>
with AutomaticKeepAliveClientMixin, SingleTickerProviderStateMixin {
List<History> histories = [];
bool loading = true;
int? _expandedIndex;
final ObfuscateService _obfuscateService = ObfuscateService();
final CallService _callService = CallService();
// 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) {
_buildHistories();
}
}
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);
}
// Check if still mounted before accessing context
if (mounted) {
await _refreshContacts();
}
} else {
debugPrint("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')));
}
}
}
/// Helper: Remove all non-digit characters for simple matching.
String sanitizeNumber(String number) {
return number.replaceAll(RegExp(r'\D'), '');
}
/// Helper: Find a contact from our list by matching phone numbers.
Contact? findContactForNumber(String number, List<Contact> contacts) {
final sanitized = sanitizeNumber(number);
for (var contact in contacts) {
for (var phone in contact.phones) {
if (sanitizeNumber(phone.number) == sanitized) {
return contact;
}
}
}
return null;
}
/// Request permission for reading call logs.
Future<bool> _requestCallLogPermission() async {
var status = await Permission.phone.status;
if (!status.isGranted) {
status = await Permission.phone.request();
}
return status.isGranted;
}
/// Build histories from the native call log using the method channel.
Future<void> _buildHistories() async {
// Request permission.
bool hasPermission = await _requestCallLogPermission();
if (!hasPermission) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('Call log permission not granted')));
setState(() {
loading = false;
});
return;
}
// Retrieve call logs from native code.
List<dynamic> nativeLogs = [];
try {
nativeLogs = await _channel.invokeMethod('getCallLogs');
} on PlatformException catch (e) {
print("Error fetching call logs: ${e.message}");
}
// Ensure contacts are loaded.
final contactState = ContactState.of(context);
if (contactState.loading) {
await Future.doWhile(() async {
await Future.delayed(const Duration(milliseconds: 100));
return contactState.loading;
});
}
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];
final String number = entry['number'] ?? '';
if (number.isEmpty) continue;
// Convert timestamp to DateTime.
DateTime callDate =
DateTime.fromMillisecondsSinceEpoch(entry['date'] ?? 0);
int typeInt = entry['type'] ?? 0;
int duration = entry['duration'] ?? 0;
String callType;
String callStatus;
// Map integer values to call type/status.
// Commonly: 1 = incoming, 2 = outgoing, 3 = missed.
switch (typeInt) {
case 1:
callType = "incoming";
callStatus = (duration == 0) ? "missed" : "answered";
break;
case 2:
callType = "outgoing";
callStatus = "answered";
break;
case 3:
callType = "incoming";
callStatus = "missed";
break;
default:
callType = "unknown";
callStatus = "unknown";
}
// Try to find a matching contact.
Contact? matchedContact = findContactForNumber(number, contacts);
if (matchedContact == null) {
// Create a dummy contact if not found.
matchedContact = Contact(
id: "dummy-$number",
displayName: number,
phones: [Phone(number)],
);
}
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.
callHistories.sort((a, b) => b.date.compareTo(a.date));
setState(() {
histories = callHistories;
loading = false;
});
}
List _buildGroupedList(List<History> historyList) {
historyList.sort((a, b) => b.date.compareTo(a.date));
final now = DateTime.now();
final today = DateTime(now.year, now.month, now.day);
final yesterday = today.subtract(const Duration(days: 1));
List<History> todayHistories = [];
List<History> yesterdayHistories = [];
List<History> olderHistories = [];
for (var history in historyList) {
final callDate =
DateTime(history.date.year, history.date.month, history.date.day);
if (callDate == today) {
todayHistories.add(history);
} else if (callDate == yesterday) {
yesterdayHistories.add(history);
} else {
olderHistories.add(history);
}
}
final items = <dynamic>[];
if (todayHistories.isNotEmpty) {
items.add('Today');
items.addAll(todayHistories);
}
if (yesterdayHistories.isNotEmpty) {
items.add('Yesterday');
items.addAll(yesterdayHistories);
}
if (olderHistories.isNotEmpty) {
items.add('Older');
items.addAll(olderHistories);
}
return items;
}
/// Returns an icon reflecting call type and status.
Icon _getCallIcon(History history) {
IconData iconData;
Color iconColor;
if (history.callType == 'incoming') {
if (history.callStatus == 'missed') {
iconData = Icons.call_missed;
iconColor = Colors.red;
} else {
iconData = Icons.call_received;
iconColor = Colors.green;
}
} else if (history.callType == 'outgoing') {
iconData = Icons.call_made;
iconColor = Colors.green;
} else {
iconData = Icons.phone;
iconColor = Colors.white;
}
return Icon(iconData, color: iconColor);
}
@override
Widget build(BuildContext context) {
super.build(context); // required due to AutomaticKeepAliveClientMixin
final contactState = ContactState.of(context);
if (loading || contactState.loading) {
return Scaffold(
backgroundColor: Colors.black,
body: const Center(child: CircularProgressIndicator()),
);
}
if (histories.isEmpty) {
return Scaffold(
backgroundColor: Colors.black,
body: const Center(
child: Text(
'No call history available.',
style: TextStyle(color: Colors.white),
),
),
);
}
List<History> missedCalls =
histories.where((h) => h.callStatus == 'missed').toList();
final allItems = _buildGroupedList(histories);
final missedItems = _buildGroupedList(missedCalls);
return DefaultTabController(
length: 2,
child: Scaffold(
backgroundColor: Colors.black,
appBar: PreferredSize(
preferredSize: const Size.fromHeight(kToolbarHeight),
child: Container(
color: Colors.black,
child: const TabBar(
tabs: [
Tab(text: 'All Calls'),
Tab(text: 'Missed Calls'),
],
indicatorColor: Colors.white,
),
),
),
body: TabBarView(
children: [
_buildListView(allItems),
_buildListView(missedItems),
],
),
),
);
}
Widget _buildListView(List items) {
return ListView.builder(
itemCount: items.length,
itemBuilder: (context, index) {
final item = items[index];
if (item is String) {
return Container(
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16),
color: Colors.grey[900],
child: Text(
item,
style: const TextStyle(
color: Colors.white70,
fontWeight: FontWeight.bold,
),
),
);
} else if (item is History) {
final history = item;
final contact = history.contact;
final isExpanded = _expandedIndex == index;
Color avatarColor = generateColorFromName(contact.displayName);
return Column(
children: [
ListTile(
leading: GestureDetector(
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,
);
},
);
},
child: 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(
DateFormat('MMM dd, hh:mm a').format(history.date),
style: const TextStyle(color: Colors.grey),
),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
_getCallIcon(history),
const SizedBox(width: 8),
Text(
'${history.attempts}x',
style: const TextStyle(color: Colors.white),
),
IconButton(
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,
);
} else {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('Contact has no phone number')),
);
}
},
),
],
),
onTap: () {
setState(() {
_expandedIndex = isExpanded ? null : index;
});
},
),
if (isExpanded)
Container(
color: Colors.grey[850],
child: FutureBuilder<bool>(
future: BlockService().isNumberBlocked(
contact.phones.isNotEmpty
? contact.phones.first.number
: ''),
builder: (context, snapshot) {
final isBlocked = snapshot.data ?? false;
return Row(
mainAxisAlignment: MainAxisAlignment.spaceAround,
children: [
TextButton.icon(
onPressed: () async {
if (history.contact.phones.isNotEmpty) {
final Uri smsUri = Uri(
scheme: 'sms',
path: history.contact.phones.first.number);
if (await canLaunchUrl(smsUri)) {
await launchUrl(smsUri);
} else {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content:
Text('Could not send message')),
);
}
} else {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content:
Text('Contact has no phone number')),
);
}
},
icon:
const Icon(Icons.message, color: Colors.white),
label: const Text('Message',
style: TextStyle(color: Colors.white)),
),
TextButton.icon(
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (_) =>
CallDetailsPage(history: history),
),
);
},
icon: const Icon(Icons.info, color: Colors.white),
label: const Text('Details',
style: TextStyle(color: Colors.white)),
),
TextButton.icon(
onPressed: () async {
final phoneNumber = contact.phones.isNotEmpty
? contact.phones.first.number
: null;
if (phoneNumber == null) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content:
Text('Contact has no phone number')),
);
return;
}
if (isBlocked) {
await BlockService().unblockNumber(phoneNumber);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('$phoneNumber unblocked')),
);
} else {
await BlockService().blockNumber(phoneNumber);
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(
content: Text('$phoneNumber blocked')),
);
}
setState(() {});
},
icon: Icon(
isBlocked ? Icons.lock_open : Icons.block,
color: Colors.white),
label: Text(isBlocked ? 'Unblock' : 'Block',
style: const TextStyle(color: Colors.white)),
),
],
);
},
),
),
],
);
}
return const SizedBox.shrink();
},
);
}
}
class CallDetailsPage extends StatelessWidget {
final History history;
final ObfuscateService _obfuscateService = ObfuscateService();
CallDetailsPage({super.key, required this.history});
@override
Widget build(BuildContext context) {
final contact = history.contact;
final contactBg = generateColorFromName(contact.displayName);
final contactLetter = darken(contactBg);
return Scaffold(
backgroundColor: Colors.black,
appBar: AppBar(
title: const Text('Call Details'),
backgroundColor: Colors.black,
),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
// Display Contact Name and Thumbnail.
Row(
children: [
(contact.thumbnail != null && contact.thumbnail!.isNotEmpty)
? ObfuscatedAvatar(
imageBytes: contact.thumbnail,
radius: 30,
backgroundColor: contactBg,
fallbackInitial: contact.displayName,
)
: CircleAvatar(
backgroundColor:
generateColorFromName(contact.displayName),
radius: 30,
child: Text(
contact.displayName.isNotEmpty
? contact.displayName[0].toUpperCase()
: '?',
style: TextStyle(color: contactLetter),
),
),
const SizedBox(width: 16),
Expanded(
child: Text(
_obfuscateService.obfuscateData(contact.displayName),
style: const TextStyle(color: Colors.white, fontSize: 24),
),
),
],
),
const SizedBox(height: 24),
// Display call details.
DetailRow(
label: 'Call Type:',
value: history.callType,
),
DetailRow(
label: 'Call Status:',
value: history.callStatus,
),
DetailRow(
label: 'Date:',
value: DateFormat('MMM dd, yyyy - hh:mm a').format(history.date),
),
DetailRow(
label: 'Attempts:',
value: '${history.attempts}',
),
const SizedBox(height: 24),
if (contact.phones.isNotEmpty)
DetailRow(
label: 'Number:',
value: _obfuscateService
.obfuscateData(contact.phones.first.number),
),
],
),
),
);
}
}
class DetailRow extends StatelessWidget {
final String label;
final String value;
const DetailRow({Key? key, required this.label, required this.value})
: super(key: key);
@override
Widget build(BuildContext context) {
return Padding(
padding: const EdgeInsets.symmetric(vertical: 8.0),
child: Row(
children: [
Text(
label,
style: const TextStyle(
color: Colors.white70, fontWeight: FontWeight.bold),
),
const SizedBox(width: 8),
Expanded(
child: Text(
value,
style: const TextStyle(color: Colors.white),
textAlign: TextAlign.right,
),
),
],
),
);
}
}

View File

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

View File

@ -1,342 +0,0 @@
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:flutter_contacts/flutter_contacts.dart';
import 'package:dialer/domain/services/contact_service.dart';
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);
_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(() {});
}
void _clearSearch() {
_searchController.clear();
_searchBarController.clear();
_rawSearchInput = '';
_onSearchChanged('');
}
void _onSearchChanged(String query) {
setState(() {
if (query.isEmpty) {
_contactSuggestions = List.from(_allContacts);
} else {
final normalizedQuery = _normalizeString(query.toLowerCase());
_contactSuggestions = _allContacts.where((contact) {
final normalizedName = _normalizeString(contact.displayName.toLowerCase());
return normalizedName.contains(normalizedQuery);
}).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();
}
void _handleTabIndex() {
setState(() {});
}
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);
_fetchContacts();
}
} 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) {
return Scaffold(
backgroundColor: Colors.black,
body: Column(
children: [
Padding(
padding: const EdgeInsets.only(
top: 24.0,
bottom: 10.0,
left: 16.0,
right: 16.0,
),
child: Row(
children: [
Expanded(
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),
),
child: SearchAnchor(
searchController: _searchBarController,
builder: (BuildContext context, SearchController controller) {
return GestureDetector(
onTap: () {
controller.openView();
},
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),
),
padding: const EdgeInsets.symmetric(vertical: 12.0, horizontal: 16.0),
child: Row(
children: [
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,
),
),
if (_rawSearchInput.isNotEmpty)
GestureDetector(
onTap: _clearSearch,
child: const Icon(
Icons.clear,
color: Colors.grey,
size: 24.0,
),
),
],
),
),
);
},
viewOnChanged: (query) {
if (_searchBarController.text != query) {
_rawSearchInput = query;
_searchBarController.text = query;
_searchController.text = query;
}
_onSearchChanged(query);
},
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),
),
onTap: () {
controller.closeView(contact.displayName);
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) {
_fetchContacts();
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,
);
},
);
},
);
}).toList();
},
),
),
),
PopupMenuButton<String>(
icon: const Icon(Icons.more_vert, color: Colors.white),
itemBuilder: (BuildContext context) => [
const PopupMenuItem<String>(
value: 'settings',
child: Text('Settings'),
),
],
onSelected: (String value) {
if (value == 'settings') {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => const SettingsPage()),
);
}
},
),
],
),
),
Expanded(
child: Stack(
children: [
TabBarView(
controller: _tabController,
children: const [
FavoritesPage(),
HistoryPage(),
ContactPage(),
VoicemailPage(),
],
),
Positioned(
right: 20,
bottom: 20,
child: FloatingActionButton(
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const CompositionPage(),
),
);
},
backgroundColor: Colors.blue,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(45),
),
child: const Icon(Icons.dialpad, color: Colors.white),
),
),
],
),
),
],
),
bottomNavigationBar: Container(
color: Colors.black,
child: TabBar(
controller: _tabController,
tabs: [
Tab(
icon: Icon(_tabController.index == 0
? Icons.star
: Icons.star_border)),
Tab(
icon: Icon(_tabController.index == 1
? Icons.access_time_filled
: Icons.access_time_outlined)),
Tab(
icon: Icon(_tabController.index == 2
? Icons.contacts
: Icons.contacts_outlined)),
Tab(
icon: Icon(_tabController.index == 3
? Icons.voicemail
: Icons.voicemail_outlined),
),
],
labelColor: Colors.white,
unselectedLabelColor: const Color.fromARGB(255, 158, 158, 158),
indicatorSize: TabBarIndicatorSize.label,
indicatorColor: Colors.white,
),
),
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key});
@override
_MyHomePageState createState() => _MyHomePageState();
}

View File

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

View File

@ -1,145 +0,0 @@
import 'package:flutter/material.dart';
import 'package:dialer/domain/services/cryptography/asymmetric_crypto_service.dart';
class ManageKeysPage extends StatefulWidget {
const ManageKeysPage({Key? key}) : super(key: key);
@override
_ManageKeysPageState createState() => _ManageKeysPageState();
}
class _ManageKeysPageState extends State<ManageKeysPage> {
final AsymmetricCryptoService _cryptoService = AsymmetricCryptoService();
List<Map<String, dynamic>> _keys = [];
bool _isLoading = false;
@override
void initState() {
super.initState();
_loadKeys();
}
Future<void> _loadKeys() async {
setState(() {
_isLoading = true;
});
try {
List<Map<String, dynamic>> keys = await _cryptoService.getAllKeys();
setState(() {
_keys = keys;
});
} catch (e) {
ScaffoldMessenger.of(context)
.showSnackBar(SnackBar(content: Text('Error loading keys: $e')));
} finally {
setState(() {
_isLoading = false;
});
}
}
Future<void> _generateKey() async {
setState(() {
_isLoading = true;
});
try {
await _cryptoService.generateKeyPair();
await _loadKeys();
ScaffoldMessenger.of(context)
.showSnackBar(const SnackBar(content: Text('Key generated successfully')));
} catch (e) {
ScaffoldMessenger.of(context)
.showSnackBar(SnackBar(content: Text('Error generating key: $e')));
} finally {
setState(() {
_isLoading = false;
});
}
}
Future<void> _deleteKey(String alias) async {
setState(() {
_isLoading = true;
});
try {
await _cryptoService.deleteKeyPair(alias);
await _loadKeys();
ScaffoldMessenger.of(context)
.showSnackBar(const SnackBar(content: Text('Key deleted successfully')));
} catch (e) {
ScaffoldMessenger.of(context)
.showSnackBar(SnackBar(content: Text('Error deleting key: $e')));
} finally {
setState(() {
_isLoading = false;
});
}
}
Future<void> _viewPublicKey(String alias) async {
try {
final publicKey = await _cryptoService.getPublicKey(alias);
showDialog(
context: context,
builder: (_) => AlertDialog(
title: const Text('Public Key'),
content: SingleChildScrollView(child: Text(publicKey)),
actions: [
TextButton(
child: const Text('Close'),
onPressed: () {
Navigator.pop(context);
},
)
],
),
);
} catch (e) {
ScaffoldMessenger.of(context)
.showSnackBar(SnackBar(content: Text('Error retrieving public key: $e')));
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('Manage Keys'),
),
body: _isLoading
? const Center(child: CircularProgressIndicator())
: _keys.isEmpty
? const Center(child: Text('No keys found'))
: ListView.builder(
itemCount: _keys.length,
itemBuilder: (context, index) {
final keyData = _keys[index];
return ListTile(
title: Text(keyData['label'] ?? 'No label'),
subtitle: Text(keyData['alias'] ?? ''),
trailing: Row(
mainAxisSize: MainAxisSize.min,
children: [
IconButton(
icon: const Icon(Icons.visibility),
tooltip: 'View Public Key',
onPressed: () => _viewPublicKey(keyData['alias']),
),
IconButton(
icon: const Icon(Icons.delete),
tooltip: 'Delete Key',
onPressed: () => _deleteKey(keyData['alias']),
),
],
),
);
},
),
floatingActionButton: FloatingActionButton(
onPressed: _generateKey,
child: const Icon(Icons.add),
tooltip: 'Generate New Key',
),
);
}
}

View File

@ -1,210 +0,0 @@
import 'package:flutter/material.dart';
import 'package:audioplayers/audioplayers.dart';
class VoicemailPage extends StatefulWidget {
const VoicemailPage({super.key});
@override
State<VoicemailPage> createState() => _VoicemailPageState();
}
class _VoicemailPageState extends State<VoicemailPage> {
bool _expanded = false;
bool _isPlaying = false;
Duration _duration = Duration.zero;
Duration _position = Duration.zero;
late AudioPlayer _audioPlayer;
bool _loading = false;
@override
void initState() {
super.initState();
_audioPlayer = AudioPlayer();
_audioPlayer.onDurationChanged.listen((Duration d) {
setState(() => _duration = d);
});
_audioPlayer.onPositionChanged.listen((Duration p) {
setState(() => _position = p);
});
_audioPlayer.onPlayerComplete.listen((event) {
setState(() {
_isPlaying = false;
_position = Duration.zero;
});
});
}
Future<void> _togglePlayPause() async {
if (_isPlaying) {
await _audioPlayer.pause();
} else {
await _audioPlayer.play(UrlSource('voicemail.mp3'));
}
setState(() => _isPlaying = !_isPlaying);
}
@override
void dispose() {
_audioPlayer.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
if (_loading) {
return const Center(child: CircularProgressIndicator());
}
return Scaffold(
backgroundColor: Colors.black,
body: ListView(
children: [
GestureDetector(
onTap: () {
setState(() {
_expanded = !_expanded;
});
},
child: AnimatedContainer(
duration: const Duration(milliseconds: 300),
margin: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: const Color.fromARGB(255, 30, 30, 30),
borderRadius: BorderRadius.circular(12.0),
),
padding: const EdgeInsets.all(16),
child: _expanded
? Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: [
const CircleAvatar(
radius: 28,
backgroundColor: Colors.amber,
child: Text(
"JD",
style: TextStyle(
color: Colors.deepOrange,
fontSize: 28,
),
),
),
const SizedBox(width: 12),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: const [
Text(
'John Doe',
style: TextStyle(color: Colors.white),
),
Text(
'Wed 3:00 PM - 1:20 min',
style: TextStyle(color: Colors.grey),
),
],
),
],
),
const SizedBox(height: 16),
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
IconButton(
icon: Icon(
_isPlaying ? Icons.pause : Icons.play_arrow,
color: Colors.white,
),
onPressed: _togglePlayPause,
),
SizedBox(
width: 200,
child: Slider(
min: 0,
max: _duration.inSeconds.toDouble(),
value: _position.inSeconds.toDouble(),
onChanged: (value) async {
final newPos = Duration(seconds: value.toInt());
await _audioPlayer.seek(newPos);
},
activeColor: Colors.blue,
inactiveColor: Colors.grey,
),
),
],
),
const SizedBox(height: 16),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Row(
children: const [
Icon(Icons.call, color: Colors.green),
SizedBox(width: 8),
Text('Call', style: TextStyle(color: Colors.white)),
],
),
const SizedBox(height: 12),
Row(
children: const [
Icon(Icons.message, color: Colors.blue),
SizedBox(width: 8),
Text('Text', style: TextStyle(color: Colors.white)),
],
),
const SizedBox(height: 12),
Row(
children: const [
Icon(Icons.block, color: Colors.red),
SizedBox(width: 8),
Text('Block', style: TextStyle(color: Colors.white)),
],
),
const SizedBox(height: 12),
Row(
children: const [
Icon(Icons.share, color: Colors.white),
SizedBox(width: 8),
Text('Share', style: TextStyle(color: Colors.white)),
],
),
],
),
],
)
: Row(
children: [
const CircleAvatar(
radius: 28,
backgroundColor: Colors.amber,
child: Text(
"JD",
style: TextStyle(
color: Colors.deepOrange,
fontSize: 28,
),
),
),
const SizedBox(width: 12),
Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: const [
Text(
'John Doe',
style: TextStyle(color: Colors.white),
),
Text(
'Wed 3:00 PM - 1:20 min',
style: TextStyle(color: Colors.grey),
),
],
),
],
),
),
),
],
),
);
}
}

View File

@ -0,0 +1,111 @@
// lib/services/audio_handler.dart
import 'dart:async';
import 'dart:typed_data';
import 'package:flutter_audio_capture/flutter_audio_capture.dart';
import 'package:encrypt/encrypt.dart' as encrypt;
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
class AudioHandler {
final FlutterAudioCapture _audioCapture = FlutterAudioCapture();
final FlutterSecureStorage _secureStorage = FlutterSecureStorage();
StreamController<Uint8List> _audioStreamController = StreamController.broadcast();
Timer? _delayTimer;
final int delaySeconds = 3;
// Encryption variables
encrypt.Encrypter? _encrypter;
encrypt.Key? _key;
bool _isEncryptionEnabled = false;
// Buffer for delay
List<Uint8List> _buffer = [];
// Constructor
AudioHandler();
// Start listening to the microphone
Future<void> startListening() async {
await _audioCapture.start(_onAudioData, _onError, sampleRate: 44100, bufferSize: 3000);
// Initialize the delay timer
_delayTimer = Timer.periodic(Duration(seconds: delaySeconds), (_) {
if (_buffer.isNotEmpty) {
// Replay the audio
Uint8List replayData = _buffer.removeAt(0);
_audioStreamController.add(replayData);
}
});
}
// Stop listening
Future<void> stopListening() async {
await _audioCapture.stop();
_delayTimer?.cancel();
_buffer.clear();
}
// Stream for UI to listen and replay audio
Stream<Uint8List> get audioStream => _audioStreamController.stream;
// Callback for audio data
void _onAudioData(dynamic obj) async {
Uint8List data = Uint8List.fromList(obj.cast<int>());
// Encrypt the data if encryption is enabled
if (_isEncryptionEnabled && _encrypter != null) {
final iv = encrypt.IV.fromSecureRandom(12); // Use a proper IV in production
final encrypted = _encrypter!.encryptBytes(data, iv: iv);
data = encrypted.bytes;
}
// Add data to buffer for delayed replay
_buffer.add(data);
// Optionally, you can handle encrypted data here
// For example, send it over the network or save to a file
}
// Error handling
void _onError(Object e) {
print('Audio Capture Error: $e');
}
// Enable encryption
Future<void> enableEncryption() async {
if (_isEncryptionEnabled) return;
// Check if key exists
String? storedKey = await _secureStorage.read(key: 'encryption_key');
if (storedKey == null) {
// Generate a new key
final newKey = _generateSecureKey();
await _secureStorage.write(key: 'encryption_key', value: newKey);
_key = encrypt.Key.fromUtf8(newKey);
} else {
_key = encrypt.Key.fromUtf8(storedKey);
}
_encrypter = encrypt.Encrypter(
encrypt.AES(
_key!,
mode: encrypt.AESMode.gcm,
padding: null,
),
);
_isEncryptionEnabled = true;
}
// Disable encryption
void disableEncryption() {
_encrypter = null;
_isEncryptionEnabled = false;
}
// Generate a secure key
String _generateSecureKey() {
// Implement a secure random key generator
// For demonstration, using a fixed key (DO NOT use in production)
return 'my32lengthsupersecretnooneknows1'; // 32 chars for AES-256
}
}

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

View 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();
}

View File

@ -0,0 +1,31 @@
import 'package:flutter_contacts/flutter_contacts.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 {
// Fetch all contacts
List<Contact> contacts = await fetchContacts();
// Filter contacts to only include those with isStarred: true
List<Contact> favoriteContacts =
contacts.where((contact) => contact.isStarred).toList();
return favoriteContacts;
}
Future<void> addNewContact(Contact contact) async {
await FlutterContacts.insertContact(contact);
}
}

View File

@ -0,0 +1,40 @@
// lib/widgets/encrypt_audio_button.dart
import 'package:flutter/material.dart';
import '../services/audio_handler.dart';
class EncryptAudioButton extends StatefulWidget {
final AudioHandler audioHandler;
const EncryptAudioButton({Key? key, required this.audioHandler}) : super(key: key);
@override
_EncryptAudioButtonState createState() => _EncryptAudioButtonState();
}
class _EncryptAudioButtonState extends State<EncryptAudioButton> {
bool _isEncrypted = false;
void _toggleEncryption() {
setState(() {
_isEncrypted = !_isEncrypted;
if (_isEncrypted) {
widget.audioHandler.enableEncryption();
} else {
widget.audioHandler.disableEncryption();
}
});
}
@override
Widget build(BuildContext context) {
return IconButton(
icon: Icon(
_isEncrypted ? Icons.lock : Icons.lock_open,
color: Colors.blueAccent,
),
onPressed: _toggleEncryption,
tooltip: _isEncrypted ? 'Disable Encryption' : 'Enable Encryption',
);
}
}

View File

@ -0,0 +1,65 @@
// lib/widgets/listen_replay_button.dart
import 'package:flutter/material.dart';
import '../services/audio_handler.dart';
import 'package:audioplayers/audioplayers.dart';
class ListenReplayButton extends StatefulWidget {
final AudioHandler audioHandler;
const ListenReplayButton({Key? key, required this.audioHandler}) : super(key: key);
@override
_ListenReplayButtonState createState() => _ListenReplayButtonState();
}
class _ListenReplayButtonState extends State<ListenReplayButton> {
bool _isListening = false;
final AudioPlayer _audioPlayer = AudioPlayer();
void _toggleListening() async {
if (_isListening) {
await widget.audioHandler.stopListening();
} else {
await widget.audioHandler.startListening();
// Listen to the audio stream and replay it
widget.audioHandler.audioStream.listen((data) async {
// Convert Uint8List to a playable format
// For simplicity, we'll write to a temporary file and play it
// In production, consider using a more efficient method
String filePath = '/tmp/temp_audio.aac'; // Adjust path as needed
// Use path_provider to get a valid directory
// Add dependency: path_provider: ^2.0.11
// Alternatively, use a package that allows in-memory playback
// Placeholder: Play directly from bytes is not supported by audioplayers
// You may need to implement native playback or use another package
// For demonstration, we'll skip actual playback
print('Replaying audio data: ${data.length} bytes');
});
}
setState(() {
_isListening = !_isListening;
});
}
@override
void dispose() {
_audioPlayer.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return IconButton(
icon: Icon(
_isListening ? Icons.stop : Icons.mic,
color: Colors.orangeAccent,
),
onPressed: _toggleListening,
tooltip: _isListening ? 'Stop Listening' : 'Listen and Replay',
);
}
}

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

View File

@ -0,0 +1,59 @@
## 2.1.1
* Fix Android building
## 2.1.0
* Make min sdk to Flutter 3.0.0
## 2.0.0
* Remove null check operator from method ListenPhone
## 1.0.4
* Null safety support
## 1.0.3
* Fix crash because of null value on empty number
## 1.0.2
* Support old Flutter plugin V1
## 1.0.1
* Fix crash related to Android 10
## 1.0.0
* Addded has phone permission
* Added request phone permission
* Added phone permission listener
* Fix bugs
## 0.0.6
* Add sample image.
## 0.0.5
* Add support for dual sim card.
## 0.0.4
* Print exception message on debugging console.
## 0.0.3
* Migrate to AndroidX
## 0.0.2
* Add gt mobile number native code to Android
## 0.0.1
* Initial Release.

View File

@ -0,0 +1,13 @@
Copyright 2023 Amr Eniou
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS,
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
See the License for the specific language governing permissions and
limitations under the License.

View File

@ -0,0 +1,66 @@
# mobile_number
This is a FLutter Plugin to get the device mobile number.
#### Note: It works for Android only because getting mobile number of sim card is not supported in iOS.
#### Note: If the mobile number is not pre-exist on sim card it will not return te phone number.
## Installation
#### Link on Flutter plugins
https://pub.dev/packages/mobile_number
#### Note:
if you still using depecated FlutterActivty on MainActivity.java
which is import of
- `import io.flutter.app.FlutterActivity;`
not
- `import io.flutter.embedding.android.FlutterActivity;`
then you need to add the following to your MainActivity.java
```
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
MobileNumberPlugin.registerWith(registrarFor("com.amorenew.mobile_number.MobileNumberPlugin()"));
}
```
## Usage
#### Check Phone Permission
```await MobileNumber.hasPhonePermission```
#### Request Phone Permission
```await MobileNumber.requestPhonePermission```
#### Listen to widget resume after Phone Permission request
```MobileNumber.listenPhonePermission((isPermissionGranted) {
if (isPermissionGranted) {
//Get mobile number
} else {
//Request Phone Permission
}
});
```
#### Get first sim card number
```Future<String> getMobileNumber() async {
final String mobileNumber = await MobileNumber.mobileNumber;
return mobileNumber;
}
```
#### Get List of sim cards for dual sim cards
```Future<List<SimCard>> geSimCards() async {
final List<SimCard> simCards = await MobileNumber.getSimCards;
return simCards;
}
```
![alt text](https://raw.githubusercontent.com/amorenew/Flutter-Mobile-Number-Plugin/master/sample1.png)

View File

@ -0,0 +1,41 @@
group 'com.amorenew.mobile_number'
version '1.0-SNAPSHOT'
buildscript {
repositories {
google()
mavenCentral()
}
dependencies {
classpath 'com.android.tools.build:gradle:7.2.2'
}
}
rootProject.allprojects {
repositories {
google()
mavenCentral()
}
}
apply plugin: 'com.android.library'
android {
compileSdkVersion 33
namespace "com.amorenew.mobile_number"
defaultConfig {
targetSdkVersion 33
minSdkVersion 23
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
lintOptions {
disable 'InvalidPackage'
}
}
dependencies {
implementation 'androidx.core:core:1.9.0'
// Other dependencies...
}

View File

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

View File

@ -0,0 +1,5 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-7.3.1-bin.zip
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

View File

@ -0,0 +1 @@
{}

View File

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

View File

@ -0,0 +1,10 @@
<?xml version="1.0" encoding="utf-8"?>
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.amorenew.mobile_number"> <!-- Package is "com.amorenew.mobile_number" -->
<uses-permission android:name="android.permission.READ_PHONE_STATE"
android:maxSdkVersion="29" />
<uses-permission android:name="android.permission.READ_PHONE_NUMBERS" />
<meta-data
android:name="flutterEmbedding"
android:value="2" />
</manifest>

View File

@ -0,0 +1,262 @@
package com.amorenew.mobile_number;
import java.util.HashMap;
import java.util.Map;
class CountryToPhonePrefix {
private static Map<String, String> map = new HashMap<>();
static String prefixFor(String iso2CountryCode) {
String result = map.get(iso2CountryCode.toUpperCase());
if (result == null) {
return "";
}
return result;
}
static {
map.put("AC", "247");
map.put("AD", "376");
map.put("AE", "971");
map.put("AF", "93");
map.put("AG", "1268");
map.put("AI", "1264");
map.put("AL", "355");
map.put("AM", "374");
map.put("AN", "599");
map.put("AO", "244");
map.put("AR", "54");
map.put("AS", "1684");
map.put("AT", "43");
map.put("AU", "61");
map.put("AW", "297");
map.put("AX", "35818");
// map.put("AZ", "37497");
map.put("AZ", "994");
map.put("BA", "387");
map.put("BB", "1246");
map.put("BD", "880");
map.put("BE", "32");
map.put("BF", "226");
map.put("BG", "359");
map.put("BH", "973");
map.put("BI", "257");
map.put("BJ", "229");
map.put("BM", "1441");
map.put("BN", "673");
map.put("BO", "591");
map.put("BR", "55");
map.put("BS", "1242");
map.put("BT", "975");
map.put("BW", "267");
map.put("BY", "375");
map.put("BZ", "501");
map.put("CA", "1");
map.put("CC", "61");
map.put("CD", "243");
map.put("CF", "236");
map.put("CG", "242");
map.put("CH", "41");
map.put("CI", "225");
map.put("CK", "682");
map.put("CL", "56");
map.put("CM", "237");
map.put("CN", "86");
map.put("CO", "57");
map.put("CR", "506");
map.put("CS", "381");
map.put("CU", "53");
map.put("CV", "238");
map.put("CX", "61");
// map.put("CY", "90392");
map.put("CY", "357");
map.put("CZ", "420");
map.put("DE", "49");
map.put("DJ", "253");
map.put("DK", "45");
map.put("DM", "1767");
map.put("DO", "1809"); // and 1829?
map.put("DZ", "213");
map.put("EC", "593");
map.put("EE", "372");
map.put("EG", "20");
map.put("EH", "212");
map.put("ER", "291");
map.put("ES", "34");
map.put("ET", "251");
map.put("FI", "358");
map.put("FJ", "679");
map.put("FK", "500");
map.put("FM", "691");
map.put("FO", "298");
map.put("FR", "33");
map.put("GA", "241");
map.put("GB", "44");
map.put("GD", "1473");
map.put("GE", "995");
map.put("GF", "594");
map.put("GG", "44");
map.put("GH", "233");
map.put("GI", "350");
map.put("GL", "299");
map.put("GM", "220");
map.put("GN", "224");
map.put("GP", "590");
map.put("GQ", "240");
map.put("GR", "30");
map.put("GT", "502");
map.put("GU", "1671");
map.put("GW", "245");
map.put("GY", "592");
map.put("HK", "852");
map.put("HN", "504");
map.put("HR", "385");
map.put("HT", "509");
map.put("HU", "36");
map.put("ID", "62");
map.put("IE", "353");
map.put("IL", "972");
map.put("IM", "44");
map.put("IN", "91");
map.put("IO", "246");
map.put("IQ", "964");
map.put("IR", "98");
map.put("IS", "354");
map.put("IT", "39");
map.put("JE", "44");
map.put("JM", "1876");
map.put("JO", "962");
map.put("JP", "81");
map.put("KE", "254");
map.put("KG", "996");
map.put("KH", "855");
map.put("KI", "686");
map.put("KM", "269");
map.put("KN", "1869");
map.put("KP", "850");
map.put("KR", "82");
map.put("KW", "965");
map.put("KY", "1345");
map.put("KZ", "7");
map.put("LA", "856");
map.put("LB", "961");
map.put("LC", "1758");
map.put("LI", "423");
map.put("LK", "94");
map.put("LR", "231");
map.put("LS", "266");
map.put("LT", "370");
map.put("LU", "352");
map.put("LV", "371");
map.put("LY", "218");
map.put("MA", "212");
map.put("MC", "377");
// map.put("MD", "373533");
map.put("MD", "373");
map.put("ME", "382");
map.put("MG", "261");
map.put("MH", "692");
map.put("MK", "389");
map.put("ML", "223");
map.put("MM", "95");
map.put("MN", "976");
map.put("MO", "853");
map.put("MP", "1670");
map.put("MQ", "596");
map.put("MR", "222");
map.put("MS", "1664");
map.put("MT", "356");
map.put("MU", "230");
map.put("MV", "960");
map.put("MW", "265");
map.put("MX", "52");
map.put("MY", "60");
map.put("MZ", "258");
map.put("NA", "264");
map.put("NC", "687");
map.put("NE", "227");
map.put("NF", "672");
map.put("NG", "234");
map.put("NI", "505");
map.put("NL", "31");
map.put("NO", "47");
map.put("NP", "977");
map.put("NR", "674");
map.put("NU", "683");
map.put("NZ", "64");
map.put("OM", "968");
map.put("PA", "507");
map.put("PE", "51");
map.put("PF", "689");
map.put("PG", "675");
map.put("PH", "63");
map.put("PK", "92");
map.put("PL", "48");
map.put("PM", "508");
map.put("PR", "1787"); // and 1939 ?
map.put("PS", "970");
map.put("PT", "351");
map.put("PW", "680");
map.put("PY", "595");
map.put("QA", "974");
map.put("RE", "262");
map.put("RO", "40");
map.put("RS", "381");
map.put("RU", "7");
map.put("RW", "250");
map.put("SA", "966");
map.put("SB", "677");
map.put("SC", "248");
map.put("SD", "249");
map.put("SE", "46");
map.put("SG", "65");
map.put("SH", "290");
map.put("SI", "386");
map.put("SJ", "47");
map.put("SK", "421");
map.put("SL", "232");
map.put("SM", "378");
map.put("SN", "221");
map.put("SO", "252");
map.put("SR", "597");
map.put("ST", "239");
map.put("SV", "503");
map.put("SY", "963");
map.put("SZ", "268");
map.put("TA", "290");
map.put("TC", "1649");
map.put("TD", "235");
map.put("TG", "228");
map.put("TH", "66");
map.put("TJ", "992");
map.put("TK", "690");
map.put("TL", "670");
map.put("TM", "993");
map.put("TN", "216");
map.put("TO", "676");
map.put("TR", "90");
map.put("TT", "1868");
map.put("TV", "688");
map.put("TW", "886");
map.put("TZ", "255");
map.put("UA", "380");
map.put("UG", "256");
map.put("US", "1");
map.put("UY", "598");
map.put("UZ", "998");
map.put("VA", "379");
map.put("VC", "1784");
map.put("VE", "58");
map.put("VG", "1284");
map.put("VI", "1340");
map.put("VN", "84");
map.put("VU", "678");
map.put("WF", "681");
map.put("WS", "685");
map.put("YE", "967");
map.put("YT", "262");
map.put("ZA", "27");
map.put("ZM", "260");
map.put("ZW", "263");
}}

View File

@ -0,0 +1,248 @@
package com.amorenew.mobile_number;
import android.Manifest;
import android.annotation.SuppressLint;
import android.app.Activity;
import android.content.Context;
import android.content.pm.PackageManager;
import android.os.Build;
import android.telephony.SubscriptionInfo;
import android.telephony.SubscriptionManager;
import android.telephony.TelephonyManager;
import android.util.Log;
import androidx.annotation.NonNull;
import androidx.annotation.RequiresApi;
import androidx.core.app.ActivityCompat;
import androidx.core.content.ContextCompat;
import org.json.JSONArray;
import java.util.ArrayList;
import java.util.List;
import io.flutter.embedding.engine.plugins.FlutterPlugin;
import io.flutter.embedding.engine.plugins.activity.ActivityAware;
import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding;
import io.flutter.plugin.common.BinaryMessenger;
import io.flutter.plugin.common.EventChannel;
import io.flutter.plugin.common.MethodCall;
import io.flutter.plugin.common.MethodChannel;
import io.flutter.plugin.common.MethodChannel.MethodCallHandler;
import io.flutter.plugin.common.MethodChannel.Result;
import io.flutter.plugin.common.PluginRegistry.Registrar;
import io.flutter.plugin.common.PluginRegistry.RequestPermissionsResultListener;
/**
* MobileNumberPlugin
*/
public class MobileNumberPlugin implements FlutterPlugin, ActivityAware, MethodCallHandler, RequestPermissionsResultListener {
private static final int MY_PERMISSIONS_REQUEST_READ_PHONE_STATE = 0;
final String Event_phonePermissionResult = "requestPhonePermission=";
private EventChannel.EventSink permissionEvent;
private Context applicationContext;
private Activity activity;
private TelephonyManager telephonyManager;
private Result result;
private MethodChannel methodChannel;
private EventChannel permissionEventChannel;
/**
* Plugin registration.
*/
public static void registerWith(Registrar registrar) {
final MobileNumberPlugin instance = new MobileNumberPlugin();
instance.onAttachedToEngine(registrar.context(), registrar.messenger(), registrar.activity());
}
@Override
public void onAttachedToEngine(@NonNull FlutterPluginBinding flutterPluginBinding) {
onAttachedToEngine(flutterPluginBinding.getApplicationContext(), flutterPluginBinding.getBinaryMessenger(), null);
}
private void onAttachedToEngine(Context applicationContext, BinaryMessenger messenger, Activity _activity) {
this.applicationContext = applicationContext;
if(_activity!=null)
this.activity=_activity;
methodChannel = new MethodChannel(messenger, "mobile_number");
methodChannel.setMethodCallHandler(this);
permissionEventChannel = new EventChannel(messenger, "phone_permission_event");
permissionEventChannel.setStreamHandler(new EventChannel.StreamHandler() {
@Override
public void onListen(Object o, EventChannel.EventSink eventSink) {
permissionEvent = eventSink;
}
@Override
public void onCancel(Object o) {
}
});
}
@Override
public void onDetachedFromEngine(@NonNull FlutterPluginBinding flutterPluginBinding) {
}
@Override
public void onAttachedToActivity(@NonNull ActivityPluginBinding activityPluginBinding) {
//MobileNumberPlugin.activity = activityPluginBinding.getActivity();
//activityV2 = activityPluginBinding.getActivity();
activity = activityPluginBinding.getActivity();
}
@Override
public void onDetachedFromActivityForConfigChanges() {
}
@Override
public void onReattachedToActivityForConfigChanges(@NonNull ActivityPluginBinding activityPluginBinding) {
}
@Override
public void onDetachedFromActivity() {
}
@Override
public void onMethodCall(MethodCall call, Result result) {
this.result = result;
final String method_GetMobileNumber = "getMobileNumber";
final String method_hasPhonePermission = "hasPhonePermission";
final String method_requestPhonePermission = "requestPhonePermission";
switch (call.method) {
case method_GetMobileNumber:
telephonyManager = (TelephonyManager) applicationContext
.getSystemService(Context.TELEPHONY_SERVICE);
getMobileNumber();
break;
case method_hasPhonePermission:
result.success(hasPhonePermission());
break;
case method_requestPhonePermission:
requestPhonePermission();
break;
default:
result.notImplemented();
break;
}
}
private boolean hasPhonePermission() {
if (android.os.Build.VERSION.SDK_INT > Build.VERSION_CODES.Q) {
return ContextCompat.checkSelfPermission(applicationContext,
Manifest.permission.READ_PHONE_NUMBERS) == PackageManager.PERMISSION_GRANTED;
} else {
return ContextCompat.checkSelfPermission(applicationContext,
Manifest.permission.READ_PHONE_STATE) == PackageManager.PERMISSION_GRANTED;
}
}
private void requestPhonePermission() {
if (android.os.Build.VERSION.SDK_INT > Build.VERSION_CODES.Q) {
if (ActivityCompat.shouldShowRequestPermissionRationale(activity,
Manifest.permission.READ_PHONE_NUMBERS)) {
// Show an explanation to the user *asynchronously* -- don't block
// this thread waiting for the user's response! After the user
// sees the explanation, try again to request the permission.
} else {
ActivityCompat.requestPermissions(activity,
new String[]{Manifest.permission.READ_PHONE_NUMBERS}, MY_PERMISSIONS_REQUEST_READ_PHONE_STATE);
}
} else {
if (ActivityCompat.shouldShowRequestPermissionRationale(activity,
Manifest.permission.READ_PHONE_STATE)) {
} else {
ActivityCompat.requestPermissions(activity,
new String[]{Manifest.permission.READ_PHONE_STATE}, MY_PERMISSIONS_REQUEST_READ_PHONE_STATE);
}
}
}
private void getMobileNumber() {
if (!hasPhonePermission()) {
requestPhonePermission();
} else {
// Permission has already been granted
generateMobileNumber();
}
}
@SuppressLint("HardwareIds")
private void generateMobileNumber() {
JSONArray simJsonArray = new JSONArray();
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.LOLLIPOP_MR1) {
for (SubscriptionInfo subscriptionInfo : getSubscriptions()) {
SimCard simCard = new SimCard(telephonyManager, subscriptionInfo);
simJsonArray.put(simCard.toJSON());
}
}
if (simJsonArray.length()==0) {
SimCard simCard = getSingleSimCard();
if (simCard != null) {
simJsonArray.put(simCard.toJSON());
}
}
if (simJsonArray.toString().isEmpty()) {
Log.d("UNAVAILABLE", "No phone number on sim card#3");
result.error("UNAVAILABLE", "No phone number on sim card", null);
} else result.success(simJsonArray.toString());
}
@SuppressLint("HardwareIds")
SimCard getSingleSimCard() {
if (ActivityCompat.checkSelfPermission(activity, Manifest.permission.READ_PHONE_NUMBERS) == PackageManager.PERMISSION_DENIED
&& ActivityCompat.checkSelfPermission(activity, Manifest.permission.READ_PHONE_STATE) == PackageManager.PERMISSION_DENIED) {
Log.e("UNAVAILABLE", "No phone number on sim card Permission Denied#2", null);
return null;
} else if (telephonyManager.getLine1Number() == null || telephonyManager.getLine1Number().isEmpty()) {
Log.e("UNAVAILABLE", "No phone number on sim card#2", null);
return null;
}
return new SimCard(telephonyManager);
}
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP_MR1)
List<SubscriptionInfo> getSubscriptions() {
final SubscriptionManager subscriptionManager = (SubscriptionManager) activity.getSystemService(Context.TELEPHONY_SUBSCRIPTION_SERVICE);
if (ActivityCompat.checkSelfPermission(activity, Manifest.permission.READ_PHONE_NUMBERS) == PackageManager.PERMISSION_DENIED
&& ActivityCompat.checkSelfPermission(activity, Manifest.permission.READ_PHONE_STATE) == PackageManager.PERMISSION_DENIED) {
Log.e("UNAVAILABLE", "No phone number on sim card Permission Denied#1", null);
return new ArrayList<>();
} else if (subscriptionManager == null) {
Log.e("UNAVAILABLE", "No phone number on sim card#1", null);
return new ArrayList<>();
}
return subscriptionManager.getActiveSubscriptionInfoList();
}
@Override
public boolean onRequestPermissionsResult(int requestCode, @NonNull String[] permissions,
@NonNull int[] grantResults) {
// If request is cancelled, the result arrays are empty.
if (requestCode == MY_PERMISSIONS_REQUEST_READ_PHONE_STATE) {
if (grantResults.length > 0 && grantResults[0] == PackageManager.PERMISSION_GRANTED) {
if (permissionEvent != null)
permissionEvent.success(true);
generateMobileNumber();
return true;
} else {
if (permissionEvent != null)
permissionEvent.success(false);
}
}
result.error("PERMISSION", "onRequestPermissionsResult is not granted", null);
return false;
}
}

View File

@ -0,0 +1,73 @@
package com.amorenew.mobile_number;
import android.annotation.SuppressLint;
import android.os.Build;
import android.telephony.SubscriptionInfo;
import android.telephony.TelephonyManager;
import androidx.annotation.RequiresApi;
import org.json.JSONException;
import org.json.JSONObject;
public class SimCard {
private String carrierName = "";
private String displayName = "";
private int slotIndex = 0;
private String number = "";
private String countryIso = "";
private String countryPhonePrefix = "";
@RequiresApi(api = Build.VERSION_CODES.LOLLIPOP_MR1)
public SimCard(TelephonyManager telephonyManager, SubscriptionInfo subscriptionInfo) {
this.carrierName = subscriptionInfo.getCarrierName().toString();
this.displayName = subscriptionInfo.getDisplayName().toString();
this.slotIndex = subscriptionInfo.getSimSlotIndex();
this.number = subscriptionInfo.getNumber();
if (subscriptionInfo.getCountryIso() != null && !subscriptionInfo.getCountryIso().isEmpty())
this.countryIso = subscriptionInfo.getCountryIso();
else if (telephonyManager.getSimCountryIso() != null)
this.countryIso = telephonyManager.getSimCountryIso();
this.countryPhonePrefix = CountryToPhonePrefix.prefixFor(this.countryIso);
}
@SuppressLint({"MissingPermission", "HardwareIds"})
public SimCard(TelephonyManager telephonyManager) {
if (telephonyManager.getSimOperator() != null)
carrierName = telephonyManager.getSimOperatorName();
if (telephonyManager.getSimOperator() != null)
displayName = telephonyManager.getSimOperatorName();
if (telephonyManager.getSimCountryIso() != null) {
countryIso = telephonyManager.getSimCountryIso();
countryPhonePrefix = CountryToPhonePrefix.prefixFor(countryIso);
}
if (telephonyManager.getLine1Number() != null && !telephonyManager.getLine1Number().isEmpty()) {
if (telephonyManager.getLine1Number().startsWith("0"))
number = countryPhonePrefix + telephonyManager.getLine1Number().substring(1);
number = telephonyManager.getLine1Number();
}
}
// final JSONArray jsonArray = new JSONArray();
JSONObject toJSON() {
JSONObject json = new JSONObject();
try {
json.put("carrierName", carrierName);
json.put("displayName", displayName);
json.put("slotIndex", slotIndex);
json.put("number", number);
json.put("countryIso", countryIso);
json.put("countryPhonePrefix", countryPhonePrefix);
} catch (JSONException e) {
e.printStackTrace();
}
return json;
}
}

View File

@ -0,0 +1,16 @@
# mobile_number_example
Demonstrates how to use the mobile_number plugin.
## Getting Started
This project is a starting point for a Flutter application.
A few resources to get you started if this is your first Flutter project:
- [Lab: Write your first Flutter app](https://flutter.dev/docs/get-started/codelab)
- [Cookbook: Useful Flutter samples](https://flutter.dev/docs/cookbook)
For help getting started with Flutter, view our
[online documentation](https://flutter.dev/docs), which offers tutorials,
samples, guidance on mobile development, and a full API reference.

View File

@ -0,0 +1,58 @@
def localProperties = new Properties()
def localPropertiesFile = rootProject.file('local.properties')
if (localPropertiesFile.exists()) {
localPropertiesFile.withReader('UTF-8') { reader ->
localProperties.load(reader)
}
}
def flutterRoot = localProperties.getProperty('flutter.sdk')
if (flutterRoot == null) {
throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.")
}
def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
if (flutterVersionCode == null) {
flutterVersionCode = '1'
}
def flutterVersionName = localProperties.getProperty('flutter.versionName')
if (flutterVersionName == null) {
flutterVersionName = '1.0'
}
apply plugin: 'com.android.application'
apply from: "$flutterRoot/packages/flutter_tools/gradle/flutter.gradle"
android {
compileSdk 33
defaultConfig {
applicationId "com.example.mobile_number_example"
minSdkVersion 23
targetSdkVersion 33
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
signingConfig signingConfigs.debug
}
}
lint {
disable 'InvalidPackage'
}
}
flutter {
source '../..'
}
dependencies {
testImplementation 'junit:junit:4.13.2'
androidTestImplementation 'androidx.test:runner:1.4.0'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0'
}

View File

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

View File

@ -0,0 +1,58 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.example.mobile_number_example">
<!-- io.flutter.app.FlutterApplication is an android.app.Application that
calls FlutterMain.startInitialization(this); in its onCreate method.
In most cases you can leave this as-is, but you if you want to provide
additional functionality it is fine to subclass or reimplement
FlutterApplication and put your custom class here. -->
<application
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher"
android:label="mobile_number_example">
<activity
android:name=".MainActivity"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:launchMode="singleTop"
android:theme="@style/LaunchTheme"
android:windowSoftInputMode="adjustResize"
android:exported="true">
<!-- This keeps the window background of the activity showing
until Flutter renders its first frame. It can be removed if
there is no splash screen (such as the default splash screen
defined in @style/LaunchTheme). -->
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<meta-data
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme" />
<!-- Displays an Android View that continues showing the launch screen
Drawable until Flutter paints its first frame, then this splash
screen fades out. A splash screen is useful to avoid any visual
gap between the end of Android's launch screen and the painting of
Flutter's first frame. -->
<meta-data
android:name="io.flutter.embedding.android.SplashScreenDrawable"
android:resource="@drawable/launch_background" />
<meta-data
android:name="io.flutter.app.android.SplashScreenUntilFirstFrame"
android:value="true" />
</activity>
<activity
android:name=".EmbeddingV1Activity"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|locale|layoutDirection|fontScale"
android:hardwareAccelerated="true"
android:theme="@style/LaunchTheme"
android:windowSoftInputMode="adjustResize" />
<meta-data
android:name="flutterEmbedding"
android:value="2" />
</application>
</manifest>

View File

@ -0,0 +1,14 @@
package com.example.mobile_number_example;
import android.os.Bundle;
import com.amorenew.mobile_number.MobileNumberPlugin;
import io.flutter.app.FlutterActivity;
public class EmbeddingV1Activity extends FlutterActivity {
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
MobileNumberPlugin.registerWith(registrarFor("com.amorenew.mobile_number.MobileNumberPlugin()"));
}
}

View File

@ -0,0 +1,15 @@
package com.example.mobile_number_example;
import io.flutter.embedding.android.FlutterActivity;
public class MainActivity extends FlutterActivity {
// @Override
// public void configureFlutterEngine(FlutterEngine flutterEngine) {
// super.configureFlutterEngine(flutterEngine);
// flutterEngine.getPlugins().add(new com.example.mobile_number.MobileNumberPlugin());
// }
}

View File

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

Binary file not shown.

After

Width:  |  Height:  |  Size: 544 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 442 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 721 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.4 KiB

Some files were not shown because too many files have changed in this diff Show More