Compare commits

...

2 Commits

Author SHA1 Message Date
1867c025fd add
All checks were successful
/ mirror (push) Successful in 4s
2025-02-15 14:35:47 +00:00
90cea674d4 add phone calls
All checks were successful
/ mirror (push) Successful in 4s
2025-02-05 15:01:52 +00:00
8 changed files with 565 additions and 96 deletions

View File

@ -1,52 +1,49 @@
<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" />
<application
android:label="Icing Dialer"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher"
android:enableOnBackInvokedCallback="true">
<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTop"
android:taskAffinity=""
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize">
<!-- Specifies an Android theme to apply to this Activity as soon as
the Android process has started. This theme is visible to the user
while the Flutter UI initializes. After that, this theme continues
to determine the Window background behind the Flutter UI. -->
<meta-data
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme"
/>
<uses-permission android:name="android.permission.READ_PHONE_STATE"/>
<uses-permission android:name="android.permission.CALL_PHONE"/>
<uses-permission android:name="android.permission.ANSWER_PHONE_CALLS"/>
<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.MANAGE_OWN_CALLS"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
<uses-permission android:name="android.permission.BIND_TELECOM_CONNECTION_SERVICE" />
<application android:label="Icing Dialer" android:name="${applicationName}" android:icon="@mipmap/ic_launcher" android:enableOnBackInvokedCallback="true">
<activity android:name=".MainActivity" android:exported="true" android:launchMode="singleTop" android:taskAffinity="" android:theme="@style/LaunchTheme" android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode" android:hardwareAccelerated="true" android:windowSoftInputMode="adjustResize">
<!-- Specifies an Android theme to apply to this Activity as soon as
the Android process has started. This theme is visible to the user
while the Flutter UI initializes. After that, this theme continues
to determine the Window background behind the Flutter UI. -->
<meta-data android:name="io.flutter.embedding.android.NormalTheme" android:resource="@style/NormalTheme"/>
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
</intent-filter>
<intent-filter>
<action android:name="android.intent.action.DIAL" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
</activity>
<!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
<meta-data
android:name="flutterEmbedding"
android:value="2" />
<service android:name=".MyConnectionService" android:label="My Connection Service" 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 android:name="flutterEmbedding" android:value="2"/>
</application>
<!-- Required to query activities that can process text, see:
https://developer.android.com/training/package-visibility and
https://developer.android.com/reference/android/content/Intent#ACTION_PROCESS_TEXT.
<!-- Required to query activities that can process text, see:
https://developer.android.com/training/package-visibility and
https://developer.android.com/reference/android/content/Intent#ACTION_PROCESS_TEXT.
In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. -->
In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. -->
<queries>
<intent>
<action android:name="android.intent.action.PROCESS_TEXT"/>
<data android:mimeType="text/plain"/>
</intent>
</queries>
</manifest>
</manifest>

View File

