Compare commits
2 Commits
dev
...
WIP-PhoneC
Author | SHA1 | Date | |
---|---|---|---|
1867c025fd | |||
90cea674d4 |
@ -1,52 +1,49 @@
|
|||||||
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
|
||||||
<uses-permission android:name="android.permission.READ_CONTACTS"/>
|
<uses-permission android:name="android.permission.READ_CONTACTS"/>
|
||||||
<uses-permission android:name="android.permission.WRITE_CONTACTS"/>
|
<uses-permission android:name="android.permission.WRITE_CONTACTS"/>
|
||||||
<uses-permission android:name="android.permission.CALL_PHONE" />
|
<uses-permission android:name="android.permission.READ_PHONE_STATE"/>
|
||||||
<uses-permission android:name="android.permission.SEND_SMS" />
|
<uses-permission android:name="android.permission.CALL_PHONE"/>
|
||||||
<uses-permission android:name="android.permission.READ_BLOCKED_NUMBERS" />
|
<uses-permission android:name="android.permission.ANSWER_PHONE_CALLS"/>
|
||||||
<uses-permission android:name="android.permission.WRITE_BLOCKED_NUMBERS" />
|
<uses-permission android:name="android.permission.SEND_SMS"/>
|
||||||
<application
|
<uses-permission android:name="android.permission.READ_BLOCKED_NUMBERS"/>
|
||||||
android:label="Icing Dialer"
|
<uses-permission android:name="android.permission.WRITE_BLOCKED_NUMBERS"/>
|
||||||
android:name="${applicationName}"
|
<uses-permission android:name="android.permission.MANAGE_OWN_CALLS"/>
|
||||||
android:icon="@mipmap/ic_launcher"
|
<uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
|
||||||
android:enableOnBackInvokedCallback="true">
|
<uses-permission android:name="android.permission.BIND_TELECOM_CONNECTION_SERVICE" />
|
||||||
<activity
|
<application android:label="Icing Dialer" android:name="${applicationName}" android:icon="@mipmap/ic_launcher" android:enableOnBackInvokedCallback="true">
|
||||||
android:name=".MainActivity"
|
<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">
|
||||||
android:exported="true"
|
<!-- Specifies an Android theme to apply to this Activity as soon as
|
||||||
android:launchMode="singleTop"
|
the Android process has started. This theme is visible to the user
|
||||||
android:taskAffinity=""
|
while the Flutter UI initializes. After that, this theme continues
|
||||||
android:theme="@style/LaunchTheme"
|
to determine the Window background behind the Flutter UI. -->
|
||||||
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
|
<meta-data android:name="io.flutter.embedding.android.NormalTheme" android:resource="@style/NormalTheme"/>
|
||||||
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>
|
<intent-filter>
|
||||||
<action android:name="android.intent.action.MAIN"/>
|
<action android:name="android.intent.action.MAIN" />
|
||||||
<category android:name="android.intent.category.LAUNCHER"/>
|
<category android:name="android.intent.category.LAUNCHER" />
|
||||||
|
</intent-filter>
|
||||||
|
<intent-filter>
|
||||||
|
<action android:name="android.intent.action.DIAL" />
|
||||||
|
<category android:name="android.intent.category.DEFAULT" />
|
||||||
</intent-filter>
|
</intent-filter>
|
||||||
</activity>
|
</activity>
|
||||||
<!-- Don't delete the meta-data below.
|
<service android:name=".MyConnectionService" android:label="My Connection Service" android:permission="android.permission.BIND_TELECOM_CONNECTION_SERVICE" android:exported="true">
|
||||||
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
|
<intent-filter>
|
||||||
<meta-data
|
<action android:name="android.telecom.ConnectionService"/>
|
||||||
android:name="flutterEmbedding"
|
</intent-filter>
|
||||||
android:value="2" />
|
</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>
|
</application>
|
||||||
<!-- Required to query activities that can process text, see:
|
<!-- Required to query activities that can process text, see:
|
||||||
https://developer.android.com/training/package-visibility and
|
https://developer.android.com/training/package-visibility and
|
||||||
https://developer.android.com/reference/android/content/Intent#ACTION_PROCESS_TEXT.
|
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>
|
<queries>
|
||||||
<intent>
|
<intent>
|
||||||
<action android:name="android.intent.action.PROCESS_TEXT"/>
|
<action android:name="android.intent.action.PROCESS_TEXT"/>
|
||||||
<data android:mimeType="text/plain"/>
|
<data android:mimeType="text/plain"/>
|
||||||
</intent>
|
</intent>
|
||||||
</queries>
|
</queries>
|
||||||
</manifest>
|
</manifest>
|
@ -1,5 +1,218 @@
|
|||||||
package com.example.dialer
|
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.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")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -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()
|
||||||
|
}
|
||||||
|
}
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
@ -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 {
|
allprojects {
|
||||||
repositories {
|
repositories {
|
||||||
google()
|
google()
|
||||||
@ -5,15 +23,18 @@ allprojects {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If you want to customize build directory paths:
|
||||||
rootProject.buildDir = "../build"
|
rootProject.buildDir = "../build"
|
||||||
subprojects {
|
subprojects {
|
||||||
project.buildDir = "${rootProject.buildDir}/${project.name}"
|
project.buildDir = "${rootProject.buildDir}/${project.name}"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// This is optional, depends on your workflow:
|
||||||
subprojects {
|
subprojects {
|
||||||
project.evaluationDependsOn(":app")
|
project.evaluationDependsOn(":app")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A custom clean task:
|
||||||
tasks.register("clean", Delete) {
|
tasks.register("clean", Delete) {
|
||||||
delete rootProject.buildDir
|
delete rootProject.buildDir
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2,3 +2,4 @@ org.gradle.jvmargs=-Xmx4G -XX:MaxMetaspaceSize=2G -XX:+HeapDumpOnOutOfMemoryErro
|
|||||||
android.useAndroidX=true
|
android.useAndroidX=true
|
||||||
android.enableJetifier=true
|
android.enableJetifier=true
|
||||||
dev.steenbakker.mobile_scanner.useUnbundled=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
|
||||||
|
@ -1,24 +1,41 @@
|
|||||||
import 'package:flutter/material.dart';
|
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/contacts/contact_page.dart';
|
||||||
import 'package:dialer/features/favorites/favorites_page.dart';
|
import 'package:dialer/features/favorites/favorites_page.dart';
|
||||||
import 'package:dialer/features/history/history_page.dart';
|
import 'package:dialer/features/history/history_page.dart';
|
||||||
import 'package:dialer/features/composition/composition.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';
|
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>
|
class _MyHomePageState extends State<MyHomePage>
|
||||||
with SingleTickerProviderStateMixin {
|
with SingleTickerProviderStateMixin {
|
||||||
late TabController _tabController;
|
late TabController _tabController;
|
||||||
List<Contact> _allContacts = [];
|
List<Contact> _allContacts = [];
|
||||||
List<Contact> _contactSuggestions = [];
|
List<Contact> _contactSuggestions = [];
|
||||||
final ContactService _contactService = ContactService();
|
|
||||||
|
|
||||||
|
final ContactService _contactService = ContactService();
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void initState() {
|
void initState() {
|
||||||
super.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 = TabController(length: 3, vsync: this, initialIndex: 1);
|
||||||
_tabController.addListener(_handleTabIndex);
|
_tabController.addListener(_handleTabIndex);
|
||||||
_fetchContacts();
|
_fetchContacts();
|
||||||
@ -30,19 +47,14 @@ class _MyHomePageState extends State<MyHomePage>
|
|||||||
}
|
}
|
||||||
|
|
||||||
void _onSearchChanged(String query) {
|
void _onSearchChanged(String query) {
|
||||||
print("Search query: $query");
|
if (query.isEmpty) {
|
||||||
|
_contactSuggestions = List.from(_allContacts);
|
||||||
setState(() {
|
} else {
|
||||||
if (query.isEmpty) {
|
_contactSuggestions = _allContacts.where((contact) {
|
||||||
_contactSuggestions = List.from(_allContacts);
|
return contact.displayName.toLowerCase().contains(query.toLowerCase());
|
||||||
} else {
|
}).toList();
|
||||||
_contactSuggestions = _allContacts.where((contact) {
|
}
|
||||||
return contact.displayName
|
setState(() {});
|
||||||
.toLowerCase()
|
|
||||||
.contains(query.toLowerCase());
|
|
||||||
}).toList();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@override
|
@override
|
||||||
@ -81,17 +93,16 @@ class _MyHomePageState extends State<MyHomePage>
|
|||||||
top: BorderSide(color: Colors.grey.shade800, width: 1),
|
top: BorderSide(color: Colors.grey.shade800, width: 1),
|
||||||
left: BorderSide(color: Colors.grey.shade800, width: 1),
|
left: BorderSide(color: Colors.grey.shade800, width: 1),
|
||||||
right: BorderSide(color: Colors.grey.shade800, width: 1),
|
right: BorderSide(color: Colors.grey.shade800, width: 1),
|
||||||
bottom:
|
bottom: BorderSide(color: Colors.grey.shade800, width: 2),
|
||||||
BorderSide(color: Colors.grey.shade800, width: 2),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
// Using Flutter 3.10+ SearchAnchor / SearchBar API:
|
||||||
child: SearchAnchor(
|
child: SearchAnchor(
|
||||||
builder:
|
builder:
|
||||||
(BuildContext context, SearchController controller) {
|
(BuildContext context, SearchController controller) {
|
||||||
return SearchBar(
|
return SearchBar(
|
||||||
controller: controller,
|
controller: controller,
|
||||||
padding:
|
padding: MaterialStateProperty.all<EdgeInsetsGeometry>(
|
||||||
MaterialStateProperty.all<EdgeInsetsGeometry>(
|
|
||||||
const EdgeInsets.only(
|
const EdgeInsets.only(
|
||||||
top: 6.0,
|
top: 6.0,
|
||||||
bottom: 6.0,
|
bottom: 6.0,
|
||||||
@ -104,7 +115,8 @@ class _MyHomePageState extends State<MyHomePage>
|
|||||||
_onSearchChanged('');
|
_onSearchChanged('');
|
||||||
},
|
},
|
||||||
backgroundColor: MaterialStateProperty.all(
|
backgroundColor: MaterialStateProperty.all(
|
||||||
const Color.fromARGB(255, 30, 30, 30)),
|
const Color.fromARGB(255, 30, 30, 30),
|
||||||
|
),
|
||||||
hintText: 'Search contacts',
|
hintText: 'Search contacts',
|
||||||
hintStyle: MaterialStateProperty.all(
|
hintStyle: MaterialStateProperty.all(
|
||||||
const TextStyle(color: Colors.grey, fontSize: 16.0),
|
const TextStyle(color: Colors.grey, fontSize: 16.0),
|
||||||
@ -114,8 +126,7 @@ class _MyHomePageState extends State<MyHomePage>
|
|||||||
color: Colors.grey,
|
color: Colors.grey,
|
||||||
size: 24.0,
|
size: 24.0,
|
||||||
),
|
),
|
||||||
shape:
|
shape: MaterialStateProperty.all<RoundedRectangleBorder>(
|
||||||
MaterialStateProperty.all<RoundedRectangleBorder>(
|
|
||||||
RoundedRectangleBorder(
|
RoundedRectangleBorder(
|
||||||
borderRadius: BorderRadius.circular(12.0),
|
borderRadius: BorderRadius.circular(12.0),
|
||||||
),
|
),
|
||||||
@ -130,9 +141,12 @@ class _MyHomePageState extends State<MyHomePage>
|
|||||||
return _contactSuggestions.map((contact) {
|
return _contactSuggestions.map((contact) {
|
||||||
return ListTile(
|
return ListTile(
|
||||||
key: ValueKey(contact.id),
|
key: ValueKey(contact.id),
|
||||||
title: Text(contact.displayName,
|
title: Text(
|
||||||
style: const TextStyle(color: Colors.white)),
|
contact.displayName,
|
||||||
|
style: const TextStyle(color: Colors.white),
|
||||||
|
),
|
||||||
onTap: () {
|
onTap: () {
|
||||||
|
// For example, you might call widget.onMakeCall(contact.phoneNumber)
|
||||||
controller.closeView(contact.displayName);
|
controller.closeView(contact.displayName);
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
@ -149,14 +163,20 @@ class _MyHomePageState extends State<MyHomePage>
|
|||||||
value: 'settings',
|
value: 'settings',
|
||||||
child: Text('Settings'),
|
child: Text('Settings'),
|
||||||
),
|
),
|
||||||
|
const PopupMenuItem<String>(
|
||||||
|
value: 'becomeDialer',
|
||||||
|
child: Text('Become Default Dialer'),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
onSelected: (String value) {
|
onSelected: (String value) {
|
||||||
if (value == 'settings') {
|
if (value == 'settings') {
|
||||||
Navigator.push(
|
Navigator.push(
|
||||||
context,
|
context,
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(builder: (context) => const SettingsPage()),
|
||||||
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(),
|
ContactPage(),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
// Floating action button for manual composition/dialpad
|
||||||
Positioned(
|
Positioned(
|
||||||
right: 20,
|
right: 20,
|
||||||
bottom: 20,
|
bottom: 20,
|
||||||
child: FloatingActionButton(
|
child: FloatingActionButton(
|
||||||
onPressed: () {
|
onPressed: () {
|
||||||
|
// Here you could do widget.onMakeCall('123456') or
|
||||||
|
// push to CompositionPage, etc.
|
||||||
Navigator.push(
|
Navigator.push(
|
||||||
context,
|
context,
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
@ -205,17 +228,20 @@ class _MyHomePageState extends State<MyHomePage>
|
|||||||
controller: _tabController,
|
controller: _tabController,
|
||||||
tabs: [
|
tabs: [
|
||||||
Tab(
|
Tab(
|
||||||
icon: Icon(_tabController.index == 0
|
icon: Icon(_tabController.index == 0
|
||||||
? Icons.star
|
? Icons.star
|
||||||
: Icons.star_border)),
|
: Icons.star_border),
|
||||||
|
),
|
||||||
Tab(
|
Tab(
|
||||||
icon: Icon(_tabController.index == 1
|
icon: Icon(_tabController.index == 1
|
||||||
? Icons.access_time_filled
|
? Icons.access_time_filled
|
||||||
: Icons.access_time_outlined)),
|
: Icons.access_time_outlined),
|
||||||
|
),
|
||||||
Tab(
|
Tab(
|
||||||
icon: Icon(_tabController.index == 2
|
icon: Icon(_tabController.index == 2
|
||||||
? Icons.contacts
|
? Icons.contacts
|
||||||
: Icons.contacts_outlined)),
|
: Icons.contacts_outlined),
|
||||||
|
),
|
||||||
],
|
],
|
||||||
labelColor: Colors.white,
|
labelColor: Colors.white,
|
||||||
unselectedLabelColor: const Color.fromARGB(255, 158, 158, 158),
|
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();
|
|
||||||
}
|
|
||||||
|
@ -1,23 +1,167 @@
|
|||||||
import 'package:dialer/features/home/home_page.dart';
|
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
import 'package:dialer/features/contacts/contact_state.dart';
|
import 'package:flutter/services.dart';
|
||||||
|
|
||||||
void main() {
|
void main() {
|
||||||
runApp(const MyApp());
|
runApp(const IcingDialerApp());
|
||||||
}
|
}
|
||||||
|
|
||||||
class MyApp extends StatelessWidget {
|
class IcingDialerApp extends StatefulWidget {
|
||||||
const MyApp({super.key});
|
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
|
@override
|
||||||
Widget build(BuildContext context) {
|
Widget build(BuildContext context) {
|
||||||
return ContactState(
|
return MaterialApp(
|
||||||
child: MaterialApp(
|
title: 'Icing Dialer',
|
||||||
theme: ThemeData(
|
theme: ThemeData.dark(),
|
||||||
brightness: Brightness.dark
|
home: isDefaultDialer
|
||||||
),
|
? const CallScreen()
|
||||||
home: SafeArea(child: MyHomePage()),
|
: 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...'),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in New Issue
Block a user