{"id":25216384,"url":"https://github.com/rohit-554/ringlr","last_synced_at":"2025-04-05T09:15:25.847Z","repository":{"id":276755379,"uuid":"925692256","full_name":"Rohit-554/Ringlr","owner":"Rohit-554","description":"A KMP library for integrating call features in Android and IOS","archived":false,"fork":false,"pushed_at":"2025-02-27T15:46:03.000Z","size":291,"stargazers_count":4,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"master","last_synced_at":"2025-04-05T09:15:18.951Z","etag":null,"topics":["alpha","android","compose","ios","ios-in-progress","kotlin-multiplatform"],"latest_commit_sha":null,"homepage":"","language":"Kotlin","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/Rohit-554.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":"Contributing.md","funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2025-02-01T14:06:52.000Z","updated_at":"2025-03-01T14:41:22.000Z","dependencies_parsed_at":null,"dependency_job_id":"7b733e90-f4c3-4113-9066-723b2477ebab","html_url":"https://github.com/Rohit-554/Ringlr","commit_stats":null,"previous_names":["rohit-554/ringlr"],"tags_count":1,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Rohit-554%2FRinglr","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Rohit-554%2FRinglr/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Rohit-554%2FRinglr/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/Rohit-554%2FRinglr/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/Rohit-554","download_url":"https://codeload.github.com/Rohit-554/Ringlr/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":247312072,"owners_count":20918344,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["alpha","android","compose","ios","ios-in-progress","kotlin-multiplatform"],"created_at":"2025-02-10T19:17:57.989Z","updated_at":"2025-04-05T09:15:25.793Z","avatar_url":"https://github.com/Rohit-554.png","language":"Kotlin","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Ringlr\n\nRinglr is a cross-platform application designed to handle and integrate phone calls and audio configurations. It leverages platform-specific APIs such as Android's Telecom framework and iOS's CallKit to provide a seamless calling experience.\n\n![ringlr_](https://github.com/user-attachments/assets/65f924e5-8f70-4bd1-87a5-a99eac86eae7)\n\n## Contents\n- [Features](#features)\n- [Upcoming Changes](#upcoming-changes-and-work-left)\n- [Contribution Guidelines](https://github.com/Rohit-554/Ringlr/blob/master/Contributing.md)\n- [Permissions](#permissions)\n- [Implementation Guide](#implementation-guide)\n  - [Local Library Integration](#local-library-integration)\n  - [Initialize the Library](#2-initialize-the-library)\n  - [Permission Handling](#3-permission-handling)\n  - [Making Calls](#4-making-calls)\n  - [Complete App.kt Example](#complete-appkt-example)\n- [CallManager API Reference](#callmanager-api-reference)\n  - [Class Declaration](#class-declaration)\n  - [Constructor Parameters](#constructor-parameters)\n  - [Call Management Functions](#call-management-functions)\n  - [Call State Control](#call-state-control)\n  - [Call Information](#call-information)\n  - [Audio Route Management](#audio-route-management)\n  - [Callback Management](#callback-management)\n  - [Example Usage](#example-usage)\n- [Getting Started](#getting-started)\n  - [Project Structure](#project-structure)\n  - [Installation](#installation)\n- [Important Notes](#important-notes)\n- [Troubleshooting](#troubleshooting)\n- [Building the Project](#building-the-project)\n  \n\n## Features\n\n- Answer and make phone calls\n- Grant permissions \n- Manage call states (mute, hold, end)\n- Configure audio settings\n- Bluetooth support\n\n# Upcoming Changes and Work Left\n\n## In Progress\n- ✨ Writing actual classes for Ios\n\n## Coming Soon\n- ✨ VoIP support\n- ✨ Custom in-app calling\n- ✨ Publishing on Maven Central\n\n## Permissions\n\nThe application can handle these permissions right now:\n\n### Android\n\n- `android.permission.ANSWER_PHONE_CALLS`\n- `android.permission.CALL_PHONE`\n- `android.permission.READ_PHONE_STATE`\n- `android.permission.MANAGE_OWN_CALLS`\n- `android.permission.READ_PHONE_NUMBERS`\n- `android.permission.MODIFY_AUDIO_SETTINGS`\n- `android.permission.RECORD_AUDIO`\n- `android.permission.BLUETOOTH`\n- `android.permission.BLUETOOTH_CONNECT`\n\n# CallManager API Reference\n\nThe `CallManager` class is the core component for handling call-related operations in Ringlr. It implements the `CallManagerInterface` and provides comprehensive call management functionality.\n\n## Class Declaration\n\n```kotlin\nexpect class CallManager(configuration: PlatformConfiguration) : CallManagerInterface\n```\n\n## Constructor Parameters\n\n| Parameter | Type | Description |\n|-----------|------|-------------|\n| `configuration` | `PlatformConfiguration` | Platform-specific configuration for call handling |\n\n## Call Management Functions\n\n### Outgoing Call Control\n\n```kotlin\nsuspend fun startOutgoingCall(\n    number: String,\n    displayName: String,\n    scheme: String\n): CallResult\u003cCall\u003e\n```\nInitiates an outgoing call.\n- `number`: Phone number to call\n- `displayName`: Name to display for the call\n- `scheme`: URI scheme for the call (e.g., \"tel\", \"sip\")\n- Returns: Result containing the Call object if successful\n\n```kotlin\nsuspend fun endCall(callId: String): CallResult\u003cUnit\u003e\n```\nEnds an active call.\n- `callId`: Identifier of the call to end\n\n### Call State Control\n\n```kotlin\nsuspend fun muteCall(callId: String, muted: Boolean): CallResult\u003cUnit\u003e\n```\nControls call muting.\n- `callId`: Identifier of the call\n- `muted`: True to mute, false to unmute\n\n```kotlin\nsuspend fun holdCall(callId: String, onHold: Boolean): CallResult\u003cUnit\u003e\n```\nControls call hold state.\n- `callId`: Identifier of the call\n- `onHold`: True to hold, false to resume\n\n## Call Information\n\n```kotlin\nsuspend fun getCallState(callId: String): CallResult\u003cCallState\u003e\n```\nRetrieves the current state of a call.\n- `callId`: Identifier of the call\n- Returns: Current CallState\n\n```kotlin\nsuspend fun getActiveCalls(): CallResult\u003cList\u003cCall\u003e\u003e\n```\nGets a list of all active calls.\n- Returns: List of active Call objects\n\n## Audio Route Management\n\n```kotlin\nsuspend fun setAudioRoute(route: AudioRoute): CallResult\u003cUnit\u003e\n```\nSets the audio output route.\n- `route`: Desired AudioRoute (e.g., SPEAKER, EARPIECE, BLUETOOTH)\n\n```kotlin\nsuspend fun getCurrentAudioRoute(): CallResult\u003cAudioRoute\u003e\n```\nGets the current audio route.\n- Returns: Current AudioRoute\n\n## Callback Management\n\n```kotlin\nfun registerCallStateCallback(callback: CallStateCallback)\n```\nRegisters a callback for call state changes.\n- `callback`: CallStateCallback implementation to receive updates\n\n```kotlin\nfun unregisterCallStateCallback(callback: CallStateCallback)\n```\nUnregisters a previously registered callback.\n- `callback`: CallStateCallback to unregister\n\n## Example Usage\n\n```kotlin\nval callManager = CallManager(platformConfiguration)\n\n// Start an outgoing call\nval callResult = callManager.startOutgoingCall(\n    number = \"+1234567890\",\n    displayName = \"John Doe\",\n    scheme = \"tel\"\n)\n\n// Handle call state changes\nval callback = object : CallStateCallback {\n    override fun onCallStateChanged(call: Call, state: CallState) {\n        // Handle state change\n    }\n}\ncallManager.registerCallStateCallback(callback)\n\n// End call\ncallResult.onSuccess { call -\u003e\n    callManager.endCall(call.id)\n}\n\n// Clean up\ncallManager.unregisterCallStateCallback(callback)\n```\n## Getting Started\n\n### Project Structure \n```\nringlr/\n├── androidApp/           # Android specific code\n│   ├── src/\n│   │   ├── main/\n│   │   │   ├── java/    # Android implementation\n│   │   │   └── res/     # Android resources\n│   │   └── test/        # Android tests\n├── shared/              # Shared KMM code\n│   ├── src/\n│   │   ├── commonMain/  # Common code\n│   │   ├── androidMain/ # Android-specific implementations\n│   │   └── iosMain/     # iOS-specific implementations\n└── iosApp/              # iOS specific code (planned)\n\n```\n### Installation\n\n1. Clone the repository:\n    ```sh\n    git clone https://github.com/yourusername/ringlr.git\n    ```\n\n# Implementation Guide\n\n### Local Library Integration\n\nRinglr is currently available as a local module. Follow these steps to integrate it into your project:\n\n1. Clone the Ringlr repository:\n```bash\ngit clone https://github.com/Rohit-554/Ringlr.git\n```\n\n2. Import the `shared` module:\n   - Copy the `shared` directory from the cloned repository to your project's root directory\n\n3. Add the module to your project's settings.gradle.kts:\n```kotlin\n// settings.gradle.kts\ninclude(\":shared\")\n```\n\n4. Add the dependency in your app's build.gradle.kts:\n```kotlin\n// app/build.gradle.kts\ndependencies {\n    implementation(project(\":shared\"))\n}\n```\n\n5. Sync your project with Gradle files\n\nNote: Make sure the `shared` module's Gradle configuration is compatible with your project's Gradle version and configuration.\n\n### 2. Initialize the Library\n\nCreate an Application class in your androidMain source set:\n\n```kotlin\nclass MyApplication : Application() {\n    override fun onCreate() {\n        super.onCreate()\n        // Initialize the Platform Configuration\n        PlatformConfiguration.init(this)\n    }\n}\n```\n\nDon't forget to declare your Application class in the AndroidManifest.xml along with the service class:\n\n```xml\n\u003capplication\n    android:name=\".MyApplication\"\n    ...\u003e\n \u003cservice\n            android:name=\".ringlr.callHandler.CallConnectionService\"\n            android:permission=\"android.permission.BIND_TELECOM_CONNECTION_SERVICE\"\n            android:exported=\"true\"\u003e\n            \u003cintent-filter\u003e\n                \u003caction android:name=\"android.telecom.ConnectionService\" /\u003e\n            \u003c/intent-filter\u003e\n        \u003c/service\u003e\n\u003c/application\u003e\n``` \n\n\n\n### 3. Permission Handling\n\nRinglr requires specific permissions to handle calls. Here's how to implement the permission flow in your Compose UI:\n\n#### Basic Permission Setup\n\nin AndroidManifest.xml androidMain[main] (your project manifest) configure what permissions you want\n```xml\n\u003cmanifest xmlns:android=\"http://schemas.android.com/apk/res/android\"\n    xmlns:tools=\"http://schemas.android.com/tools\"\u003e\n    \u003c!-- Essential permissions for call handling --\u003e\n    \u003cuses-permission android:name=\"android.permission.ANSWER_PHONE_CALLS\" /\u003e\n    \u003cuses-permission android:name=\"android.permission.CALL_PHONE\" /\u003e\n    \u003cuses-permission android:name=\"android.permission.READ_PHONE_STATE\" /\u003e\n    \u003cuses-permission android:name=\"android.permission.MANAGE_OWN_CALLS\" /\u003e\n\n    \u003c!-- Permissions for audio handling --\u003e\n    \u003cuses-permission android:name=\"android.permission.MODIFY_AUDIO_SETTINGS\" /\u003e\n    \u003cuses-permission android:name=\"android.permission.RECORD_AUDIO\" /\u003e\n\n    \u003c!-- Bluetooth permissions --\u003e\n    \u003cuses-permission android:name=\"android.permission.BLUETOOTH\" /\u003e\n    \u003cuses-permission android:name=\"android.permission.BLUETOOTH_CONNECT\" /\u003e\n\n    \u003c!-- Feature declarations --\u003e\n    \u003cuses-feature android:name=\"android.hardware.telephony\" android:required=\"true\" /\u003e\n    \u003cuses-feature android:name=\"android.hardware.microphone\" android:required=\"true\" /\u003e\n ...\n```\n\n```kotlin\n@Composable\nfun App() {\n    // Initialize permission controller\n    val factory: PermissionsControllerFactory = rememberPermissionsControllerFactory()\n    val controller: PermissionsController = remember(factory) { \n        factory.createPermissionsController() \n    }\n    \n    // Bind the controller\n    BindEffect(controller)\n    \n    // Your UI content\n}\n```\n\n#### Complete Permission Flow Example\n\nHere's a complete example of handling call permissions and making calls:\n\n```kotlin\n@Composable\nfun CallScreen(\n    phoneNumber: String,\n    platformConfig: PlatformConfig\n) {\n    val scope = rememberCoroutineScope()\n    \n    Button(\n        onClick = {\n            scope.launch {\n                try {\n                    val initialState = controller.getPermissionState(Permission.CALL_PHONE)\n\n                    when (initialState) {\n                        PermissionState.Granted -\u003e {\n                            // Permission already granted, make the call\n                            CallManager(platformConfig).startOutgoingCall(\n                                phoneNumber,\n                                \"Call from KMM\"\n                            )\n                        }\n                        PermissionState.DeniedAlways -\u003e {\n                            return@launch\n                        }\n                        else -\u003e {\n                            // Request permission\n                            handlePermissionRequest(\n                                controller,\n                                phoneNumber,\n                                platformConfig\n                            )\n                        }\n                    }\n                } catch (e: Exception) {\n                    handlePermissionError(e)\n                }\n            }\n        }\n    ) {\n        Text(\"Make Call\")\n    }\n}\n\nprivate suspend fun handlePermissionRequest(\n    controller: PermissionsController,\n    phoneNumber: String,\n    platformConfig: PlatformConfig\n) {\n    try {\n        controller.providePermission(Permission.CALL_PHONE)\n        \n        when (controller.getPermissionState(Permission.CALL_PHONE)) {\n            PermissionState.Granted -\u003e {\n                CallManager(platformConfig).startOutgoingCall(\n                    phoneNumber,\n                    \"Call from KMM\"\n                )\n            }\n            PermissionState.DeniedAlways -\u003e {\n                return\n            }\n            else -\u003e {\n                return\n            }\n        }\n    } catch (e: Exception) {\n        handlePermissionError(e)\n    }\n}\n\nprivate fun handlePermissionError(error: Exception) {\n    // Handle errors according to your app's needs\n    when (error) {\n        is DeniedAlwaysException -\u003e { /* Handle permanent denial */ }\n        is DeniedException -\u003e { /* Handle temporary denial */ }\n        else -\u003e { /* Handle other errors */ }\n    }\n}\n```\n\n### 4. Making Calls\n\nOnce permissions are granted, you can use the `CallManager` to initiate calls:\n\n```kotlin\nCallManager(platformConfig).startOutgoingCall(\n    phoneNumber = phoneNumber,    // The phone number to call\n    displayName = \"Call from KMM\" // Display name shown on the call screen\n)\n```\n\nThis will launch the default system dialer app to make the call.\n\n### Complete App.kt Example\n\nHere's a complete example of how your App.kt might look:\n\n```kotlin\n@Composable\n@Preview\nfun App() {\n    val isToastTapped = remember { mutableStateOf(false) }\n    MaterialTheme {\n            Scaffold(\n                Modifier.fillMaxSize()\n            ) {\n                val platformConfig = PlatformConfiguration.create()\n                platformConfig.initializeCallConfiguration()\n                val scope = rememberCoroutineScope()\n\n                val factory: PermissionsControllerFactory = rememberPermissionsControllerFactory()\n                val controller: PermissionsController = remember(factory) { factory.createPermissionsController() }\n                BindEffect(controller)\n                Column(\n                    modifier = Modifier.fillMaxSize(),\n                    horizontalAlignment = Alignment.CenterHorizontally,\n                    verticalArrangement = Arrangement.Center\n                ) {\n                    var phoneNumber by remember { mutableStateOf(\"\") }\n\n                    TextField(\n                        value = phoneNumber,\n                        onValueChange = { phoneNumber = it },\n                        label = { Text(\"Enter phone number\") },\n                        modifier = Modifier.fillMaxWidth().padding(16.dp)\n                    )\n\n                    Button(\n                        onClick = {\n                            scope.launch {\n                                try {\n                                    val initialState = controller.getPermissionState(Permission.CALL_PHONE)\n\n                                    when (initialState) {\n                                        PermissionState.Granted -\u003e {\n                                            // Permission already granted, proceed with call\n                                            CallManager(platformConfig).startOutgoingCall(\n                                                phoneNumber,\n                                                \"Call from KMM\"\n                                            )\n                                        }\n                                        PermissionState.DeniedAlways -\u003e {\n                                           // print denied always\n                                            return@launch\n                                        }\n                                        else -\u003e {\n                                            // Request permission\n                                            try {\n                                                controller.providePermission(Permission.CALL_PHONE)\n\n                                                // Check result after permission request\n                                                when (controller.getPermissionState(Permission.CALL_PHONE)) {\n                                                    PermissionState.Granted -\u003e {\n                                                        CallManager(platformConfig).startOutgoingCall(\n                                                            phoneNumber,\n                                                            \"Call from KMM\"\n                                                        )\n                                                    }\n                                                    PermissionState.DeniedAlways -\u003e {\n                                                        //print permission denied\n                                                        return@launch\n                                                    }\n                                                    else -\u003e {\n                                                        // print permission required to make calls \n                                                        return@launch\n                                                    }\n                                                }\n                                            } catch (e: DeniedAlwaysException) {\n                                                toastManager.showShortToast(\"Permission permanently denied\")\n                                                return@launch\n                                            } catch (e: DeniedException) {\n                                                toastManager.showShortToast(\"Permission denied\")\n                                                return@launch\n                                            }\n                                        }\n                                    }\n                                } catch (e: DeniedAlwaysException) {\n                                   //print exception\n                                } catch (e: DeniedException) {\n                                    toastManager.showShortToast(\"Permission denied\")\n                                } catch (e: Exception) {\n                                   //print exception\n                                }\n                            }\n                        },\n                        modifier = Modifier.padding(16.dp)\n                    ) {\n                        Text(\"Call\")\n                    }\n                }\n            }\n    }\n}\n\n```\n\n## Important Notes\n\n- The `CallManager` requires valid platform configuration to work properly\n- Always handle permission cases appropriately to ensure good user experience\n- The library will use the system's default dialer app for making calls\n- Make sure to handle all potential exceptions when requesting permissions\n- Test the implementation thoroughly on different Android versions\n\n## Troubleshooting\n\nCommon issues and their solutions:\n\n1. Permission Denied Always:\n   - Guide users to app settings to enable permissions manually\n   - Consider implementing your own user feedback mechanism\n\n2. Call Failed to Initialize:\n   - Verify platform configuration is properly initialized\n   - Check if all required permissions are granted\n\n3. Permission Flow Issues:\n   - Ensure `BindEffect` is called with the controller\n   - Verify the permission state handling in all cases\n\n### Building the Project\n\nTo build the project, use the following Gradle command:\n```sh\n./gradlew build\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Frohit-554%2Fringlr","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Frohit-554%2Fringlr","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Frohit-554%2Fringlr/lists"}