@ -1,5 +1,218 @@
package com.example.dialer
import android.app.role.RoleManager
import android.content.BroadcastReceiver
import android.content.ComponentName
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.content.pm.PackageManager
import android.os.Build
import android.os.Bundle
import android.os.Handler
import android.telecom.PhoneAccount
import android.telecom.PhoneAccountHandle
import android.telecom.TelecomManager
import android.util.Log
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodChannel
class MainActivity: FlutterActivity()
class MainActivity : FlutterActivity() {
private val CHANNEL = "com.example.dialer/call_events"
private lateinit var methodChannel: MethodChannel
// Request code for the default-dialer request.
private val DEFAULT_DIALER_REQUEST_CODE = 1001
// Request code for runtime permissions.
private val PERMISSION_REQUEST_CODE = 1002
// BroadcastReceiver to catch incoming call events from our ConnectionService.
private val incomingCallReceiver = object : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
val phoneNumber = intent?.getStringExtra("phoneNumber") ?: "Unknown"
notifyIncomingCall(phoneNumber)
}
}
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
methodChannel = MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL)
methodChannel.setMethodCallHandler { call, result ->
when (call.method) {
"requestDefaultDialer" -> {
requestDefaultDialer()
result.success(null)
}
else -> result.notImplemented()
}
}
}
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
// Request runtime permissions.
checkAndRequestPermissions()
// Create the intent filter for incoming calls.
val filter = IntentFilter("com.example.dialer.INCOMING_CALL")
// Register the receiver. (For Android 13+ we must specify not exported.)
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
registerReceiver(incomingCallReceiver, filter, Context.RECEIVER_NOT_EXPORTED)
} else {
registerReceiver(incomingCallReceiver, filter)
}
}
override fun onDestroy() {
super.onDestroy()
unregisterReceiver(incomingCallReceiver)
}
override fun onResume() {
super.onResume()
// Check if we are now the default dialer and update the UI accordingly.
if (isDefaultDialer()) {
registerManagedPhoneAccount()
notifyDefaultDialerSet()
}
}
private fun isDefaultDialer(): Boolean {
return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
val roleManager = getSystemService(RoleManager::class.java)
roleManager.isRoleHeld(RoleManager.ROLE_DIALER)
} else {
val telecomManager = getSystemService(TELECOM_SERVICE) as TelecomManager
telecomManager.defaultDialerPackage == packageName
}
}
// Request to become the default dialer.
private fun requestDefaultDialer() {
try {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
// Use the RoleManager API for Android Q and above.
val roleManager = getSystemService(RoleManager::class.java)
if (!roleManager.isRoleHeld(RoleManager.ROLE_DIALER)) {
val intent = roleManager.createRequestRoleIntent(RoleManager.ROLE_DIALER)
startActivityForResult(intent, DEFAULT_DIALER_REQUEST_CODE)
Log.d("MainActivity", "Requested ROLE_DIALER via RoleManager")
} else {
Log.d("MainActivity", "Already default dialer (ROLE_DIALER held)")
registerManagedPhoneAccount()
notifyDefaultDialerSet()
}
} else {
// Fallback for older versions.
val telecomManager = getSystemService(TELECOM_SERVICE) as TelecomManager
if (telecomManager.defaultDialerPackage != packageName) {
val intent = Intent(TelecomManager.ACTION_CHANGE_DEFAULT_DIALER)
intent.putExtra(TelecomManager.EXTRA_CHANGE_DEFAULT_DIALER_PACKAGE_NAME, packageName)
startActivityForResult(intent, DEFAULT_DIALER_REQUEST_CODE)
Log.d("MainActivity", "Requested default dialer via TelecomManager")
} else {
registerManagedPhoneAccount()
notifyDefaultDialerSet()
}
}
} catch (ex: Exception) {
Log.e("MainActivity", "Error requesting default dialer", ex)
}
}
// Handle the result from the default-dialer role request.
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
super.onActivityResult(requestCode, resultCode, data)
if (requestCode == DEFAULT_DIALER_REQUEST_CODE) {
Log.d("MainActivity", "onActivityResult received: resultCode=$resultCode")
when (resultCode) {
RESULT_OK -> {
Log.d("MainActivity", "User granted default dialer role")
Handler(mainLooper).postDelayed({
if (isDefaultDialer()) {
Log.d("MainActivity", "Default dialer successfully set")
registerManagedPhoneAccount()
notifyDefaultDialerSet()
} else {
Log.d("MainActivity", "Default dialer not set")
// Notify Flutter that the default dialer role was not set
methodChannel.invokeMethod("onDefaultDialerNotSet", null)
}
}, 500)
}
RESULT_CANCELED -> {
Log.d("MainActivity", "User denied default dialer role")
// Notify Flutter that the user denied the request
methodChannel.invokeMethod("onDefaultDialerDenied", null)
}
else -> {
Log.d("MainActivity", "Unknown resultCode: $resultCode")
}
}
}
}
// Register a managed PhoneAccount with the TelecomManager.
private fun registerManagedPhoneAccount() {
val telecomManager = getSystemService(TELECOM_SERVICE) as TelecomManager
val phoneAccountHandle = PhoneAccountHandle(
ComponentName(this, MyConnectionService::class.java),
"MyPhoneAccountID"
)
val phoneAccount = PhoneAccount.builder(phoneAccountHandle, "Icing Dialer")
.setCapabilities(PhoneAccount.CAPABILITY_CALL_PROVIDER)
.setShortDescription("Icing Custom Dialer")
.build()
telecomManager.registerPhoneAccount(phoneAccount)
Log.d("MainActivity", "PhoneAccount registered")
}
// Notify Flutter that we are now the default dialer.
private fun notifyDefaultDialerSet() {
methodChannel.invokeMethod("onDefaultDialerSet", null)
}
// Notify Flutter of an incoming call.
private fun notifyIncomingCall(phoneNumber: String) {
methodChannel.invokeMethod("onIncomingCall", phoneNumber)
}
// Check and request runtime permissions.
private fun checkAndRequestPermissions() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
val permissionsNeeded = mutableListOf<String>()
val permissionsToRequest = mutableListOf<String>()
// Check if permissions are granted
if (ContextCompat.checkSelfPermission(this, android.Manifest.permission.READ_PHONE_STATE) != PackageManager.PERMISSION_GRANTED) {
permissionsToRequest.add(android.Manifest.permission.READ_PHONE_STATE)
}
if (ContextCompat.checkSelfPermission(this, android.Manifest.permission.CALL_PHONE) != PackageManager.PERMISSION_GRANTED) {
permissionsToRequest.add(android.Manifest.permission.CALL_PHONE)
}
if (ContextCompat.checkSelfPermission(this, android.Manifest.permission.ANSWER_PHONE_CALLS) != PackageManager.PERMISSION_GRANTED) {
permissionsToRequest.add(android.Manifest.permission.ANSWER_PHONE_CALLS)
}
// Request permissions if needed
if (permissionsToRequest.isNotEmpty()) {
ActivityCompat.requestPermissions(this, permissionsToRequest.toTypedArray(), PERMISSION_REQUEST_CODE)
}
}
}
override fun onRequestPermissionsResult(requestCode: Int, permissions: Array<String>, grantResults: IntArray) {
super.onRequestPermissionsResult(requestCode, permissions, grantResults)
if (requestCode == PERMISSION_REQUEST_CODE) {
if (grantResults.all { it == PackageManager.PERMISSION_GRANTED }) {
Log.d("MainActivity", "All permissions granted")
} else {
Log.d("MainActivity", "Some permissions denied")
}
}
}
}

