diff --git a/.gitea/workflows/apk.yaml b/.gitea/workflows/apk.yaml
index db63776..5ddefac 100644
--- a/.gitea/workflows/apk.yaml
+++ b/.gitea/workflows/apk.yaml
@@ -10,8 +10,22 @@ jobs:
- uses: actions/checkout@v1
with:
subpath: dialer/
- - uses: icing/flutter@main
+ - 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
diff --git a/dialer/android/.gitignore b/dialer/android/.gitignore
index e6d71b3..ebc61c7 100644
--- a/dialer/android/.gitignore
+++ b/dialer/android/.gitignore
@@ -7,6 +7,7 @@ gradle-wrapper.jar
/gradle.properties
GeneratedPluginRegistrant.java
gradle.properties
+.cxx
# Remember to never publicly share your keystore.
# See https://flutter.dev/to/reference-keystore
diff --git a/dialer/android/app/src/main/AndroidManifest.xml b/dialer/android/app/src/main/AndroidManifest.xml
index e0de6a4..95b4a91 100644
--- a/dialer/android/app/src/main/AndroidManifest.xml
+++ b/dialer/android/app/src/main/AndroidManifest.xml
@@ -1,13 +1,17 @@
-
-
+
+
+
+
-
-
-
+
+
+
+
+
-
+ android:name="io.flutter.embedding.android.NormalTheme"
+ android:resource="@style/NormalTheme" />
-
-
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
-
-
-
+
+
-
+
\ No newline at end of file
diff --git a/dialer/android/app/src/main/kotlin/com/icing/dialer/activities/MainActivity.kt b/dialer/android/app/src/main/kotlin/com/icing/dialer/activities/MainActivity.kt
index a2397d6..2fe4eb9 100644
--- a/dialer/android/app/src/main/kotlin/com/icing/dialer/activities/MainActivity.kt
+++ b/dialer/android/app/src/main/kotlin/com/icing/dialer/activities/MainActivity.kt
@@ -1,54 +1,91 @@
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.net.Uri
+import android.os.Build
import android.os.Bundle
import android.provider.CallLog
-import io.flutter.embedding.android.FlutterActivity
-import io.flutter.embedding.engine.FlutterEngine
-import io.flutter.plugin.common.MethodCall
-import io.flutter.plugin.common.MethodChannel
+import 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() {
- // Existing channel for keystore operations.
+class MainActivity : FlutterActivity() {
private val KEYSTORE_CHANNEL = "com.example.keystore"
- // New channel for call log access.
private val CALLLOG_CHANNEL = "com.example.calllog"
-
private val CALL_CHANNEL = "call_service"
+ private val TAG = "MainActivity"
+ private val REQUEST_CODE_SET_DEFAULT_DIALER = 1001
+
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ Log.d(TAG, "onCreate started")
+ Log.d(TAG, "Waiting for Flutter to signal permissions")
+ }
override fun configureFlutterEngine(flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
+ Log.d(TAG, "Configuring Flutter engine")
- // Call service channel
- MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CALL_CHANNEL).setMethodCallHandler { call, result ->
- when (call.method) {
- "makeGsmCall" -> {
- val phoneNumber = call.argument("phoneNumber")
- if (phoneNumber != null) {
- CallService.makeGsmCall(this, phoneNumber)
- result.success("Calling $phoneNumber")
- } else {
- result.error("INVALID_PHONE_NUMBER", "Phone number is required", null)
- }
- }
- "hangUpCall" -> {
- CallService.hangUpCall(this)
- result.success("Call ended")
- }
- else -> result.notImplemented()
- }
- }
-
- // Set up the keystore channel.
- MethodChannel(flutterEngine.dartExecutor.binaryMessenger, KEYSTORE_CHANNEL)
+ MyInCallService.channel = MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CALL_CHANNEL)
+ MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CALL_CHANNEL)
.setMethodCallHandler { call, result ->
- // Delegate method calls to KeystoreHelper.
- KeystoreHelper(call, result).handleMethodCall()
+ when (call.method) {
+ "permissionsGranted" -> {
+ Log.d(TAG, "Received permissionsGranted from Flutter")
+ checkAndRequestDefaultDialer()
+ result.success(true)
+ }
+ "makeGsmCall" -> {
+ val phoneNumber = call.argument("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 = CallService.hangUpCall(this)
+ if (success) {
+ result.success(mapOf("status" to "ended"))
+ } else {
+ result.error("HANGUP_FAILED", "Failed to end call", null)
+ }
+ }
+ "answerCall" -> {
+ val success = MyInCallService.currentCall?.let {
+ it.answer(0) // 0 for default video state (audio-only)
+ Log.d(TAG, "Answered call")
+ true
+ } ?: false
+ if (success) {
+ result.success(mapOf("status" to "answered"))
+ } else {
+ result.error("ANSWER_FAILED", "No active call to answer", null)
+ }
+ }
+ else -> result.notImplemented()
+ }
}
- // Set up the call log channel.
+ 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") {
@@ -60,35 +97,78 @@ class MainActivity: FlutterActivity() {
}
}
- /**
- * Queries the Android call log and returns a list of maps.
- * Each map contains keys: "number", "type", "date", and "duration".
- */
+ 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 {
+ Log.d(TAG, "RoleManager: Available=${roleManager.isRoleAvailable(RoleManager.ROLE_DIALER)}, Held=${roleManager.isRoleHeld(RoleManager.ROLE_DIALER)}")
+ }
+ } 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()
+ }
+ }
+ }
+
private fun getCallLogs(): List