diff --git a/dialer/android/app/src/main/AndroidManifest.xml b/dialer/android/app/src/main/AndroidManifest.xml
index db77a47..6e89d61 100644
--- a/dialer/android/app/src/main/AndroidManifest.xml
+++ b/dialer/android/app/src/main/AndroidManifest.xml
@@ -1,52 +1,49 @@
-
-
-
-
-
-
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
+
+
+
+
+
+
-
-
+
+
+
+
+
+
+
-
+ In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. -->
-
+
\ No newline at end of file
diff --git a/dialer/android/app/src/main/kotlin/com/example/dialer/MainActivity.kt b/dialer/android/app/src/main/kotlin/com/example/dialer/MainActivity.kt
index 1ad0677..cc34665 100644
--- a/dialer/android/app/src/main/kotlin/com/example/dialer/MainActivity.kt
+++ b/dialer/android/app/src/main/kotlin/com/example/dialer/MainActivity.kt
@@ -1,5 +1,160 @@
+// File: android/app/src/main/kotlin/com/example/dialer/MainActivity.kt
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.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 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
+
+ // 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)
+ // 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")
+ // Delay a short time before checking the role.
+ Handler(mainLooper).postDelayed({
+ if (isDefaultDialer()) {
+ Log.d("MainActivity", "Default dialer successfully set")
+ registerManagedPhoneAccount()
+ notifyDefaultDialerSet()
+ } else {
+ Log.d("MainActivity", "Default dialer not set")
+ }
+ }, 500)
+ }
+ }
+
+ // 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)
+ }
+
+ // 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)
+ }
+}
diff --git a/dialer/android/app/src/main/kotlin/com/example/dialer/MyConnection.kt b/dialer/android/app/src/main/kotlin/com/example/dialer/MyConnection.kt
new file mode 100644
index 0000000..731e5b6
--- /dev/null
+++ b/dialer/android/app/src/main/kotlin/com/example/dialer/MyConnection.kt
@@ -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()
+ }
+}
diff --git a/dialer/android/app/src/main/kotlin/com/example/dialer/MyConnectionService.kt b/dialer/android/app/src/main/kotlin/com/example/dialer/MyConnectionService.kt
new file mode 100644
index 0000000..68e0805
--- /dev/null
+++ b/dialer/android/app/src/main/kotlin/com/example/dialer/MyConnectionService.kt
@@ -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
+ }
+}
diff --git a/dialer/android/build.gradle b/dialer/android/build.gradle
index 457de89..8bc21dc 100644
--- a/dialer/android/build.gradle
+++ b/dialer/android/build.gradle
@@ -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
}
-
diff --git a/dialer/android/gradle.properties b/dialer/android/gradle.properties
index 8371d42..d009fe7 100644
--- a/dialer/android/gradle.properties
+++ b/dialer/android/gradle.properties
@@ -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
diff --git a/dialer/lib/features/home/home_page.dart b/dialer/lib/features/home/home_page.dart
index dbfc84f..629c448 100644
--- a/dialer/lib/features/home/home_page.dart
+++ b/dialer/lib/features/home/home_page.dart
@@ -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
with SingleTickerProviderStateMixin {
late TabController _tabController;
List _allContacts = [];
List _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
}
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
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(
+ padding: MaterialStateProperty.all(
const EdgeInsets.only(
top: 6.0,
bottom: 6.0,
@@ -104,7 +115,8 @@ class _MyHomePageState extends State
_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
color: Colors.grey,
size: 24.0,
),
- shape:
- MaterialStateProperty.all(
+ shape: MaterialStateProperty.all(
RoundedRectangleBorder(
borderRadius: BorderRadius.circular(12.0),
),
@@ -130,9 +141,12 @@ class _MyHomePageState extends State
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
value: 'settings',
child: Text('Settings'),
),
+ const PopupMenuItem(
+ 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
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
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
);
}
}
-
-class MyHomePage extends StatefulWidget {
- const MyHomePage({super.key});
-
- @override
- _MyHomePageState createState() => _MyHomePageState();
-}
diff --git a/dialer/lib/main.dart b/dialer/lib/main.dart
index a9ca957..e3d6aa8 100644
--- a/dialer/lib/main.dart
+++ b/dialer/lib/main.dart
@@ -1,23 +1,138 @@
-import 'package:dialer/features/home/home_page.dart';
+// File: lib/main.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 createState() => _IcingDialerAppState();
+}
+
+class _IcingDialerAppState extends State {
+ // 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 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 'onIncomingCall':
+ final phoneNumber = call.arguments as String? ?? "Unknown";
+ _showIncomingCallDialog(phoneNumber);
+ break;
+ default:
+ print("Unhandled method: ${call.method}");
+ }
+ }
+
+ // 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 requestDefaultDialer() async {
+ try {
+ await platform.invokeMethod('requestDefaultDialer');
+ } catch (e) {
+ print('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...'),
+ ),
);
}
}