View File

@ -0,0 +1,30 @@
// File: android/app/src/main/kotlin/com/example/dialer/MyConnection.kt
package com.example.dialer
import android.telecom.Connection
import android.telecom.DisconnectCause
import android.util.Log
class MyConnection : Connection() {
override fun onAnswer(videoState: Int) {
super.onAnswer(videoState)
Log.d("MyConnection", "onAnswer called")
setActive()
// (You can later add notifications back to Flutter here if needed.)
}
override fun onReject() {
super.onReject()
Log.d("MyConnection", "onReject called")
setDisconnected(DisconnectCause(DisconnectCause.REJECTED))
destroy()
}
override fun onDisconnect() {
super.onDisconnect()
Log.d("MyConnection", "onDisconnect called")
setDisconnected(DisconnectCause(DisconnectCause.LOCAL))
destroy()
}
}

View File

@ -0,0 +1,44 @@
// File: android/app/src/main/kotlin/com/example/dialer/MyConnectionService.kt
package com.example.dialer
import android.content.Intent
import android.net.Uri
import android.telecom.Connection
import android.telecom.ConnectionRequest
import android.telecom.ConnectionService
import android.telecom.PhoneAccountHandle
import android.util.Log
class MyConnectionService : ConnectionService() {
override fun onCreateIncomingConnection(
connectionManagerPhoneAccount: PhoneAccountHandle,
request: ConnectionRequest
): Connection? {
Log.d("MyConnectionService", "onCreateIncomingConnection")
val myConnection = MyConnection()
// Try to extract the incoming phone number from the request.
val phoneUri: Uri? = request.address
val phoneNumber = phoneUri?.schemeSpecificPart ?: "Unknown"
// Broadcast an intent so MainActivity can notify Flutter.
val intent = Intent("com.example.dialer.INCOMING_CALL")
intent.putExtra("phoneNumber", phoneNumber)
applicationContext.sendBroadcast(intent)
// Mark the connection as ringing.
myConnection.setRinging()
return myConnection
}
override fun onCreateOutgoingConnection(
connectionManagerPhoneAccount: PhoneAccountHandle,
request: ConnectionRequest
): Connection? {
Log.d("MyConnectionService", "onCreateOutgoingConnection")
val myConnection = MyConnection()
myConnection.setDialing()
return myConnection
}
}

View File

@ -1,3 +1,21 @@
// android/build.gradle (Project-level Gradle file)
buildscript {
ext.kotlin_version = "1.8.10"
repositories {
google()
mavenCentral()
}
dependencies {
// Here is where the 'classpath' declarations go
classpath 'com.android.tools.build:gradle:8.1.2'
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
}
}
// Then your allprojects block can be minimal in newer Gradle versions:
allprojects {
repositories {
google()
@ -5,15 +23,18 @@ allprojects {
}
}
// If you want to customize build directory paths:
rootProject.buildDir = "../build"
subprojects {
project.buildDir = "${rootProject.buildDir}/${project.name}"
}
// This is optional, depends on your workflow:
subprojects {
project.evaluationDependsOn(":app")
}
// A custom clean task:
tasks.register("clean", Delete) {
delete rootProject.buildDir
}

View File

@ -2,3 +2,4 @@ org.gradle.jvmargs=-Xmx4G -XX:MaxMetaspaceSize=2G -XX:+HeapDumpOnOutOfMemoryErro
android.useAndroidX=true
android.enableJetifier=true
dev.steenbakker.mobile_scanner.useUnbundled=true
org.gradle.java.home=/usr/lib/jvm/java-17-openjdk-17.0.13.0.11-3.fc41.x86_64

View File

@ -1,24 +1,41 @@
import 'package:flutter/material.dart';
import 'package:flutter_contacts/flutter_contacts.dart';
import 'package:dialer/features/settings/settings.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';
/// This MyHomePage now expects two callbacks.
/// - onMakeCall: used to start calls
/// - onRequestDialer: used to become default dialer
class MyHomePage extends StatefulWidget {
final Function(String) onMakeCall;
final Function onRequestDialer;
const MyHomePage({
Key? key,
required this.onMakeCall,
required this.onRequestDialer,
}) : super(key: key);
@override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage>
with SingleTickerProviderStateMixin {
late TabController _tabController;
List<Contact> _allContacts = [];
List<Contact> _contactSuggestions = [];
final ContactService _contactService = ContactService();
final ContactService _contactService = ContactService();
@override
void initState() {
super.initState();
// Set the TabController length to 3
// We have 3 tabs: Favorites, History, Contacts
_tabController = TabController(length: 3, vsync: this, initialIndex: 1);
_tabController.addListener(_handleTabIndex);
_fetchContacts();
@ -30,19 +47,14 @@ class _MyHomePageState extends State<MyHomePage>
}
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();
}
});
if (query.isEmpty) {
_contactSuggestions = List.from(_allContacts);
} else {
_contactSuggestions = _allContacts.where((contact) {
return contact.displayName.toLowerCase().contains(query.toLowerCase());
}).toList();
}
setState(() {});
}
@override
@ -81,17 +93,16 @@ class _MyHomePageState extends State<MyHomePage>
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),
bottom: BorderSide(color: Colors.grey.shade800, width: 2),
),
),
// Using Flutter 3.10+ SearchAnchor / SearchBar API:
child: SearchAnchor(
builder:
(BuildContext context, SearchController controller) {
return SearchBar(
controller: controller,
padding:
MaterialStateProperty.all<EdgeInsetsGeometry>(
padding: MaterialStateProperty.all<EdgeInsetsGeometry>(
const EdgeInsets.only(
top: 6.0,
bottom: 6.0,
@ -104,7 +115,8 @@ class _MyHomePageState extends State<MyHomePage>
_onSearchChanged('');
},
backgroundColor: MaterialStateProperty.all(
const Color.fromARGB(255, 30, 30, 30)),
const Color.fromARGB(255, 30, 30, 30),
),
hintText: 'Search contacts',
hintStyle: MaterialStateProperty.all(
const TextStyle(color: Colors.grey, fontSize: 16.0),
@ -114,8 +126,7 @@ class _MyHomePageState extends State<MyHomePage>
color: Colors.grey,
size: 24.0,
),
shape:
MaterialStateProperty.all<RoundedRectangleBorder>(
shape: MaterialStateProperty.all<RoundedRectangleBorder>(
RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12.0),
),
@ -130,9 +141,12 @@ class _MyHomePageState extends State<MyHomePage>
return _contactSuggestions.map((contact) {
return ListTile(
key: ValueKey(contact.id),
title: Text(contact.displayName,
style: const TextStyle(color: Colors.white)),
title: Text(
contact.displayName,
style: const TextStyle(color: Colors.white),
),
onTap: () {
// For example, you might call widget.onMakeCall(contact.phoneNumber)
controller.closeView(contact.displayName);
},
);
@ -149,14 +163,20 @@ class _MyHomePageState extends State<MyHomePage>
value: 'settings',
child: Text('Settings'),
),
const PopupMenuItem<String>(
value: 'becomeDialer',
child: Text('Become Default Dialer'),
),
],
onSelected: (String value) {
if (value == 'settings') {
Navigator.push(
context,
MaterialPageRoute(
builder: (context) => const SettingsPage()),
MaterialPageRoute(builder: (context) => const SettingsPage()),
);
} else if (value == 'becomeDialer') {
// Use the callback passed in from main.dart:
widget.onRequestDialer();
}
},
),
@ -175,11 +195,14 @@ class _MyHomePageState extends State<MyHomePage>
ContactPage(),
],
),
// Floating action button for manual composition/dialpad
Positioned(
right: 20,
bottom: 20,
child: FloatingActionButton(
onPressed: () {
// Here you could do widget.onMakeCall('123456') or
// push to CompositionPage, etc.
Navigator.push(
context,
MaterialPageRoute(
@ -205,17 +228,20 @@ class _MyHomePageState extends State<MyHomePage>
controller: _tabController,
tabs: [
Tab(
icon: Icon(_tabController.index == 0
? Icons.star
: Icons.star_border)),
icon: Icon(_tabController.index == 0
? Icons.star
: Icons.star_border),
),
Tab(
icon: Icon(_tabController.index == 1
? Icons.access_time_filled
: Icons.access_time_outlined)),
icon: Icon(_tabController.index == 1
? Icons.access_time_filled
: Icons.access_time_outlined),
),
Tab(
icon: Icon(_tabController.index == 2
? Icons.contacts
: Icons.contacts_outlined)),
icon: Icon(_tabController.index == 2
? Icons.contacts
: Icons.contacts_outlined),
),
],
labelColor: Colors.white,
unselectedLabelColor: const Color.fromARGB(255, 158, 158, 158),
@ -226,10 +252,3 @@ class _MyHomePageState extends State<MyHomePage>
);
}
}
class MyHomePage extends StatefulWidget {
const MyHomePage({super.key});
@override
_MyHomePageState createState() => _MyHomePageState();
}

View File

@ -1,23 +1,167 @@
import 'package:dialer/features/home/home_page.dart';
import 'package:flutter/material.dart';
import 'package:dialer/features/contacts/contact_state.dart';
import 'package:flutter/services.dart';
void main() {
runApp(const MyApp());
runApp(const IcingDialerApp());
}
class MyApp extends StatelessWidget {
const MyApp({super.key});
class IcingDialerApp extends StatefulWidget {
const IcingDialerApp({Key? key}) : super(key: key);
@override
State<IcingDialerApp> createState() => _IcingDialerAppState();
}
class _IcingDialerAppState extends State<IcingDialerApp> {
// This channel must match the one in MainActivity.kt.
static const platform = MethodChannel('com.example.dialer/call_events');
bool isDefaultDialer = false;
@override
void initState() {
super.initState();
// Listen for method calls from the Android side.
platform.setMethodCallHandler(handleMethodCalls);
}
Future<void> handleMethodCalls(MethodCall call) async {
switch (call.method) {
case 'onDefaultDialerSet':
// Update our UI to show that we are now the default dialer.
setState(() {
isDefaultDialer = true;
});
break;
case 'onDefaultDialerDenied':
// Show a message to the user when they deny the request.
_showDefaultDialerDeniedDialog();
break;
case 'onIncomingCall':
final phoneNumber = call.arguments as String? ?? "Unknown";
_showIncomingCallDialog(phoneNumber);
break;
default:
print("Unhandled method: ${call.method}");
}
}
// Show a dialog when the user denies the default dialer request.
void _showDefaultDialerDeniedDialog() {
showDialog(
context: context,
builder: (context) => AlertDialog(
title: const Text('Permission Denied'),
content: const Text(
'To use this app as your default dialer, please grant the permission when prompted.'),
actions: [
TextButton(
onPressed: () {
Navigator.of(context).pop();
},
child: const Text('OK'),
),
],
),
);
}
// Show a dialog when an incoming call is intercepted.
void _showIncomingCallDialog(String phoneNumber) {
showDialog(
context: context,
barrierDismissible: false,
builder: (_) => AlertDialog(
title: const Text('Incoming Call'),
content: Text('Call from: $phoneNumber'),
actions: [
TextButton(
onPressed: () {
// (Here you could add code to reject the call.)
Navigator.of(context).pop();
},
child: const Text('Reject'),
),
ElevatedButton(
onPressed: () {
// (Here you could add code to accept the call.)
Navigator.of(context).pop();
},
child: const Text('Accept'),
),
],
),
);
}
// Invoke the native method to request to be the default dialer.
Future<void> requestDefaultDialer() async {
try {
await platform.invokeMethod('requestDefaultDialer');
} catch (e) {
print('Error requesting default dialer: $e');
// Show an error message if the request fails.
ScaffoldMessenger.of(context).showSnackBar(
SnackBar(content: Text('Error requesting default dialer: $e')),
);
}
}
@override
Widget build(BuildContext context) {
return ContactState(
child: MaterialApp(
theme: ThemeData(
brightness: Brightness.dark
),
home: SafeArea(child: MyHomePage()),
)
return MaterialApp(
title: 'Icing Dialer',
theme: ThemeData.dark(),
home: isDefaultDialer
? const CallScreen()
: DefaultDialerSetupScreen(onRequestDefaultDialer: requestDefaultDialer),
);
}
}
/// Shown when the app is not yet the default dialer.
class DefaultDialerSetupScreen extends StatelessWidget {
final VoidCallback onRequestDefaultDialer;
const DefaultDialerSetupScreen({Key? key, required this.onRequestDefaultDialer})
: super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('Set as Default Dialer')),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text(
'This app is not set as your default dialer.',
textAlign: TextAlign.center,
),
const SizedBox(height: 20),
ElevatedButton(
onPressed: onRequestDefaultDialer,
child: const Text('Set as Default Dialer'),
),
],
),
),
);
}
}
/// Shown when the app is the default dialer.
class CallScreen extends StatelessWidget {
const CallScreen({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
// A simple UI indicating that the app is waiting for incoming calls.
return Scaffold(
appBar: AppBar(title: const Text('Icing Dialer')),
body: const Center(
child: Text('Waiting for incoming calls...'),
),
);
}
}