ub3lal 7 tháng trước cách đây
commit
069e4dc169
100 tập tin đã thay đổi với 5475 bổ sung0 xóa
  1. 12 0
      .gitignore
  2. 3 0
      .idea/.gitignore
  3. 3 0
      .idea/dictionaries/hiennguyen92.xml
  4. 27 0
      .idea/libraries/Dart_SDK.xml
  5. 11 0
      .idea/misc.xml
  6. 8 0
      .idea/modules.xml
  7. 6 0
      .idea/runConfigurations/example_lib_main_dart.xml
  8. 6 0
      .idea/vcs.xml
  9. 10 0
      .metadata
  10. 247 0
      CHANGELOG.md
  11. 7 0
      CMD.md
  12. 22 0
      LICENSE
  13. 89 0
      PUSHKIT.md
  14. 646 0
      README.md
  15. 1 0
      analysis_options.yaml
  16. 8 0
      android/.gitignore
  17. 67 0
      android/build.gradle
  18. 5 0
      android/consumer-rules.pro
  19. 3 0
      android/gradle.properties
  20. 6 0
      android/gradle/wrapper/gradle-wrapper.properties
  21. 1 0
      android/settings.gradle
  22. 60 0
      android/src/main/AndroidManifest.xml
  23. 15 0
      android/src/main/kotlin/com/hiennv/flutter_callkit_incoming/AppUtils.kt
  24. 439 0
      android/src/main/kotlin/com/hiennv/flutter_callkit_incoming/Call.kt
  25. 83 0
      android/src/main/kotlin/com/hiennv/flutter_callkit_incoming/CallkitConstants.kt
  26. 381 0
      android/src/main/kotlin/com/hiennv/flutter_callkit_incoming/CallkitIncomingActivity.kt
  27. 249 0
      android/src/main/kotlin/com/hiennv/flutter_callkit_incoming/CallkitIncomingBroadcastReceiver.kt
  28. 664 0
      android/src/main/kotlin/com/hiennv/flutter_callkit_incoming/CallkitNotificationManager.kt
  29. 170 0
      android/src/main/kotlin/com/hiennv/flutter_callkit_incoming/CallkitSoundPlayerService.kt
  30. 368 0
      android/src/main/kotlin/com/hiennv/flutter_callkit_incoming/FlutterCallkitIncomingPlugin.kt
  31. 267 0
      android/src/main/kotlin/com/hiennv/flutter_callkit_incoming/OngoingNotificationService.kt
  32. 75 0
      android/src/main/kotlin/com/hiennv/flutter_callkit_incoming/SharedPreferencesUtils.kt
  33. 45 0
      android/src/main/kotlin/com/hiennv/flutter_callkit_incoming/TransparentActivity.kt
  34. 82 0
      android/src/main/kotlin/com/hiennv/flutter_callkit_incoming/Utils.kt
  35. 36 0
      android/src/main/kotlin/com/hiennv/flutter_callkit_incoming/widgets/CircleTransform.kt
  36. 146 0
      android/src/main/kotlin/com/hiennv/flutter_callkit_incoming/widgets/RippleRelativeLayout.kt
  37. 9 0
      android/src/main/res/anim/shake_anim.xml
  38. BIN
      android/src/main/res/drawable-hdpi/ic_accept.png
  39. BIN
      android/src/main/res/drawable-hdpi/ic_call_missed.png
  40. BIN
      android/src/main/res/drawable-hdpi/ic_decline.png
  41. BIN
      android/src/main/res/drawable-hdpi/ic_video.png
  42. BIN
      android/src/main/res/drawable-hdpi/ic_video_missed.png
  43. BIN
      android/src/main/res/drawable-mdpi/ic_accept.png
  44. BIN
      android/src/main/res/drawable-mdpi/ic_call_missed.png
  45. BIN
      android/src/main/res/drawable-mdpi/ic_decline.png
  46. BIN
      android/src/main/res/drawable-mdpi/ic_video.png
  47. BIN
      android/src/main/res/drawable-mdpi/ic_video_missed.png
  48. 9 0
      android/src/main/res/drawable-v21/bg_button_accept.xml
  49. 9 0
      android/src/main/res/drawable-v21/bg_button_decline.xml
  50. 10 0
      android/src/main/res/drawable-v21/rounded_button_accept.xml
  51. 10 0
      android/src/main/res/drawable-v21/rounded_button_decline.xml
  52. BIN
      android/src/main/res/drawable-xhdpi/ic_accept.png
  53. BIN
      android/src/main/res/drawable-xhdpi/ic_call_missed.png
  54. BIN
      android/src/main/res/drawable-xhdpi/ic_decline.png
  55. BIN
      android/src/main/res/drawable-xhdpi/ic_video.png
  56. BIN
      android/src/main/res/drawable-xhdpi/ic_video_missed.png
  57. BIN
      android/src/main/res/drawable-xxhdpi/ic_accept.png
  58. BIN
      android/src/main/res/drawable-xxhdpi/ic_call_missed.png
  59. BIN
      android/src/main/res/drawable-xxhdpi/ic_decline.png
  60. BIN
      android/src/main/res/drawable-xxhdpi/ic_video.png
  61. BIN
      android/src/main/res/drawable-xxhdpi/ic_video_missed.png
  62. BIN
      android/src/main/res/drawable-xxxhdpi/ic_accept.png
  63. BIN
      android/src/main/res/drawable-xxxhdpi/ic_call_missed.png
  64. BIN
      android/src/main/res/drawable-xxxhdpi/ic_decline.png
  65. BIN
      android/src/main/res/drawable-xxxhdpi/ic_default_avatar.png
  66. BIN
      android/src/main/res/drawable-xxxhdpi/ic_logo.png
  67. BIN
      android/src/main/res/drawable-xxxhdpi/ic_video.png
  68. BIN
      android/src/main/res/drawable-xxxhdpi/ic_video_missed.png
  69. 5 0
      android/src/main/res/drawable/bg_button_accept.xml
  70. 5 0
      android/src/main/res/drawable/bg_button_decline.xml
  71. 6 0
      android/src/main/res/drawable/rounded_button_accept.xml
  72. 6 0
      android/src/main/res/drawable/rounded_button_decline.xml
  73. BIN
      android/src/main/res/drawable/transparent.png
  74. 201 0
      android/src/main/res/layout-w600dp-land/activity_callkit_incoming.xml
  75. 206 0
      android/src/main/res/layout/activity_callkit_incoming.xml
  76. 90 0
      android/src/main/res/layout/layout_custom_miss_notification.xml
  77. 131 0
      android/src/main/res/layout/layout_custom_notification.xml
  78. 103 0
      android/src/main/res/layout/layout_custom_ongoing_notification.xml
  79. 143 0
      android/src/main/res/layout/layout_custom_small_ex_notification.xml
  80. 138 0
      android/src/main/res/layout/layout_custom_small_notification.xml
  81. BIN
      android/src/main/res/raw/ringtone_default.mp3
  82. 4 0
      android/src/main/res/values-sw600dp/dimens.xml
  83. 11 0
      android/src/main/res/values/attrs.xml
  84. 11 0
      android/src/main/res/values/colors.xml
  85. 25 0
      android/src/main/res/values/dimens.xml
  86. 12 0
      android/src/main/res/values/strings.xml
  87. 31 0
      android/src/main/res/values/styles.xml
  88. 32 0
      flutter_callkit_incoming.iml
  89. BIN
      images/KeychainAccess.png
  90. BIN
      images/Setting.png
  91. BIN
      images/TestingApp.png
  92. BIN
      images/VoIPServicesCertificate.png
  93. BIN
      images/Xcode-S1.png
  94. BIN
      images/Xcode-S2.png
  95. BIN
      images/Xcode-S3.png
  96. BIN
      images/image1.png
  97. BIN
      images/image2.png
  98. BIN
      images/image3.png
  99. BIN
      images/image4.jpg
  100. BIN
      images/image5.jpg

+ 12 - 0
.gitignore

@@ -0,0 +1,12 @@
+.DS_Store
+.dart_tool/
+
+.packages
+.pub/
+
+build/
+
+pubspec.lock
+
+example/ios/Podfile.lock
+example/pubspec.lock

+ 3 - 0
.idea/.gitignore

@@ -0,0 +1,3 @@
+# Default ignored files
+/shelf/
+/workspace.xml

+ 3 - 0
.idea/dictionaries/hiennguyen92.xml

@@ -0,0 +1,3 @@
+<component name="ProjectDictionaryState">
+  <dictionary name="hiennguyen92" />
+</component>

+ 27 - 0
.idea/libraries/Dart_SDK.xml

@@ -0,0 +1,27 @@
+<component name="libraryTable">
+  <library name="Dart SDK">
+    <CLASSES>
+      <root url="file://$PROJECT_DIR$/../Development/flutter/bin/cache/dart-sdk/lib/async" />
+      <root url="file://$PROJECT_DIR$/../Development/flutter/bin/cache/dart-sdk/lib/cli" />
+      <root url="file://$PROJECT_DIR$/../Development/flutter/bin/cache/dart-sdk/lib/collection" />
+      <root url="file://$PROJECT_DIR$/../Development/flutter/bin/cache/dart-sdk/lib/convert" />
+      <root url="file://$PROJECT_DIR$/../Development/flutter/bin/cache/dart-sdk/lib/core" />
+      <root url="file://$PROJECT_DIR$/../Development/flutter/bin/cache/dart-sdk/lib/developer" />
+      <root url="file://$PROJECT_DIR$/../Development/flutter/bin/cache/dart-sdk/lib/ffi" />
+      <root url="file://$PROJECT_DIR$/../Development/flutter/bin/cache/dart-sdk/lib/html" />
+      <root url="file://$PROJECT_DIR$/../Development/flutter/bin/cache/dart-sdk/lib/indexed_db" />
+      <root url="file://$PROJECT_DIR$/../Development/flutter/bin/cache/dart-sdk/lib/io" />
+      <root url="file://$PROJECT_DIR$/../Development/flutter/bin/cache/dart-sdk/lib/isolate" />
+      <root url="file://$PROJECT_DIR$/../Development/flutter/bin/cache/dart-sdk/lib/js" />
+      <root url="file://$PROJECT_DIR$/../Development/flutter/bin/cache/dart-sdk/lib/js_util" />
+      <root url="file://$PROJECT_DIR$/../Development/flutter/bin/cache/dart-sdk/lib/math" />
+      <root url="file://$PROJECT_DIR$/../Development/flutter/bin/cache/dart-sdk/lib/mirrors" />
+      <root url="file://$PROJECT_DIR$/../Development/flutter/bin/cache/dart-sdk/lib/svg" />
+      <root url="file://$PROJECT_DIR$/../Development/flutter/bin/cache/dart-sdk/lib/typed_data" />
+      <root url="file://$PROJECT_DIR$/../Development/flutter/bin/cache/dart-sdk/lib/web_audio" />
+      <root url="file://$PROJECT_DIR$/../Development/flutter/bin/cache/dart-sdk/lib/web_gl" />
+    </CLASSES>
+    <JAVADOC />
+    <SOURCES />
+  </library>
+</component>

+ 11 - 0
.idea/misc.xml

@@ -0,0 +1,11 @@
+<project version="4">
+  <component name="FrameworkDetectionExcludesConfiguration">
+    <type id="android" />
+  </component>
+  <component name="ProjectRootManager">
+    <output url="file://$PROJECT_DIR$/out" />
+  </component>
+  <component name="ProjectType">
+    <option name="id" value="io.flutter" />
+  </component>
+</project>

+ 8 - 0
.idea/modules.xml

@@ -0,0 +1,8 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="ProjectModuleManager">
+    <modules>
+      <module fileurl="file://$PROJECT_DIR$/flutter_callkit_incoming.iml" filepath="$PROJECT_DIR$/flutter_callkit_incoming.iml" />
+    </modules>
+  </component>
+</project>

+ 6 - 0
.idea/runConfigurations/example_lib_main_dart.xml

@@ -0,0 +1,6 @@
+<component name="ProjectRunConfigurationManager">
+  <configuration default="false" name="example/lib/main.dart" type="FlutterRunConfigurationType" factoryName="Flutter">
+    <option name="filePath" value="$PROJECT_DIR$/example/lib/main.dart" />
+    <method />
+  </configuration>
+</component>

+ 6 - 0
.idea/vcs.xml

@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project version="4">
+  <component name="VcsDirectoryMappings">
+    <mapping directory="" vcs="Git" />
+  </component>
+</project>

+ 10 - 0
.metadata

@@ -0,0 +1,10 @@
+# This file tracks properties of this Flutter project.
+# Used by Flutter tool to assess capabilities and perform upgrades etc.
+#
+# This file should be version controlled and should not be manually edited.
+
+version:
+  revision: f4abaa0735eba4dfd8f33f73363911d63931fe03
+  channel: stable
+
+project_type: plugin

+ 247 - 0
CHANGELOG.md

@@ -0,0 +1,247 @@
+## 2.5.2
+* Add notification calling for Android `callingNotification`, thank @ebsangam https://github.com/hiennguyen92/flutter_callkit_incoming/pull/662
+* Add `logoUrl` properties (inside android prop) 
+* Fixed issue DMTF IOS, thank @minn-ee https://github.com/hiennguyen92/flutter_callkit_incoming/issues/577
+* Fixed issue duplicate missing notification Android
+* Fixed some bugs.
+
+## 2.5.1
+* Fix issue security Android, thanks @datpt11 https://github.com/hiennguyen92/flutter_callkit_incoming/issues/651
+
+## 2.5.0
+* update jvmToolchain(17) for Android
+
+## 2.0.4+2
+* add func `requestFullIntentPermission` (Android 14+) thank @Spyspyspy https://github.com/hiennguyen92/flutter_callkit_incoming/pull/584
+* set Notification call style (Android) thank @AAkira https://github.com/hiennguyen92/flutter_callkit_incoming/pull/553
+* Many other issues
+    1. add prop `accepted` in activeCalls (iOS) thank @vasilich6107
+
+## 2.0.4+1
+* Removed `Telecom Framework` (Android)
+
+## 2.0.4
+* Removed `Telecom Framework` (Android)
+* Fixed hide notification for action `CallBack` (Android)
+
+## 2.0.3
+* Fixed linked func `hideCallkitIncoming`
+
+## 2.0.2+2
+* Fixed linked func `hideCallkitIncoming`
+
+## 2.0.2+1
+* Fixed linked func `hideCallkitIncoming`
+
+## 2.0.2
+* Add func `hideCallkitIncoming` clear the incoming notification/ring (after accept/decline/timeout)
+* Add props `isShowFullLockedScreen` on Android
+* Fixed example/Fixed update android 14
+
+## 2.0.1+2
+* Add Action for onDecline
+* Add Action for onEnd
+* add android props `isShowCallID`
+
+## 2.0.1+1
+* Add Callback AVAudioSession for WebRTC setup
+* Fix issue no audio for using WebRTC
+
+## 2.0.1-dev.2
+* Add Action for onAccept
+
+## 2.0.1-dev.1
+* Add AVAudioSession Appdelegate(iOS)
+
+## 2.0.1-dev
+* Add AVAudioSession Appdelegate(iOS)
+
+## 2.0.1
+
+* Fixed some bugs.
+* `Android` using Telecom Framework
+* Add `silenceEvents`
+* Add `normalHandle` props https://github.com/hiennguyen92/flutter_callkit_incoming/pull/403
+* Android add `textColor` props https://github.com/hiennguyen92/flutter_callkit_incoming/pull/398
+* Android invisible avatar for default https://github.com/hiennguyen92/flutter_callkit_incoming/pull/393
+* Add Method for call API when accept/decline/end/timeout
+
+## 2.0.0+2
+
+* Fixed some bugs.
+* Support request permission for Android 13+ `requestNotificationPermission`
+
+## 2.0.0+1
+
+* Fixed some bugs.
+* Add `landscape` for tablet
+* Fix issue head-up for redmi / xiaomi devices
+
+## 2.0.0
+
+* Fixed some bugs.
+* Adapt flutter_lints and use lowerCamelCase to Event enum
+* Rename properties 
+        `textMisssedCall` -> `subtitle`,
+        `textCallback` -> `callbackText`,
+        `isShowMissedCallNotification` -> `showNotification`,
+* Move inside properties `missedCallNotification {showNotification, isShowCallback, subtitle, callbackText}`
+* Add setCallConnected option iOS `await FlutterCallkitIncoming.setCallConnected(this._currentUuid)`
+* Add hold option iOS
+* Add mute call option iOS
+* Many other issues
+    1. Thank @ryojiro
+    https://github.com/hiennguyen92/flutter_callkit_incoming/pull/263
+    https://github.com/hiennguyen92/flutter_callkit_incoming/pull/264
+    https://github.com/hiennguyen92/flutter_callkit_incoming/pull/262
+    2. Many Thank @mouEsam
+    https://github.com/hiennguyen92/flutter_callkit_incoming/pull/227
+    3. ...
+
+
+## 1.0.3+3
+
+* Update README.md
+* Fixed some bugs.
+
+## 1.0.3+2
+
+* REMOVED
+
+## 1.0.3+1
+
+* Dart class models instead using dynamic types and Maps (thank @icodelifee - https://github.com/hiennguyen92/flutter_callkit_incoming/pull/180)
+* Allow to call from native Android (thank @fabiocody - https://github.com/hiennguyen92/flutter_callkit_incoming/pull/185)
+* Add android notification channel name `incomingCallNotificationChannelName` `missedCallNotificationChannelName` (thank @AAkira - https://github.com/hiennguyen92/flutter_callkit_incoming/pull/177)
+* Adding the feature to change template of notification to small `isCustomSmallExNotification` (thank @anocean2 - https://github.com/hiennguyen92/flutter_callkit_incoming/pull/196)
+* Fixed ringtone sound not playing in Release mode on Android (thank @mschudt - https://github.com/hiennguyen92/flutter_callkit_incoming/pull/204)
+* Fixed some bugs.
+
+## 1.0.3
+
+* REMOVED
+
+## 1.0.2+2
+
+* Fix notification Android 12
+* Fix sound notification
+* Support `backgroundUrl` using path assets
+* Fixed some bugs.
+
+## 1.0.2+1
+
+* Issue no audio when Accept(iOS)
+* Duplicate sound notification(Android)
+* Support Flutter 3
+* Fixed some bugs.
+
+## 1.0.2
+
+* Fixed issue open app(terminated/background state - Android).
+* Completed Example  
+* Fixed some bugs.
+
+## 1.0.1+8
+
+* Add props `isShowMissedCallNotification` using show Missed call notification(Android)
+* Fixed issue decline(terminated/background state - there will be about 3 seconds to call the api before the app is closed.)
+* Fixed some bugs.
+
+## 1.0.1+7
+
+* Fixed issue open app(terminated/background state).
+* Fixed some bugs.
+
+## 1.0.1+6
+
+* Add props for text
+* Fixed issue open app(terminated/background state).
+* Fixed some bugs.
+
+## 1.0.1+5
+
+* Update custom miss call notification
+* Fixed issue open app(terminated/background state).
+
+## 1.0.1+4
+
+* add `showMissCallNotification` only for Android, using show miss call notification 
+* Fixed some bugs.
+
+## 1.0.1+3
+
+* add props `isShowCallback` using show Callback for miss call(Android)
+* public props data call for Object-C/Swift
+* Example using FCM(Android)
+* Fixed some bugs.
+
+## 1.0.1+2
+
+* Fixed issue default ringtone(Android)
+* Fixed issue vibration(Android)
+* Fixed issue sound play type ringtone volumn system(Android)
+* Fixed flow incomming screen(Android)
+* Fixed some bugs.
+
+## 1.0.1+1
+
+* Switch using Service for Ringtone(Android)
+* Fixed issue vibration(Android)
+* Add `getDevicePushTokenVoIP()` feature
+* Fixed some bugs.
+
+## 1.0.1
+
+* Pustkit and VoIP setup instructions (PUSHKIT.md)
+* Callback from Recent History IOS
+* Using System ringtone for default
+* Fixed func `endAllCalls()` Android
+* Bugs Android 12.
+* Fixed some bugs.
+
+## 1.0.0+8
+
+* Share func call from native(iOS)
+
+## 1.0.0+7
+
+* Add custom `headers` using for avatar/background image (only for Android)
+
+## 1.0.0+6
+
+* Fixed func `activeCalls()` Get active calls
+
+## 1.0.0+5
+
+* Fixed endCall
+* Bugs Targeting Android 12 (Android).
+* Bugs `audio session` (iOS)
+* Fixed some bugs.
+
+## 1.0.0+4
+
+* Update README.md.
+* Add func `activeCalls()` Get active calls
+* Remove notification when click action `Call back` (Android).
+* Bugs `no activation of the audio session` (iOS)
+* Fixed some bugs.
+
+## 1.0.0+3
+
+* Update README.md.
+* Add props android `isShowLogo`.
+* Fixed some bugs.
+
+## 1.0.0+2
+
+* Update README.md.
+* Update documentation.
+
+## 1.0.0+1
+
+* Update README.md.
+* Fixed some bugs.
+
+## 1.0.0
+
+* Initial release.

+ 7 - 0
CMD.md

@@ -0,0 +1,7 @@
+```
+flutter pub publish --dry-run
+```
+
+```
+flutter pub publish
+```

+ 22 - 0
LICENSE

@@ -0,0 +1,22 @@
+
+MIT License
+
+Copyright (c) 2021 Hien Nguyen
+
+Permission is hereby granted, free of charge, to any person obtaining a copy
+of this software and associated documentation files (the "Software"), to deal
+in the Software without restriction, including without limitation the rights
+to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the Software is
+furnished to do so, subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+SOFTWARE.

+ 89 - 0
PUSHKIT.md

@@ -0,0 +1,89 @@
+# Flutter Callkit Incoming
+
+## Setup Pushkit for IOS.
+
+If you are making VoIP application than you definitely want to update your application in the background state as well as wake your application when any VoIP call is being received.
+
+## 🚀&nbsp; Setup
+
+
+Make sure when you create Bundle ID(https://developer.apple.com/account/resources/identifiers) for app you have checked `Push Notifications`
+
+
+1. Enable Voice over IP Setting
+  * Xcode Project > Capabilities
+
+    ![image info](https://raw.githubusercontent.com/hiennguyen92/flutter_callkit_incoming/master/images/Setting.png)
+
+  <br>
+
+  * VoIP Services Certificate
+
+    Go to https://developer.apple.com/account/resources/certificates/add
+    ![image info](https://raw.githubusercontent.com/hiennguyen92/flutter_callkit_incoming/master/images/VoIPServicesCertificate.png)
+
+    Download the certificate and install it into the Keychain Access app(download .cer and double click to install).
+
+  <br>
+    
+  * Export .p12
+
+    ![image info](https://raw.githubusercontent.com/hiennguyen92/flutter_callkit_incoming/master/images/KeychainAccess.png)
+
+  <br>
+    
+  * Convert .p12 to .pem (VOIP.pem)
+
+    ```console
+      openssl pkcs12 -in YOUR_CERTIFICATES.p12 -out VOIP.pem -nodes -clcerts
+    ```
+<br>
+
+2. Configure VoIP Push Notifications in Xcode project (Swift)
+
+* Setup VoIP
+
+  ![image info](https://raw.githubusercontent.com/hiennguyen92/flutter_callkit_incoming/master/images/Xcode-S1.png)
+
+<br>
+
+* DeviceToken and handle incoming pushs
+  ![image info](https://raw.githubusercontent.com/hiennguyen92/flutter_callkit_incoming/master/images/Xcode-S2.png)
+
+<br>
+
+* Start call from Recent history on click (add if necessary)
+
+  ![image info](https://raw.githubusercontent.com/hiennguyen92/flutter_callkit_incoming/master/images/Xcode-S3.png)
+
+<br>
+
+* Example
+
+  https://github.com/hiennguyen92/flutter_callkit_incoming/blob/master/example/ios/Runner/AppDelegate.swift
+<br>
+<br>
+3. Testing
+
+  * Using App
+    https://github.com/onmyway133/PushNotifications
+
+    ![image info](https://raw.githubusercontent.com/hiennguyen92/flutter_callkit_incoming/master/images/TestingApp.png)
+
+<br>
+
+  * Using Curl
+    ```
+    curl -v \
+    -d '{"aps":{"alert":"Hien Nguyen Call"},"id":"44d915e1-5ff4-4bed-bf13-c423048ec97a","nameCaller":"Hien Nguyen","handle":"0123456789","isVideo":true}' \
+    -H "apns-topic: com.hiennv.testing.voip" \
+    -H "apns-push-type: voip" \
+    --http2 \
+    --cert VOIP.pem:'<passphrase>' \
+    https://api.development.push.apple.com/3/device/<device token>
+    ```
+
+  * NOTE
+
+    To be able to testing in Terminated State
+    `Xcode -> Edit Schema -> Wait for the executable to be launched`

+ 646 - 0
README.md

@@ -0,0 +1,646 @@
+# Flutter Callkit Incoming
+
+A Flutter plugin to show incoming call in your Flutter app(Custom for Android/Callkit for iOS).
+
+[![pub package](https://img.shields.io/pub/v/flutter_callkit_incoming.svg)](https://pub.dev/packages/flutter_callkit_incoming)
+[![pub points](https://img.shields.io/pub/points/flutter_callkit_incoming?label=pub%20points)](https://pub.dev/packages/flutter_callkit_incoming/score)
+[![GitHub stars](https://img.shields.io/github/stars/hiennguyen92/flutter_callkit_incoming.svg?style=social)](https://github.com/hiennguyen92/flutter_callkit_incoming/stargazers)
+[![GitHub forks](https://img.shields.io/github/forks/hiennguyen92/flutter_callkit_incoming.svg?style=social)](https://github.com/hiennguyen92/flutter_callkit_incoming/network)
+[![GitHub license](https://img.shields.io/github/license/hiennguyen92/flutter_callkit_incoming.svg)](https://github.com/hiennguyen92/flutter_callkit_incoming/blob/master/LICENSE)
+[![Build Status](https://github.com/hiennguyen92/flutter_callkit_incoming/actions/workflows/main.yml/badge.svg)](https://github.com/hiennguyen92/flutter_callkit_incoming/actions/workflows/main.yml)
+
+## Sponsors
+
+Our top sponsors are shown below!
+
+<a href="https://getstream.io/video/sdk/flutter/tutorial/video-calling/?utm_source=Github&utm_medium=Github_Repo_Content_Ad&utm_content=Developer&utm_campaign=Github_Video&utm_term=flutter_callkit" target="_blank"><img width="250px" src="https://stream-blog.s3.amazonaws.com/blog/wp-content/uploads/fc148f0fc75d02841d017bb36e14e388/Stream-logo-with-background-.png"/></a><br/><span><a href="https://getstream.io/video/sdk/flutter/tutorial/video-calling/?utm_source=Github&utm_medium=Github_Repo_Content_Ad&utm_content=Developer&utm_campaign=Github_Video&utm_term=flutter_callkit" target="_blank">Try the Flutter Video Tutorial 📹</a></span>
+
+<a href="https://www.buymeacoffee.com/hiennguyen92" target="_blank"><img src="https://cdn.buymeacoffee.com/buttons/default-orange.png" alt="Buy Me A Coffee" height="41" width="174"></a>
+
+## :star: Features
+
+* Show an incoming call
+* Start an outgoing call
+* Custom UI Android/Callkit for iOS
+* Example using Pushkit/VoIP for iOS
+
+  <br>
+
+## iOS: ONLY WORKING ON REAL DEVICE PLEASE MAKE SURE SETUP/USING <a href="https://github.com/hiennguyen92/flutter_callkit_incoming/blob/master/PUSHKIT.md" target="_blank">PUSHKIT</a> FOR VOIP
+* please not using on simulator(Callkit framework not working on simulator)
+
+<br>
+
+## 🚀&nbsp; Installation
+
+1. Install Packages(for version >=v2.5.0, please make sure install and use java sdk version >= 17(Android))
+  * Run this command:
+    ```console
+    flutter pub add flutter_callkit_incoming
+    ```
+    * Add pubspec.yaml:
+      ```console
+          dependencies:
+            flutter_callkit_incoming: any
+      ```
+2. Configure Project
+    * Android
+      * AndroidManifest.xml
+      ```
+        <manifest...>
+            ...
+            <!--
+                Using for load image from internet
+            -->
+            <uses-permission android:name="android.permission.INTERNET"/>
+
+          <application ...>
+              <activity ...
+                android:name=".MainActivity"
+                android:launchMode="singleInstance">
+              ...
+          ...
+
+        </manifest>
+      ```
+      The following rule needs to be added in the proguard-rules.pro to avoid obfuscated keys.
+      ```
+        -keep class com.hiennv.flutter_callkit_incoming.** { *; }
+      ```
+    * iOS
+      * Info.plist
+        ```
+        <key>UIBackgroundModes</key>
+        <array>
+            <string>voip</string>
+            <string>remote-notification</string>
+            <string>processing</string> //you can add this if needed
+        </array>
+        ```
+
+3. Usage
+  * Import
+    ```console
+    import 'package:flutter_callkit_incoming/flutter_callkit_incoming.dart';
+    ```
+  * Received an incoming call
+    ```dart
+      this._currentUuid = _uuid.v4();
+      CallKitParams callKitParams = CallKitParams(
+        id: _currentUuid,
+        nameCaller: 'Hien Nguyen',
+        appName: 'Callkit',
+        avatar: 'https://i.pravatar.cc/100',
+        handle: '0123456789',
+        type: 0,
+        textAccept: 'Accept',
+        textDecline: 'Decline',
+        missedCallNotification: NotificationParams(
+            showNotification: true,
+            isShowCallback: true,
+            subtitle: 'Missed call',
+            callbackText: 'Call back',
+        ),
+        callingNotification: const NotificationParams(
+          showNotification: true,
+          isShowCallback: true,
+          subtitle: 'Calling...',
+          callbackText: 'Hang Up',
+        ),
+        duration: 30000,
+        extra: <String, dynamic>{'userId': '1a2b3c4d'},
+        headers: <String, dynamic>{'apiKey': 'Abc@123!', 'platform': 'flutter'},
+        android: const AndroidParams(
+            isCustomNotification: true,
+            isShowLogo: false,
+            logoUrl: 'https://i.pravatar.cc/100',
+            ringtonePath: 'system_ringtone_default',
+            backgroundColor: '#0955fa',
+            backgroundUrl: 'https://i.pravatar.cc/500',
+            actionColor: '#4CAF50',
+            textColor: '#ffffff',
+            incomingCallNotificationChannelName: "Incoming Call",
+            missedCallNotificationChannelName: "Missed Call",
+            isShowCallID: false
+        ),
+        ios: IOSParams(
+          iconName: 'CallKitLogo',
+          handleType: 'generic',
+          supportsVideo: true,
+          maximumCallGroups: 2,
+          maximumCallsPerCallGroup: 1,
+          audioSessionMode: 'default',
+          audioSessionActive: true,
+          audioSessionPreferredSampleRate: 44100.0,
+          audioSessionPreferredIOBufferDuration: 0.005,
+          supportsDTMF: true,
+          supportsHolding: true,
+          supportsGrouping: false,
+          supportsUngrouping: false,
+          ringtonePath: 'system_ringtone_default',
+        ),
+      );
+      await FlutterCallkitIncoming.showCallkitIncoming(callKitParams);
+    ```
+    Note: Firebase Message: `@pragma('vm:entry-point')` <br/>
+    https://github.com/firebase/flutterfire/blob/master/docs/cloud-messaging/receive.md#apple-platforms-and-android
+
+  * request permission for post Notification Android 13+
+  For Android 13+, please `requestNotificationPermission` or requestPermission of firebase_messaging before `showCallkitIncoming`
+    ```dart
+      await FlutterCallkitIncoming.requestNotificationPermission({
+        "rationaleMessagePermission": "Notification permission is required, to show notification.",
+        "postNotificationMessageRequired": "Notification permission is required, Please allow notification permission from setting."
+      });
+    ```
+
+  * request permission for full intent Notification/full screen locked screen Android 14+
+  For Android 14+, please `requestFullIntentPermission`
+    ```dart
+      await FlutterCallkitIncoming.requestFullIntentPermission();
+    ```
+
+  * Show miss call notification
+    ```dart
+      this._currentUuid = _uuid.v4();
+      CallKitParams params = CallKitParams(
+        id: _currentUuid,
+        nameCaller: 'Hien Nguyen',
+        handle: '0123456789',
+        type: 1,
+        missedCallNotification: const NotificationParams(
+          showNotification: true,
+          isShowCallback: true,
+          subtitle: 'Missed call',
+          callbackText: 'Call back',
+        ),
+        android: const AndroidParams(
+          isCustomNotification: true,
+          isShowCallID: true,
+        )
+        extra: <String, dynamic>{'userId': '1a2b3c4d'},
+      );
+      await FlutterCallkitIncoming.showMissCallNotification(params);
+    ```
+  * Hide notification call for Android
+    ```
+      CallKitParams params = CallKitParams(
+        id: _currentUuid,
+      );
+     await FlutterCallkitIncoming.hideCallkitIncoming(params);
+    ```
+
+  * Started an outgoing call
+    ```dart
+      this._currentUuid = _uuid.v4();
+      CallKitParams params = CallKitParams(
+        id: this._currentUuid,
+        nameCaller: 'Hien Nguyen',
+        handle: '0123456789',
+        type: 1,
+        extra: <String, dynamic>{'userId': '1a2b3c4d'},
+        ios: IOSParams(handleType: 'generic'),
+        callingNotification: const NotificationParams(
+          showNotification: true,
+          isShowCallback: true,
+          subtitle: 'Calling...',
+          callbackText: 'Hang Up',
+        ),
+        android: const AndroidParams(
+          isCustomNotification: true,
+          isShowCallID: true,
+        )
+      );
+      await FlutterCallkitIncoming.startCall(params);
+    ```
+
+  * Ended an incoming/outgoing call
+    ```dart
+      await FlutterCallkitIncoming.endCall(this._currentUuid);
+    ```
+
+  * Ended all calls
+    ```dart
+      await FlutterCallkitIncoming.endAllCalls();
+    ```
+
+  * Get active calls. iOS: return active calls from Callkit(only id), Android: only return last call
+    ```dart
+      await FlutterCallkitIncoming.activeCalls();
+    ```
+    Output
+    ```json
+    [{"id": "8BAA2B26-47AD-42C1-9197-1D75F662DF78", ...}]
+    ```
+  * Set status call connected (only for iOS - used to determine Incoming Call or Outgoing Call status in phone book)
+    ```
+      await FlutterCallkitIncoming.setCallConnected(this._currentUuid);
+    ```
+    After the call is ACCEPT or startCall please call this func.
+    normally it should be called when webrtc/p2p.... is established.
+
+  * Get device push token VoIP. iOS: return deviceToken, Android: none
+
+    ```dart
+      await FlutterCallkitIncoming.getDevicePushTokenVoIP();
+    ```
+    Output
+
+    ```json
+    <device token>
+
+    //Example
+    d6a77ca80c5f09f87f353cdd328ec8d7d34e92eb108d046c91906f27f54949cd
+
+    ```
+    Make sure using `SwiftFlutterCallkitIncomingPlugin.sharedInstance?.setDevicePushTokenVoIP(deviceToken)` inside AppDelegate.swift (<a href="https://github.com/hiennguyen92/flutter_callkit_incoming/blob/master/example/ios/Runner/AppDelegate.swift">Example</a>)
+    ```swift
+    func pushRegistry(_ registry: PKPushRegistry, didUpdate credentials: PKPushCredentials, for type: PKPushType) {
+        print(credentials.token)
+        let deviceToken = credentials.token.map { String(format: "%02x", $0) }.joined()
+        //Save deviceToken to your server
+        SwiftFlutterCallkitIncomingPlugin.sharedInstance?.setDevicePushTokenVoIP(deviceToken)
+    }
+
+    func pushRegistry(_ registry: PKPushRegistry, didInvalidatePushTokenFor type: PKPushType) {
+        print("didInvalidatePushTokenFor")
+        SwiftFlutterCallkitIncomingPlugin.sharedInstance?.setDevicePushTokenVoIP("")
+    }
+    ```
+
+
+  * Listen events
+    ```dart
+      FlutterCallkitIncoming.onEvent.listen((CallEvent event) {
+        switch (event!.event) {
+          case Event.actionCallIncoming:
+            // TODO: received an incoming call
+            break;
+          case Event.actionCallStart:
+            // TODO: started an outgoing call
+            // TODO: show screen calling in Flutter
+            break;
+          case Event.actionCallAccept:
+            // TODO: accepted an incoming call
+            // TODO: show screen calling in Flutter
+            break;
+          case Event.actionCallDecline:
+            // TODO: declined an incoming call
+            break;
+          case Event.actionCallEnded:
+            // TODO: ended an incoming/outgoing call
+            break;
+          case Event.actionCallTimeout:
+            // TODO: missed an incoming call
+            break;
+          case Event.actionCallCallback:
+            // TODO: only Android - click action `Call back` from missed call notification
+            break;
+          case Event.actionCallToggleHold:
+            // TODO: only iOS
+            break;
+          case Event.actionCallToggleMute:
+            // TODO: only iOS
+            break;
+          case Event.actionCallToggleDmtf:
+            // TODO: only iOS
+            break;
+          case Event.actionCallToggleGroup:
+            // TODO: only iOS
+            break;
+          case Event.actionCallToggleAudioSession:
+            // TODO: only iOS
+            break;
+          case Event.actionDidUpdateDevicePushTokenVoip:
+            // TODO: only iOS
+            break;
+          case Event.actionCallCustom:
+            // TODO: for custom action
+            break;
+        }
+      });
+    ```
+  * Call from Native (iOS/Android)
+
+    ```swift
+      //Swift iOS
+      var info = [String: Any?]()
+      info["id"] = "44d915e1-5ff4-4bed-bf13-c423048ec97a"
+      info["nameCaller"] = "Hien Nguyen"
+      info["handle"] = "0123456789"
+      info["type"] = 1
+      //... set more data
+      SwiftFlutterCallkitIncomingPlugin.sharedInstance?.showCallkitIncoming(flutter_callkit_incoming.Data(args: info), fromPushKit: true)
+
+      //please make sure call `completion()` at the end of the pushRegistry(......, completion: @escaping () -> Void)
+      // or `DispatchQueue.main.asyncAfter(deadline: .now() + 1.5) { completion() }`
+      // if you don't call completion() in pushRegistry(......, completion: @escaping () -> Void), there may be app crash by system when receiving voIP
+    ```
+
+    ```kotlin
+        //Kotlin/Java Android
+        FlutterCallkitIncomingPlugin.getInstance().showIncomingNotification(...)
+    ```
+
+    <br>
+
+    ```swift
+      //OR
+      let data = flutter_callkit_incoming.Data(id: "44d915e1-5ff4-4bed-bf13-c423048ec97a", nameCaller: "Hien Nguyen", handle: "0123456789", type: 0)
+      data.nameCaller = "Johnny"
+      data.extra = ["user": "abc@123", "platform": "ios"]
+      //... set more data
+      SwiftFlutterCallkitIncomingPlugin.sharedInstance?.showCallkitIncoming(data, fromPushKit: true)
+    ```
+
+    <br>
+
+    ```objc
+      //Objective-C
+      #if __has_include(<flutter_callkit_incoming/flutter_callkit_incoming-Swift.h>)
+      #import <flutter_callkit_incoming/flutter_callkit_incoming-Swift.h>
+      #else
+      #import "flutter_callkit_incoming-Swift.h"
+      #endif
+
+      Data * data = [[Data alloc]initWithId:@"44d915e1-5ff4-4bed-bf13-c423048ec97a" nameCaller:@"Hien Nguyen" handle:@"0123456789" type:1];
+      [data setNameCaller:@"Johnny"];
+      [data setExtra:@{ @"userId" : @"HelloXXXX", @"key2" : @"value2"}];
+      //... set more data
+      [SwiftFlutterCallkitIncomingPlugin.sharedInstance showCallkitIncoming:data fromPushKit:YES];
+    ```
+
+    <br>
+
+    ```swift
+      //send custom event from native
+      SwiftFlutterCallkitIncomingPlugin.sharedInstance?.sendEventCustom(body: ["customKey": "customValue"])
+
+    ```
+
+    ```kotlin
+        //Kotlin/Java Android
+        FlutterCallkitIncomingPlugin.getInstance().sendEventCustom(body: Map<String, Any>)
+    ```
+    * 3.1 Call API when accept/decline/end/timeout
+    ```swift
+    //Appdelegate
+    ...
+    @UIApplicationMain
+    @objc class AppDelegate: FlutterAppDelegate, PKPushRegistryDelegate, CallkitIncomingAppDelegate {
+    ...
+
+    override func application(
+        _ application: UIApplication,
+        didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
+    ) -> Bool {
+        GeneratedPluginRegistrant.register(with: self)
+        
+        //Setup VOIP
+        let mainQueue = DispatchQueue.main
+        let voipRegistry: PKPushRegistry = PKPushRegistry(queue: mainQueue)
+        voipRegistry.delegate = self
+        voipRegistry.desiredPushTypes = [PKPushType.voIP]
+
+        //Use if using WebRTC
+        //RTCAudioSession.sharedInstance().useManualAudio = true
+        //RTCAudioSession.sharedInstance().isAudioEnabled = false
+        
+        return super.application(application, didFinishLaunchingWithOptions: launchOptions)
+    }
+
+
+    // Func Call api for Accept
+    func onAccept(_ call: Call, _ action: CXAnswerCallAction) {
+        let json = ["action": "ACCEPT", "data": call.data.toJSON()] as [String: Any]
+        print("LOG: onAccept")
+        self.performRequest(parameters: json) { result in
+            switch result {
+            case .success(let data):
+                print("Received data: \(data)")
+                //Make sure call action.fulfill() when you are done(connected WebRTC - Start counting seconds)
+                action.fulfill()
+
+            case .failure(let error):
+                print("Error: \(error.localizedDescription)")
+            }
+        }
+    }
+    
+    // Func Call API for Decline
+    func onDecline(_ call: Call, _ action: CXEndCallAction) {
+        let json = ["action": "DECLINE", "data": call.data.toJSON()] as [String: Any]
+        print("LOG: onDecline")
+        self.performRequest(parameters: json) { result in
+            switch result {
+            case .success(let data):
+                print("Received data: \(data)")
+                //Make sure call action.fulfill() when you are done
+                action.fulfill()
+
+            case .failure(let error):
+                print("Error: \(error.localizedDescription)")
+            }
+        }
+    }
+    
+    // Func Call API for End
+    func onEnd(_ call: Call, _ action: CXEndCallAction) {
+        let json = ["action": "END", "data": call.data.toJSON()] as [String: Any]
+        print("LOG: onEnd")
+        self.performRequest(parameters: json) { result in
+            switch result {
+            case .success(let data):
+                print("Received data: \(data)")
+                //Make sure call action.fulfill() when you are done
+                action.fulfill()
+
+            case .failure(let error):
+                print("Error: \(error.localizedDescription)")
+            }
+        }
+    }
+    
+    func onTimeOut(_ call: Call) {
+        let json = ["action": "TIMEOUT", "data": call.data.toJSON()] as [String: Any]
+        print("LOG: onTimeOut")
+        self.performRequest(parameters: json) { result in
+            switch result {
+            case .success(let data):
+                print("Received data: \(data)")
+
+            case .failure(let error):
+                print("Error: \(error.localizedDescription)")
+            }
+        }
+    }
+
+    func didActivateAudioSession(_ audioSession: AVAudioSession) {
+        //Use if using WebRTC
+        //RTCAudioSession.sharedInstance().audioSessionDidActivate(audioSession)
+        //RTCAudioSession.sharedInstance().isAudioEnabled = true
+    }
+    
+    func didDeactivateAudioSession(_ audioSession: AVAudioSession) {
+        //Use if using WebRTC
+        //RTCAudioSession.sharedInstance().audioSessionDidDeactivate(audioSession)
+        //RTCAudioSession.sharedInstance().isAudioEnabled = false
+    }
+    ...
+
+    ``` 
+    <a href='https://github.com/hiennguyen92/flutter_callkit_incoming/blob/master/example/ios/Runner/AppDelegate.swift'>Please check full: Example</a>
+
+ Properties
+
+    | Prop            | Description                                                             | Default     |
+    | --------------- | ----------------------------------------------------------------------- | ----------- |
+    |  **`id`**       | UUID identifier for each call. UUID should be unique for every call and when the call is  ended, the same UUID for that call to be used. suggest using <a href='https://pub.dev/packages/uuid'>uuid.</a> ACCEPT ONLY UUID    | Required    |
+    | **`nameCaller`**| Caller's name.                                                          | _None_      |
+    | **`appName`**   | App's name. using for display inside Callkit(iOS).                      |   App Name, `Deprecated for iOS > 14, default using App name`  |
+    | **`avatar`**    | Avatar's URL used for display for Android. `/android/src/main/res/drawable-xxxhdpi/ic_default_avatar.png`                             |    _None_   |
+    | **`handle`**    | Phone number/Email/Any.                                                 |    _None_   |
+    |   **`type`**    |  0 - Audio Call, 1 - Video Call                                         |     `0`     |
+    | **`duration`**  | Incoming call/Outgoing call display time (second). If the time is over, the call will be missed.                                                                                     |    `30000`  |
+   | **`textAccept`**  | Text `Accept` used in Android                                            |    `Accept`  |
+   | **`textDecline`**  | Text `Decline` used in Android                                           |    `Decline`  |
+    |   **`extra`**   | Any data added to the event when received.                              |     `{}`    |
+    |   **`headers`** | Any data for custom header avatar/background image.                     |     `{}`    |
+    |  **`missedCallNotification`**  | Android data needed to customize Miss Call Notification.                                    |    Below    | 
+    |  **`callingNotification`**  | Android data needed to customize Calling Notification.                                    |    Below    |
+    |  **`android`**  | Android data needed to customize UI.                                    |    Below    |
+    |    **`ios`**    | iOS data needed.                                                        |    Below    |
+
+    <br>
+
+* Missed Call Notification
+
+    | Prop            | Description                                                              | Default     |
+    | --------------- |--------------------------------------------------------------------------| ----------- |
+    | **`subtitle`**  | Text `Missed Call` used in Android (show in miss call notification)      |    `Missed Call`  |
+   | **`callbackText`**  | Text `Call back` used in Android (show in miss call notification action) |    `Call back`  |
+   |       **`showNotification`**      | Show missed call notification when timeout                               | `true`          |
+    |       **`isShowCallback`**      | Show callback action from miss call notification.                        | `true`          |
+
+* Calling Notification
+
+  | Prop            | Description                                                            | Default      |
+      | --------------- |------------------------------------------------------------------------|--------------|
+  | **`subtitle`**  | Text `Missed Call` used in Android (show in calling notification)      | `Calling...` |
+  | **`callbackText`**  | Text `Call back` used in Android (show in calling notification action) | `Hang up`    |
+  |       **`showNotification`**      | Show calling notification when start call/accept call                  | `true`       |
+  |       **`isShowCallback`**      | Show hang up action from calling notification.                         | `true`       |
+* Android
+
+    | Prop                                      | Description                                                                                                        | Default                                                           |
+    |-------------------------------------------|--------------------------------------------------------------------------------------------------------------------|-------------------------------------------------------------------|
+    | **`isCustomNotification`**                | Using custom notifications.                                                                                        | `false`                                                           |
+    | **`isCustomSmallExNotification`**         | Using custom notification small on some devices clipped out in android.                                            | `false`                                                           |
+    | **`isShowLogo`**                          | Show logo app inside full screen. `/android/src/main/res/drawable-xxxhdpi/ic_logo.png`                             | `false`                                                           |
+    | **`logoUrl`**                             | Logo app inside full screen. example: http://... https://... or "assets/abc.png"                                   | _None_                                                            |
+    | **`ringtonePath`**                        | File name of a ringtone ex: `ringtone_default`. Put file into `/android/app/src/main/res/raw/ringtone_default.mp3` | `system_ringtone_default` <br>using ringtone default of the phone |
+    | **`backgroundColor`**                     | Incoming call screen background color.                                                                             | `#0955fa`                                                         |
+    | **`backgroundUrl`**                       | Using image background for Incoming call screen. example: http://... https://... or "assets/abc.png"               | _None_                                                            |
+    | **`actionColor`**                         | Color used in button/text on notification.                                                                         | `#4CAF50`                                                         |
+    | **`textColor`**                           | Color used for the text in full screen notification.                                                               | `#ffffff`                                                         |
+    | **`incomingCallNotificationChannelName`** | Notification channel name of incoming call.                                                                        | `Incoming call`                                                   |
+    | **`missedCallNotificationChannelName`**   | Notification channel name of missed call.                                                                          | `Missed call`                                                     |
+    | **`isShowCallID`**                        | Show call id app inside full screen/notification.                                                                  | false                                                             |
+    | **`isShowFullLockedScreen`**              | Show full screen on Locked Screen(please make sure call `requestFullIntentPermission` for android 14+).            | true                                                              |
+
+    <br>
+
+* iOS
+
+    | Prop                                      | Description                                                             | Default     |
+    | ----------------------------------------- | ----------------------------------------------------------------------- | ----------- |
+    |               **`iconName`**              | App's Icon. using for display inside Callkit(iOS)                       | `CallKitLogo` <br> using from `Images.xcassets/CallKitLogo`    |
+    |              **`handleType`**             | Type handle call `generic`, `number`, `email`                           | `generic`   |
+    |             **`supportsVideo`**           |                                                                         |   `true`    |
+    |          **`maximumCallGroups`**          |                                                                         |     `2`     |
+    |       **`maximumCallsPerCallGroup`**      |                                                                         |     `1`     |
+    |           **`audioSessionMode`**          |                                                                         |   _None_, `gameChat`, `measurement`, `moviePlayback`, `spokenAudio`, `videoChat`, `videoRecording`, `voiceChat`, `voicePrompt`  |
+    |        **`audioSessionActive`**           |                                                                         |    `true`   |
+    |   **`audioSessionPreferredSampleRate`**   |                                                                         |  `44100.0`  |
+    |**`audioSessionPreferredIOBufferDuration`**|                                                                         |  `0.005`    |
+    |            **`supportsDTMF`**             |                                                                         |    `true`   |
+    |            **`supportsHolding`**          |                                                                         |    `true`   |
+    |          **`supportsGrouping`**           |                                                                         |    `true`   |
+    |         **`supportsUngrouping`**          |                                                                         |   `true`    |
+    |           **`ringtonePath`**              | Add file to root project xcode  `/ios/Runner/Ringtone.caf`  and Copy Bundle Resources(Build Phases)                                                                                                               |`Ringtone.caf`<br>`system_ringtone_default` <br>using ringtone default of the phone|
+
+
+5. Source code
+
+    ```
+    please checkout repo github
+    https://github.com/hiennguyen92/flutter_callkit_incoming
+    ```
+    * <a href='https://github.com/hiennguyen92/flutter_callkit_incoming'>https://github.com/hiennguyen92/flutter_callkit_incoming</a>
+    * <a href='https://github.com/hiennguyen92/flutter_callkit_incoming/blob/master/example/lib/main.dart'>Example</a>
+
+  <br>
+
+6. Pushkit - Received VoIP and Wake app from Terminated State (only for IOS)
+  * Please check <a href="https://github.com/hiennguyen92/flutter_callkit_incoming/blob/master/PUSHKIT.md">PUSHKIT.md</a> setup Pushkit for IOS
+
+  <br>
+
+7. Todo
+  * Run background
+  * Simplify the setup process
+  * Custom notification for iOS(Missing notification)
+  * Keep notification when calling
+    <br>
+
+## :bulb: Demo
+
+1. Demo Illustration:
+2. Image
+<table>
+  <tr>
+    <td>iOS(Lockscreen)</td>
+    <td>iOS(full screen)</td>
+    <td>iOS(Alert)</td>
+  </tr>
+  <tr>
+    <td>
+      <img src="https://raw.githubusercontent.com/hiennguyen92/flutter_callkit_incoming/master/images/image1.png" width="220">
+    </td>
+    <td>
+      <img src="https://raw.githubusercontent.com/hiennguyen92/flutter_callkit_incoming/master/images/image2.png" width="220">
+    </td>
+    <td>
+      <img src="https://raw.githubusercontent.com/hiennguyen92/flutter_callkit_incoming/master/images/image3.png" width="220">
+    </td>
+  </tr>
+  <tr>
+    <td>Android(Lockscreen) - Audio</td>
+    <td>Android(Alert) - Audio</td>
+    <td>Android(Lockscreen) - Video</td>
+  </tr>
+  <tr>
+    <td>
+      <img src="https://raw.githubusercontent.com/hiennguyen92/flutter_callkit_incoming/master/images/image4.jpg" width="220">
+    </td>
+    <td>
+      <img src="https://raw.githubusercontent.com/hiennguyen92/flutter_callkit_incoming/master/images/image5.jpg" width="220">
+    </td>
+    <td>
+      <img src="https://raw.githubusercontent.com/hiennguyen92/flutter_callkit_incoming/master/images/image6.jpg" width="220">
+    </td>
+  </tr>
+  <tr>
+    <td>Android(Alert) - Video</td>
+    <td>isCustomNotification: false</td>
+    <td></td>
+  </tr>
+  <tr>
+    <td>
+      <img src="https://raw.githubusercontent.com/hiennguyen92/flutter_callkit_incoming/master/images/image7.jpg" width="220">
+    </td>
+    <td>
+      <img src="https://raw.githubusercontent.com/hiennguyen92/flutter_callkit_incoming/master/images/image8.jpg" width="220">
+    </td>
+  </tr>
+ </table>

+ 1 - 0
analysis_options.yaml

@@ -0,0 +1 @@
+include: package:flutter_lints/flutter.yaml

+ 8 - 0
android/.gitignore

@@ -0,0 +1,8 @@
+*.iml
+.gradle
+/local.properties
+/.idea/workspace.xml
+/.idea/libraries
+.DS_Store
+/build
+/captures

+ 67 - 0
android/build.gradle

@@ -0,0 +1,67 @@
+group 'com.example.flutter_callkit_incoming'
+version '1.0-SNAPSHOT'
+
+buildscript {
+    ext.kotlin_version = '1.8.0'
+    repositories {
+        google()
+        jcenter()
+    }
+
+    dependencies {
+        //classpath 'com.android.tools.build:gradle:4.1.0'
+        //classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
+        
+        classpath 'com.android.tools.build:gradle:7.1.2'
+        classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version"
+    }
+}
+
+rootProject.allprojects {
+    repositories {
+        google()
+        jcenter()
+    }
+}
+
+apply plugin: 'com.android.library'
+apply plugin: 'kotlin-android'
+
+android {
+    compileSdk 34
+
+    if (project.android.hasProperty("namespace")) {
+        namespace "com.hiennv.flutter_callkit_incoming"
+    }
+    
+    sourceSets {
+        main.java.srcDirs += 'src/main/kotlin'
+    }
+    defaultConfig {
+        minSdk 16
+        targetSdk 34
+
+        consumerProguardFiles 'consumer-rules.pro'
+    }
+}
+
+java {
+    toolchain {
+        languageVersion.set(JavaLanguageVersion.of(17))
+    }
+}
+
+kotlin {
+    jvmToolchain(17)
+}
+
+dependencies {
+    implementation "org.jetbrains.kotlin:kotlin-stdlib-jdk7:$kotlin_version"
+    implementation 'androidx.core:core-ktx:1.13.1'
+    implementation 'androidx.appcompat:appcompat:1.6.1'
+    implementation 'de.hdodenhof:circleimageview:3.1.0'
+    implementation 'com.squareup.picasso:picasso:2.71828'
+    implementation 'androidx.localbroadcastmanager:localbroadcastmanager:1.1.0'
+    implementation 'com.fasterxml.jackson.core:jackson-core:2.11.1'
+    implementation 'com.fasterxml.jackson.core:jackson-databind:2.11.1'
+}

+ 5 - 0
android/consumer-rules.pro

@@ -0,0 +1,5 @@
+#flutter_callkit_incoming
+# Issue: https://github.com/hiennguyen92/flutter_callkit_incoming/issues/171
+-keep class com.hiennv.flutter_callkit_incoming.** { *; }
+-keep class com.fasterxml.** { *; }
+-keep class org.codehaus.** { *; }

+ 3 - 0
android/gradle.properties

@@ -0,0 +1,3 @@
+org.gradle.jvmargs=-Xmx1536M
+android.useAndroidX=true
+android.enableJetifier=true

+ 6 - 0
android/gradle/wrapper/gradle-wrapper.properties

@@ -0,0 +1,6 @@
+#Fri Jun 23 08:50:38 CEST 2017
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-7.4-all.zip

+ 1 - 0
android/settings.gradle

@@ -0,0 +1 @@
+rootProject.name = 'flutter_callkit_incoming'

+ 60 - 0
android/src/main/AndroidManifest.xml

@@ -0,0 +1,60 @@
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+    package="com.hiennv.flutter_callkit_incoming">
+
+    
+    <uses-permission android:name="android.permission.INTERNET" />
+    <uses-permission android:name="android.permission.VIBRATE" />
+    <uses-permission android:name="android.permission.WAKE_LOCK" />
+    <uses-permission android:name="android.permission.ACCESS_NOTIFICATION_POLICY" />
+    <uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
+    <uses-permission android:name="android.permission.FOREGROUND_SERVICE" />
+
+    <application>
+
+        <activity
+            android:name="com.hiennv.flutter_callkit_incoming.CallkitIncomingActivity"
+            android:taskAffinity="com.hiennv.flutter_callkit_incoming.INCOMING_CALL_AFFINITY"
+            android:excludeFromRecents="true"
+            android:noHistory="true"
+            android:turnScreenOn="true"
+            android:configChanges="orientation"
+            android:exported="false"
+            android:launchMode="singleInstance"
+            android:allowTaskReparenting="false"
+            android:theme="@style/CallkitIncomingTheme">
+            <intent-filter>
+                <action android:name="${applicationId}.com.hiennv.flutter_callkit_incoming.ACTION_CALL_INCOMING" />
+                <category android:name="android.intent.category.DEFAULT" />
+            </intent-filter>
+        </activity>
+        <activity
+            android:exported="false"
+            android:theme="@style/TranslucentTheme"
+            android:name="com.hiennv.flutter_callkit_incoming.TransparentActivity"/>
+
+        <receiver
+            android:name="com.hiennv.flutter_callkit_incoming.CallkitIncomingBroadcastReceiver"
+            android:enabled="true"
+            android:exported="true">
+            <intent-filter>
+                <action android:name="${applicationId}.com.hiennv.flutter_callkit_incoming.ACTION_CALL_INCOMING" />
+                <action android:name="${applicationId}.com.hiennv.flutter_callkit_incoming.ACTION_CALL_ACCEPT" />
+                <action android:name="${applicationId}.com.hiennv.flutter_callkit_incoming.ACTION_CALL_DECLINE" />
+                <action android:name="${applicationId}.com.hiennv.flutter_callkit_incoming.ACTION_CALL_ENDED" />
+                <action android:name="${applicationId}.com.hiennv.flutter_callkit_incoming.ACTION_CALL_TIMEOUT" />
+            </intent-filter>
+        </receiver>
+
+        <service
+            android:enabled="true"
+            android:exported="true"
+            android:name="com.hiennv.flutter_callkit_incoming.CallkitSoundPlayerService"/>
+
+        <service
+            android:enabled="true"
+            android:exported="true"
+            android:foregroundServiceType="phoneCall"
+            android:name="com.hiennv.flutter_callkit_incoming.OngoingNotificationService"/>
+
+    </application>
+</manifest>

+ 15 - 0
android/src/main/kotlin/com/hiennv/flutter_callkit_incoming/AppUtils.kt

@@ -0,0 +1,15 @@
+package com.hiennv.flutter_callkit_incoming
+
+import android.content.Context
+import android.content.Intent
+import android.os.Bundle
+
+object AppUtils {
+    fun getAppIntent(context: Context, action: String? = null, data: Bundle? = null): Intent? {
+        val intent = context.packageManager.getLaunchIntentForPackage(context.packageName)?.cloneFilter()
+        intent?.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_REORDER_TO_FRONT or Intent.FLAG_ACTIVITY_CLEAR_TOP)
+        intent?.putExtra(FlutterCallkitIncomingPlugin.EXTRA_CALLKIT_CALL_DATA, data)
+        intent?.action = action
+        return intent
+    }
+}

+ 439 - 0
android/src/main/kotlin/com/hiennv/flutter_callkit_incoming/Call.kt

@@ -0,0 +1,439 @@
+package com.hiennv.flutter_callkit_incoming
+
+import android.os.Bundle
+import com.fasterxml.jackson.annotation.JsonProperty
+
+@Suppress("UNCHECKED_CAST")
+data class Data(val args: Map<String, Any?>) {
+
+    constructor() : this(emptyMap())
+
+
+    @JsonProperty("id")
+    var id: String = (args["id"] as? String) ?: ""
+
+    @JsonProperty("uuid")
+    var uuid: String = (args["id"] as? String) ?: ""
+
+    @JsonProperty("nameCaller")
+    var nameCaller: String = (args["nameCaller"] as? String) ?: ""
+
+    @JsonProperty("appName")
+    var appName: String = (args["appName"] as? String) ?: ""
+
+    @JsonProperty("handle")
+    var handle: String = (args["handle"] as? String) ?: ""
+
+    @JsonProperty("avatar")
+    var avatar: String = (args["avatar"] as? String) ?: ""
+
+    @JsonProperty("type")
+    var type: Int = (args["type"] as? Int) ?: 0
+
+    @JsonProperty("duration")
+    var duration: Long =
+        (args["duration"] as? Long) ?: ((args["duration"] as? Int)?.toLong() ?: 30000L)
+
+    @JsonProperty("textAccept")
+    var textAccept: String = (args["textAccept"] as? String) ?: ""
+
+    @JsonProperty("textDecline")
+    var textDecline: String = (args["textDecline"] as? String) ?: ""
+
+    @JsonProperty("extra")
+    var extra: HashMap<String, Any?> =
+        (args["extra"] ?: HashMap<String, Any?>()) as HashMap<String, Any?>
+
+    @JsonProperty("headers")
+    var headers: HashMap<String, Any?> =
+        (args["headers"] ?: HashMap<String, Any?>()) as HashMap<String, Any?>
+
+    @JsonProperty("from")
+    var from: String = ""
+
+    @JsonProperty("isCustomNotification")
+    var isCustomNotification: Boolean = false
+
+    @JsonProperty("isCustomSmallExNotification")
+    var isCustomSmallExNotification: Boolean = false
+
+    @JsonProperty("isShowLogo")
+    var isShowLogo: Boolean = false
+
+    @JsonProperty("logoUrl")
+    var logoUrl: String
+
+    @JsonProperty("isShowCallID")
+    var isShowCallID: Boolean = false
+
+    @JsonProperty("ringtonePath")
+    var ringtonePath: String
+
+    @JsonProperty("backgroundColor")
+    var backgroundColor: String
+
+    @JsonProperty("backgroundUrl")
+    var backgroundUrl: String
+
+    @JsonProperty("textColor")
+    var textColor: String
+
+    @JsonProperty("actionColor")
+    var actionColor: String
+
+    @JsonProperty("incomingCallNotificationChannelName")
+    var incomingCallNotificationChannelName: String? = null
+
+    @JsonProperty("missedCallNotificationChannelName")
+    var missedCallNotificationChannelName: String? = null
+
+    @JsonProperty("missedNotificationId")
+    var missedNotificationId: Int? = null
+
+    @JsonProperty("isShowMissedCallNotification")
+    var isShowMissedCallNotification: Boolean = true
+
+    @JsonProperty("missedNotificationCount")
+    var missedNotificationCount: Int = 1
+
+    @JsonProperty("missedNotificationSubtitle")
+    var missedNotificationSubtitle: String? = null
+
+    @JsonProperty("missedNotificationCallbackText")
+    var missedNotificationCallbackText: String? = null
+
+    @JsonProperty("isShowCallback")
+    var isShowCallback: Boolean = true
+
+    @JsonProperty("isAccepted")
+    var isAccepted: Boolean = false
+
+    @JsonProperty("callingNotificationId")
+    var callingNotificationId: Int? = null
+
+    @JsonProperty("isShowCallingNotification")
+    var isShowCallingNotification: Boolean = true
+
+    @JsonProperty("callingNotificationSubtitle")
+    var callingNotificationSubtitle: String? = null
+
+    @JsonProperty("callingNotificationCallbackText")
+    var callingNotificationHangupText: String? = null
+
+    @JsonProperty("isShowHangup")
+    var isShowHangup: Boolean = true
+
+    @JsonProperty("isOnHold")
+    var isOnHold: Boolean = (args["isOnHold"] as? Boolean) ?: false
+
+    @JsonProperty("audioRoute")
+    var audioRoute: Int = (args["audioRoute"] as? Int) ?: 1
+
+    @JsonProperty("isMuted")
+    var isMuted: Boolean = (args["isMuted"] as? Boolean) ?: false
+
+    @JsonProperty("isShowFullLockedScreen")
+    var isShowFullLockedScreen: Boolean = true
+
+    @JsonProperty("isImportant")
+    var isImportant: Boolean = false
+
+    @JsonProperty("isBot")
+    var isBot: Boolean = false
+
+    init {
+        var android: Map<String, Any?>? = args["android"] as? HashMap<String, Any?>?
+        android = android ?: args
+        isCustomNotification = android["isCustomNotification"] as? Boolean ?: false
+        isCustomSmallExNotification = android["isCustomSmallExNotification"] as? Boolean ?: false
+        isShowLogo = android["isShowLogo"] as? Boolean ?: false
+        logoUrl = android["logoUrl"] as? String ?: ""
+        isShowCallID = android["isShowCallID"] as? Boolean ?: false
+        ringtonePath = android["ringtonePath"] as? String ?: ""
+        backgroundColor = android["backgroundColor"] as? String ?: "#0955fa"
+        backgroundUrl = android["backgroundUrl"] as? String ?: ""
+        actionColor = android["actionColor"] as? String ?: "#4CAF50"
+        textColor = android["textColor"] as? String ?: "#ffffff"
+        incomingCallNotificationChannelName =
+            android["incomingCallNotificationChannelName"] as? String
+        missedCallNotificationChannelName = android["missedCallNotificationChannelName"] as? String
+        isShowFullLockedScreen = android["isShowFullLockedScreen"] as? Boolean ?: true
+        isImportant = android["isImportant"] as? Boolean ?: false
+        isBot = android["isBot"] as? Boolean ?: false
+
+        val missedNotification: Map<String, Any?>? =
+            args["missedCallNotification"] as? Map<String, Any?>?
+
+
+        if (missedNotification != null) {
+            missedNotificationId = missedNotification["id"] as? Int?
+            missedNotificationSubtitle = missedNotification["subtitle"] as? String?
+            missedNotificationCount = missedNotification["count"] as? Int? ?: 1
+            missedNotificationCallbackText = missedNotification["callbackText"] as? String?
+            isShowCallback = missedNotification["isShowCallback"] as? Boolean ?: true
+            isShowMissedCallNotification =
+                missedNotification["showNotification"] as? Boolean ?: true
+        } else {
+            missedNotificationSubtitle = args["textMissedCall"] as? String ?: ""
+            missedNotificationCallbackText = args["textCallback"] as? String ?: ""
+            isShowCallback = android["isShowCallback"] as? Boolean ?: true
+            isShowMissedCallNotification =
+                android["isShowMissedCallNotification"] as? Boolean ?: true
+        }
+
+        val callingNotification: Map<String, Any?>? =
+            args["callingNotification"] as? Map<String, Any?>?
+
+        if (callingNotification != null) {
+            callingNotificationId = callingNotification["id"] as? Int?
+            callingNotificationSubtitle = callingNotification["subtitle"] as? String?
+            //callingNotificationCount = missedNotification["count"] as? Int? ?: 1
+            callingNotificationHangupText = callingNotification["callbackText"] as? String?
+            isShowHangup = callingNotification["isShowCallback"] as? Boolean ?: true
+            isShowCallingNotification =
+                callingNotification["showNotification"] as? Boolean ?: true
+        }
+
+
+    }
+
+    override fun hashCode(): Int {
+        return id.hashCode()
+    }
+
+    override fun equals(other: Any?): Boolean {
+        if (other == null) return false
+        val e: Data = other as Data
+        return this.id == e.id
+    }
+
+
+    fun toBundle(): Bundle {
+        val bundle = Bundle()
+        bundle.putString(CallkitConstants.EXTRA_CALLKIT_ID, id)
+        bundle.putString(CallkitConstants.EXTRA_CALLKIT_NAME_CALLER, nameCaller)
+        bundle.putString(CallkitConstants.EXTRA_CALLKIT_HANDLE, handle)
+        bundle.putString(CallkitConstants.EXTRA_CALLKIT_AVATAR, avatar)
+        bundle.putInt(CallkitConstants.EXTRA_CALLKIT_TYPE, type)
+        bundle.putLong(CallkitConstants.EXTRA_CALLKIT_DURATION, duration)
+        bundle.putString(CallkitConstants.EXTRA_CALLKIT_TEXT_ACCEPT, textAccept)
+        bundle.putString(CallkitConstants.EXTRA_CALLKIT_TEXT_DECLINE, textDecline)
+
+        missedNotificationId?.let {
+            bundle.putInt(
+                CallkitConstants.EXTRA_CALLKIT_MISSED_CALL_ID,
+                it
+            )
+        }
+        bundle.putBoolean(
+            CallkitConstants.EXTRA_CALLKIT_MISSED_CALL_SHOW,
+            isShowMissedCallNotification
+        )
+        bundle.putInt(
+            CallkitConstants.EXTRA_CALLKIT_MISSED_CALL_COUNT,
+            missedNotificationCount
+        )
+        bundle.putString(
+            CallkitConstants.EXTRA_CALLKIT_MISSED_CALL_SUBTITLE,
+            missedNotificationSubtitle
+        )
+        bundle.putBoolean(
+            CallkitConstants.EXTRA_CALLKIT_MISSED_CALL_CALLBACK_SHOW,
+            isShowCallback
+        )
+        bundle.putString(
+            CallkitConstants.EXTRA_CALLKIT_MISSED_CALL_CALLBACK_TEXT,
+            missedNotificationCallbackText
+        )
+
+        callingNotificationId?.let {
+            bundle.putInt(
+                CallkitConstants.EXTRA_CALLKIT_CALLING_ID,
+                it
+            )
+        }
+        bundle.putBoolean(
+            CallkitConstants.EXTRA_CALLKIT_CALLING_SHOW,
+            isShowCallingNotification
+        )
+        bundle.putString(
+            CallkitConstants.EXTRA_CALLKIT_CALLING_SUBTITLE,
+            callingNotificationSubtitle
+        )
+        bundle.putBoolean(
+            CallkitConstants.EXTRA_CALLKIT_CALLING_HANG_UP_SHOW,
+            isShowHangup
+        )
+        bundle.putString(
+            CallkitConstants.EXTRA_CALLKIT_CALLING_HANG_UP_TEXT,
+            callingNotificationHangupText
+        )
+
+
+        bundle.putSerializable(CallkitConstants.EXTRA_CALLKIT_EXTRA, extra)
+        bundle.putSerializable(CallkitConstants.EXTRA_CALLKIT_HEADERS, headers)
+
+        bundle.putBoolean(
+            CallkitConstants.EXTRA_CALLKIT_IS_CUSTOM_NOTIFICATION,
+            isCustomNotification
+        )
+        bundle.putBoolean(
+            CallkitConstants.EXTRA_CALLKIT_IS_CUSTOM_SMALL_EX_NOTIFICATION,
+            isCustomSmallExNotification
+        )
+        bundle.putBoolean(
+            CallkitConstants.EXTRA_CALLKIT_IS_SHOW_LOGO,
+            isShowLogo
+        )
+        bundle.putString(
+            CallkitConstants.EXTRA_CALLKIT_LOGO_URL,
+            logoUrl
+        )
+        bundle.putBoolean(
+            CallkitConstants.EXTRA_CALLKIT_IS_SHOW_CALL_ID,
+            isShowCallID
+        )
+        bundle.putString(CallkitConstants.EXTRA_CALLKIT_RINGTONE_PATH, ringtonePath)
+        bundle.putString(
+            CallkitConstants.EXTRA_CALLKIT_BACKGROUND_COLOR,
+            backgroundColor
+        )
+        bundle.putString(
+            CallkitConstants.EXTRA_CALLKIT_BACKGROUND_URL,
+            backgroundUrl
+        )
+        bundle.putString(CallkitConstants.EXTRA_CALLKIT_TEXT_COLOR, textColor)
+        bundle.putString(CallkitConstants.EXTRA_CALLKIT_ACTION_COLOR, actionColor)
+        bundle.putString(CallkitConstants.EXTRA_CALLKIT_ACTION_FROM, from)
+        bundle.putString(
+            CallkitConstants.EXTRA_CALLKIT_INCOMING_CALL_NOTIFICATION_CHANNEL_NAME,
+            incomingCallNotificationChannelName
+        )
+        bundle.putString(
+            CallkitConstants.EXTRA_CALLKIT_MISSED_CALL_NOTIFICATION_CHANNEL_NAME,
+            missedCallNotificationChannelName
+        )
+        bundle.putBoolean(
+            CallkitConstants.EXTRA_CALLKIT_IS_SHOW_FULL_LOCKED_SCREEN,
+            isShowFullLockedScreen
+        )
+        bundle.putBoolean(
+            CallkitConstants.EXTRA_CALLKIT_IS_IMPORTANT,
+            isImportant,
+        )
+        bundle.putBoolean(
+            CallkitConstants.EXTRA_CALLKIT_IS_BOT,
+            isBot,
+        )
+        return bundle
+    }
+
+    companion object {
+
+        fun fromBundle(bundle: Bundle): Data {
+            val data = Data(emptyMap())
+            data.id = bundle.getString(CallkitConstants.EXTRA_CALLKIT_ID, "")
+            data.nameCaller =
+                bundle.getString(CallkitConstants.EXTRA_CALLKIT_NAME_CALLER, "")
+            data.appName =
+                bundle.getString(CallkitConstants.EXTRA_CALLKIT_APP_NAME, "")
+            data.handle =
+                bundle.getString(CallkitConstants.EXTRA_CALLKIT_HANDLE, "")
+            data.avatar =
+                bundle.getString(CallkitConstants.EXTRA_CALLKIT_AVATAR, "")
+            data.type = bundle.getInt(CallkitConstants.EXTRA_CALLKIT_TYPE, 0)
+            data.duration =
+                bundle.getLong(CallkitConstants.EXTRA_CALLKIT_DURATION, 30000L)
+            data.textAccept =
+                bundle.getString(CallkitConstants.EXTRA_CALLKIT_TEXT_ACCEPT, "")
+            data.textDecline =
+                bundle.getString(CallkitConstants.EXTRA_CALLKIT_TEXT_DECLINE, "")
+            data.isImportant =
+                bundle.getBoolean(CallkitConstants.EXTRA_CALLKIT_IS_IMPORTANT, false)
+            data.isBot =
+                bundle.getBoolean(CallkitConstants.EXTRA_CALLKIT_IS_BOT, false)
+
+            data.missedNotificationId =
+                bundle.getInt(CallkitConstants.EXTRA_CALLKIT_MISSED_CALL_ID)
+            data.isShowMissedCallNotification =
+                bundle.getBoolean(CallkitConstants.EXTRA_CALLKIT_MISSED_CALL_SHOW, true)
+            data.missedNotificationCount =
+                bundle.getInt(CallkitConstants.EXTRA_CALLKIT_MISSED_CALL_COUNT, 1)
+            data.missedNotificationSubtitle =
+                bundle.getString(CallkitConstants.EXTRA_CALLKIT_MISSED_CALL_SUBTITLE, "")
+            data.isShowCallback =
+                bundle.getBoolean(CallkitConstants.EXTRA_CALLKIT_MISSED_CALL_CALLBACK_SHOW, false)
+            data.missedNotificationCallbackText =
+                bundle.getString(CallkitConstants.EXTRA_CALLKIT_MISSED_CALL_CALLBACK_TEXT, "")
+
+
+            data.callingNotificationId = bundle.getInt(CallkitConstants.EXTRA_CALLKIT_CALLING_ID)
+            data.isShowCallingNotification =
+                bundle.getBoolean(CallkitConstants.EXTRA_CALLKIT_CALLING_SHOW, true)
+            data.callingNotificationSubtitle =
+                bundle.getString(CallkitConstants.EXTRA_CALLKIT_CALLING_SUBTITLE, "")
+            data.isShowHangup =
+                bundle.getBoolean(CallkitConstants.EXTRA_CALLKIT_CALLING_HANG_UP_SHOW, false)
+            data.callingNotificationHangupText =
+                bundle.getString(CallkitConstants.EXTRA_CALLKIT_CALLING_HANG_UP_TEXT, "")
+
+
+            data.extra =
+                bundle.getSerializable(CallkitConstants.EXTRA_CALLKIT_EXTRA) as HashMap<String, Any?>
+            data.headers =
+                bundle.getSerializable(CallkitConstants.EXTRA_CALLKIT_HEADERS) as HashMap<String, Any?>
+
+            data.isCustomNotification = bundle.getBoolean(
+                CallkitConstants.EXTRA_CALLKIT_IS_CUSTOM_NOTIFICATION,
+                false
+            )
+            data.isCustomSmallExNotification = bundle.getBoolean(
+                CallkitConstants.EXTRA_CALLKIT_IS_CUSTOM_SMALL_EX_NOTIFICATION,
+                false
+            )
+            data.isShowLogo = bundle.getBoolean(
+                CallkitConstants.EXTRA_CALLKIT_IS_SHOW_LOGO,
+                false
+            )
+            data.logoUrl =
+                bundle.getString(CallkitConstants.EXTRA_CALLKIT_LOGO_URL, "")
+            data.isShowCallID = bundle.getBoolean(
+                CallkitConstants.EXTRA_CALLKIT_IS_SHOW_CALL_ID,
+                false
+            )
+            data.ringtonePath = bundle.getString(
+                CallkitConstants.EXTRA_CALLKIT_RINGTONE_PATH,
+                ""
+            )
+            data.backgroundColor = bundle.getString(
+                CallkitConstants.EXTRA_CALLKIT_BACKGROUND_COLOR,
+                "#0955fa"
+            )
+            data.backgroundUrl =
+                bundle.getString(CallkitConstants.EXTRA_CALLKIT_BACKGROUND_URL, "")
+            data.actionColor = bundle.getString(
+                CallkitConstants.EXTRA_CALLKIT_ACTION_COLOR,
+                "#4CAF50"
+            )
+            data.textColor = bundle.getString(
+                CallkitConstants.EXTRA_CALLKIT_TEXT_COLOR,
+                "#FFFFFF"
+            )
+            data.from =
+                bundle.getString(CallkitConstants.EXTRA_CALLKIT_ACTION_FROM, "")
+
+            data.incomingCallNotificationChannelName = bundle.getString(
+                CallkitConstants.EXTRA_CALLKIT_INCOMING_CALL_NOTIFICATION_CHANNEL_NAME
+            )
+            data.missedCallNotificationChannelName = bundle.getString(
+                CallkitConstants.EXTRA_CALLKIT_MISSED_CALL_NOTIFICATION_CHANNEL_NAME
+            )
+            data.isShowFullLockedScreen = bundle.getBoolean(
+                CallkitConstants.EXTRA_CALLKIT_IS_SHOW_FULL_LOCKED_SCREEN,
+                true
+            )
+            return data
+        }
+    }
+
+}

+ 83 - 0
android/src/main/kotlin/com/hiennv/flutter_callkit_incoming/CallkitConstants.kt

@@ -0,0 +1,83 @@
+package com.hiennv.flutter_callkit_incoming
+
+object CallkitConstants {
+    const val ACTION_CALL_INCOMING =
+        "com.hiennv.flutter_callkit_incoming.ACTION_CALL_INCOMING"
+    const val ACTION_CALL_START = "com.hiennv.flutter_callkit_incoming.ACTION_CALL_START"
+    const val ACTION_CALL_ACCEPT =
+        "com.hiennv.flutter_callkit_incoming.ACTION_CALL_ACCEPT"
+    const val ACTION_CALL_DECLINE =
+        "com.hiennv.flutter_callkit_incoming.ACTION_CALL_DECLINE"
+    const val ACTION_CALL_ENDED =
+        "com.hiennv.flutter_callkit_incoming.ACTION_CALL_ENDED"
+    const val ACTION_CALL_TOGGLE_MUTE =
+        "com.hiennv.flutter_callkit_incoming.ACTION_CALL_TOGGLE_MUTE"
+    const val ACTION_CALL_TOGGLE_HOLD =
+        "com.hiennv.flutter_callkit_incoming.ACTION_CALL_TOGGLE_HOLD"
+    const val ACTION_CALL_TIMEOUT =
+        "com.hiennv.flutter_callkit_incoming.ACTION_CALL_TIMEOUT"
+    const val ACTION_CALL_CALLBACK =
+        "com.hiennv.flutter_callkit_incoming.ACTION_CALL_CALLBACK"
+    const val ACTION_CALL_CUSTOM =
+        "com.hiennv.flutter_callkit_incoming.ACTION_CALL_CUSTOM"
+    const val ACTION_CALL_AUDIO_STATE_CHANGE =
+        "com.hiennv.flutter_callkit_incoming.ACTION_CALL_AUDIO_STATE_CHANGE"
+    const val ACTION_CALL_HELD = "com.hiennv.flutter_callkit_incoming.ACTION_CALL_HELD"
+    const val ACTION_CALL_UNHELD = "com.hiennv.flutter_callkit_incoming.ACTION_CALL_UNHELD"
+
+
+    const val EXTRA_CALLKIT_INCOMING_DATA = "EXTRA_CALLKIT_INCOMING_DATA"
+
+    const val EXTRA_CALLKIT_ID = "EXTRA_CALLKIT_ID"
+    const val EXTRA_CALLKIT_NAME_CALLER = "EXTRA_CALLKIT_NAME_CALLER"
+    const val EXTRA_CALLKIT_APP_NAME = "EXTRA_CALLKIT_APP_NAME"
+    const val EXTRA_CALLKIT_HANDLE = "EXTRA_CALLKIT_HANDLE"
+    const val EXTRA_CALLKIT_TYPE = "EXTRA_CALLKIT_TYPE"
+    const val EXTRA_CALLKIT_AVATAR = "EXTRA_CALLKIT_AVATAR"
+    const val EXTRA_CALLKIT_DURATION = "EXTRA_CALLKIT_DURATION"
+    const val EXTRA_CALLKIT_TEXT_ACCEPT = "EXTRA_CALLKIT_TEXT_ACCEPT"
+    const val EXTRA_CALLKIT_TEXT_DECLINE = "EXTRA_CALLKIT_TEXT_DECLINE"
+
+    const val EXTRA_CALLKIT_MISSED_CALL_ID = "EXTRA_CALLKIT_MISSED_CALL_ID"
+    const val EXTRA_CALLKIT_MISSED_CALL_SHOW = "EXTRA_CALLKIT_MISSED_CALL_SHOW"
+    const val EXTRA_CALLKIT_MISSED_CALL_COUNT = "EXTRA_CALLKIT_MISSED_CALL_COUNT"
+    const val EXTRA_CALLKIT_MISSED_CALL_SUBTITLE = "EXTRA_CALLKIT_MISSED_CALL_SUBTITLE"
+    const val EXTRA_CALLKIT_MISSED_CALL_CALLBACK_SHOW = "EXTRA_CALLKIT_MISSED_CALL_CALLBACK_SHOW"
+    const val EXTRA_CALLKIT_MISSED_CALL_CALLBACK_TEXT =
+        "EXTRA_CALLKIT_MISSED_CALL_CALLBACK_TEXT"
+
+    const val EXTRA_CALLKIT_CALLING_ID = "EXTRA_CALLKIT_CALLING_ID"
+    const val EXTRA_CALLKIT_CALLING_SHOW = "EXTRA_CALLKIT_CALLING_SHOW"
+    const val EXTRA_CALLKIT_CALLING_SUBTITLE = "EXTRA_CALLKIT_CALLING_SUBTITLE"
+    const val EXTRA_CALLKIT_CALLING_HANG_UP_SHOW = "EXTRA_CALLKIT_CALLING_HANG_UP_SHOW"
+    const val EXTRA_CALLKIT_CALLING_HANG_UP_TEXT =
+        "EXTRA_CALLKIT_CALLING_HANG_UP_TEXT"
+    const val EXTRA_CALLKIT_CALLING_TAP_OPEN_TEXT =
+        "EXTRA_CALLKIT_CALLING_TAP_OPEN_TEXT"
+
+    const val EXTRA_CALLKIT_EXTRA = "EXTRA_CALLKIT_EXTRA"
+    const val EXTRA_CALLKIT_HEADERS = "EXTRA_CALLKIT_HEADERS"
+    const val EXTRA_CALLKIT_IS_CUSTOM_NOTIFICATION = "EXTRA_CALLKIT_IS_CUSTOM_NOTIFICATION"
+    const val EXTRA_CALLKIT_IS_CUSTOM_SMALL_EX_NOTIFICATION =
+        "EXTRA_CALLKIT_IS_CUSTOM_SMALL_EX_NOTIFICATION"
+    const val EXTRA_CALLKIT_IS_SHOW_LOGO = "EXTRA_CALLKIT_IS_SHOW_LOGO"
+    const val EXTRA_CALLKIT_LOGO_URL = "EXTRA_CALLKIT_LOGO_URL"
+    const val EXTRA_CALLKIT_IS_SHOW_CALL_ID = "EXTRA_CALLKIT_IS_SHOW_CALL_ID"
+    const val EXTRA_CALLKIT_RINGTONE_PATH = "EXTRA_CALLKIT_RINGTONE_PATH"
+    const val EXTRA_CALLKIT_BACKGROUND_COLOR = "EXTRA_CALLKIT_BACKGROUND_COLOR"
+    const val EXTRA_CALLKIT_BACKGROUND_URL = "EXTRA_CALLKIT_BACKGROUND_URL"
+    const val EXTRA_CALLKIT_ACTION_COLOR = "EXTRA_CALLKIT_ACTION_COLOR"
+    const val EXTRA_CALLKIT_TEXT_COLOR = "EXTRA_CALLKIT_TEXT_COLOR"
+    const val EXTRA_CALLKIT_INCOMING_CALL_NOTIFICATION_CHANNEL_NAME =
+        "EXTRA_CALLKIT_INCOMING_CALL_NOTIFICATION_CHANNEL_NAME"
+    const val EXTRA_CALLKIT_MISSED_CALL_NOTIFICATION_CHANNEL_NAME =
+        "EXTRA_CALLKIT_MISSED_CALL_NOTIFICATION_CHANNEL_NAME"
+    const val EXTRA_CALLKIT_ONGOING_CALL_NOTIFICATION_CHANNEL_NAME =
+        "EXTRA_CALLKIT_ONGOING_CALL_NOTIFICATION_CHANNEL_NAME"
+
+    const val EXTRA_CALLKIT_ACTION_FROM = "EXTRA_CALLKIT_ACTION_FROM"
+
+    const val EXTRA_CALLKIT_IS_SHOW_FULL_LOCKED_SCREEN = "EXTRA_CALLKIT_IS_SHOW_FULL_LOCKED_SCREEN"
+    const val EXTRA_CALLKIT_IS_IMPORTANT = "EXTRA_CALLKIT_IS_IMPORTANT"
+    const val EXTRA_CALLKIT_IS_BOT = "EXTRA_CALLKIT_IS_BOT"
+}

+ 381 - 0
android/src/main/kotlin/com/hiennv/flutter_callkit_incoming/CallkitIncomingActivity.kt

@@ -0,0 +1,381 @@
+package com.hiennv.flutter_callkit_incoming
+
+import android.app.Activity
+import android.app.ActivityManager
+import android.app.KeyguardManager
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import android.content.IntentFilter
+import android.content.pm.ActivityInfo
+import android.graphics.Color
+import android.os.Build
+import android.os.Bundle
+import android.os.Handler
+import android.os.Looper
+import android.view.View
+import android.view.Window
+import android.view.WindowManager
+import android.view.animation.AnimationUtils
+import android.widget.ImageView
+import android.widget.LinearLayout
+import android.widget.TextView
+import com.hiennv.flutter_callkit_incoming.widgets.RippleRelativeLayout
+import com.squareup.picasso.Picasso
+import de.hdodenhof.circleimageview.CircleImageView
+import kotlin.math.abs
+import okhttp3.OkHttpClient
+import com.squareup.picasso.OkHttp3Downloader
+import android.view.ViewGroup.MarginLayoutParams
+import android.os.PowerManager
+import android.text.TextUtils
+import android.util.Log
+
+
+class CallkitIncomingActivity : Activity() {
+
+    companion object {
+
+        private const val ACTION_ENDED_CALL_INCOMING =
+            "com.hiennv.flutter_callkit_incoming.ACTION_ENDED_CALL_INCOMING"
+
+        fun getIntent(context: Context, data: Bundle) =
+            Intent(CallkitConstants.ACTION_CALL_INCOMING).apply {
+                action = "${context.packageName}.${CallkitConstants.ACTION_CALL_INCOMING}"
+                putExtra(CallkitConstants.EXTRA_CALLKIT_INCOMING_DATA, data)
+                flags = Intent.FLAG_ACTIVITY_NEW_TASK
+            }
+
+        fun getIntentEnded(context: Context, isAccepted: Boolean): Intent {
+            val intent = Intent("${context.packageName}.${ACTION_ENDED_CALL_INCOMING}")
+            intent.putExtra("ACCEPTED", isAccepted)
+            return intent
+        }
+    }
+
+    inner class EndedCallkitIncomingBroadcastReceiver : BroadcastReceiver() {
+        override fun onReceive(context: Context, intent: Intent) {
+            if (!isFinishing) {
+                val isAccepted = intent.getBooleanExtra("ACCEPTED", false)
+                if (isAccepted) {
+                    finishDelayed()
+                } else {
+                    finishTask()
+                }
+            }
+        }
+    }
+
+    private var endedCallkitIncomingBroadcastReceiver = EndedCallkitIncomingBroadcastReceiver()
+
+    private lateinit var ivBackground: ImageView
+    private lateinit var llBackgroundAnimation: RippleRelativeLayout
+
+    private lateinit var tvNameCaller: TextView
+    private lateinit var tvNumber: TextView
+    private lateinit var ivLogo: ImageView
+    private lateinit var ivAvatar: CircleImageView
+
+    private lateinit var llAction: LinearLayout
+    private lateinit var ivAcceptCall: ImageView
+    private lateinit var tvAccept: TextView
+
+    private lateinit var ivDeclineCall: ImageView
+    private lateinit var tvDecline: TextView
+
+    @Suppress("DEPRECATION")
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+        requestedOrientation = if (!Utils.isTablet(this@CallkitIncomingActivity)) {
+            ActivityInfo.SCREEN_ORIENTATION_PORTRAIT
+        } else {
+            ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED
+        }
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
+            window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
+            setTurnScreenOn(true)
+            setShowWhenLocked(true)
+        } else {
+            window.addFlags(WindowManager.LayoutParams.FLAG_KEEP_SCREEN_ON)
+            window.addFlags(WindowManager.LayoutParams.FLAG_TURN_SCREEN_ON)
+            window.addFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED)
+            window.addFlags(WindowManager.LayoutParams.FLAG_DISMISS_KEYGUARD)
+        }
+        transparentStatusAndNavigation()
+        setContentView(R.layout.activity_callkit_incoming)
+        initView()
+        incomingData(intent)
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) {
+            registerReceiver(
+                endedCallkitIncomingBroadcastReceiver,
+                IntentFilter("${packageName}.${ACTION_ENDED_CALL_INCOMING}"),
+                Context.RECEIVER_EXPORTED,
+            )
+        } else {
+            registerReceiver(
+                endedCallkitIncomingBroadcastReceiver,
+                IntentFilter("${packageName}.${ACTION_ENDED_CALL_INCOMING}")
+            )
+        }
+    }
+
+    private fun wakeLockRequest(duration: Long) {
+
+        val pm = applicationContext.getSystemService(POWER_SERVICE) as PowerManager
+        val wakeLock = pm.newWakeLock(
+            PowerManager.SCREEN_BRIGHT_WAKE_LOCK or PowerManager.FULL_WAKE_LOCK or PowerManager.ACQUIRE_CAUSES_WAKEUP,
+            "Callkit:PowerManager"
+        )
+        wakeLock.acquire(duration)
+    }
+
+    private fun transparentStatusAndNavigation() {
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT && Build.VERSION.SDK_INT < Build.VERSION_CODES.LOLLIPOP) {
+            setWindowFlag(
+                WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS
+                        or WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION, true
+            )
+        }
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
+            window.decorView.systemUiVisibility = (View.SYSTEM_UI_FLAG_LAYOUT_STABLE
+                    or View.SYSTEM_UI_FLAG_LAYOUT_FULLSCREEN
+                    or View.SYSTEM_UI_FLAG_LAYOUT_HIDE_NAVIGATION)
+        }
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+            setWindowFlag(
+                (WindowManager.LayoutParams.FLAG_TRANSLUCENT_STATUS
+                        or WindowManager.LayoutParams.FLAG_TRANSLUCENT_NAVIGATION), false
+            )
+            window.statusBarColor = Color.TRANSPARENT
+            window.navigationBarColor = Color.TRANSPARENT
+        }
+    }
+
+    private fun setWindowFlag(bits: Int, on: Boolean) {
+        val win: Window = window
+        val winParams: WindowManager.LayoutParams = win.attributes
+        if (on) {
+            winParams.flags = winParams.flags or bits
+        } else {
+            winParams.flags = winParams.flags and bits.inv()
+        }
+        win.attributes = winParams
+    }
+
+
+    private fun incomingData(intent: Intent) {
+        val data = intent.extras?.getBundle(CallkitConstants.EXTRA_CALLKIT_INCOMING_DATA)
+        if (data == null) finish()
+
+        val isShowFullLockedScreen =
+            data?.getBoolean(CallkitConstants.EXTRA_CALLKIT_IS_SHOW_FULL_LOCKED_SCREEN, true)
+        if (isShowFullLockedScreen == true) {
+            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O_MR1) {
+                setShowWhenLocked(true)
+            } else {
+                window.addFlags(WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED)
+            }
+        }
+
+        val textColor = data?.getString(CallkitConstants.EXTRA_CALLKIT_TEXT_COLOR, "#ffffff")
+        val isShowCallID = data?.getBoolean(CallkitConstants.EXTRA_CALLKIT_IS_SHOW_CALL_ID, false)
+        tvNameCaller.text = data?.getString(CallkitConstants.EXTRA_CALLKIT_NAME_CALLER, "")
+        tvNumber.text = data?.getString(CallkitConstants.EXTRA_CALLKIT_HANDLE, "")
+        tvNumber.visibility = if (isShowCallID == true) View.VISIBLE else View.INVISIBLE
+
+        try {
+            tvNameCaller.setTextColor(Color.parseColor(textColor))
+            tvNumber.setTextColor(Color.parseColor(textColor))
+        } catch (error: Exception) {
+        }
+
+        val isShowLogo = data?.getBoolean(CallkitConstants.EXTRA_CALLKIT_IS_SHOW_LOGO, false)
+        ivLogo.visibility = if (isShowLogo == true) View.VISIBLE else View.INVISIBLE
+        var logoUrl = data?.getString(CallkitConstants.EXTRA_CALLKIT_LOGO_URL, "")
+        if (logoUrl != null && logoUrl.isNotEmpty()) {
+            if (!logoUrl.startsWith("http://", true) && !logoUrl.startsWith("https://", true)) {
+                logoUrl = String.format("file:///android_asset/flutter_assets/%s", logoUrl)
+            }
+            val headers =
+                data?.getSerializable(CallkitConstants.EXTRA_CALLKIT_HEADERS) as HashMap<String, Any?>
+            getPicassoInstance(this@CallkitIncomingActivity, headers)
+                .load(logoUrl)
+                .placeholder(R.drawable.transparent)
+                .error(R.drawable.transparent)
+                .into(ivLogo)
+        }
+
+        val avatarUrl = data?.getString(CallkitConstants.EXTRA_CALLKIT_AVATAR, "")
+        if (avatarUrl != null && avatarUrl.isNotEmpty()) {
+            ivAvatar.visibility = View.VISIBLE
+            val headers =
+                data.getSerializable(CallkitConstants.EXTRA_CALLKIT_HEADERS) as HashMap<String, Any?>
+            getPicassoInstance(this@CallkitIncomingActivity, headers)
+                .load(avatarUrl)
+                .placeholder(R.drawable.ic_default_avatar)
+                .error(R.drawable.ic_default_avatar)
+                .into(ivAvatar)
+        }
+
+        val callType = data?.getInt(CallkitConstants.EXTRA_CALLKIT_TYPE, 0) ?: 0
+        if (callType > 0) {
+            ivAcceptCall.setImageResource(R.drawable.ic_video)
+        }
+        val duration = data?.getLong(CallkitConstants.EXTRA_CALLKIT_DURATION, 0L) ?: 0L
+        wakeLockRequest(duration)
+
+        finishTimeout(data, duration)
+
+        val textAccept = data?.getString(CallkitConstants.EXTRA_CALLKIT_TEXT_ACCEPT, "")
+        tvAccept.text =
+            if (TextUtils.isEmpty(textAccept)) getString(R.string.text_accept) else textAccept
+        val textDecline = data?.getString(CallkitConstants.EXTRA_CALLKIT_TEXT_DECLINE, "")
+        tvDecline.text =
+            if (TextUtils.isEmpty(textDecline)) getString(R.string.text_decline) else textDecline
+
+        try {
+            tvAccept.setTextColor(Color.parseColor(textColor))
+            tvDecline.setTextColor(Color.parseColor(textColor))
+        } catch (error: Exception) {
+        }
+
+        val backgroundColor =
+            data?.getString(CallkitConstants.EXTRA_CALLKIT_BACKGROUND_COLOR, "#0955fa")
+        try {
+            ivBackground.setBackgroundColor(Color.parseColor(backgroundColor))
+        } catch (error: Exception) {
+        }
+        var backgroundUrl = data?.getString(CallkitConstants.EXTRA_CALLKIT_BACKGROUND_URL, "")
+        if (backgroundUrl != null && backgroundUrl.isNotEmpty()) {
+            if (!backgroundUrl.startsWith("http://", true) && !backgroundUrl.startsWith(
+                    "https://",
+                    true
+                )
+            ) {
+                backgroundUrl =
+                    String.format("file:///android_asset/flutter_assets/%s", backgroundUrl)
+            }
+            val headers =
+                data?.getSerializable(CallkitConstants.EXTRA_CALLKIT_HEADERS) as HashMap<String, Any?>
+            getPicassoInstance(this@CallkitIncomingActivity, headers)
+                .load(backgroundUrl)
+                .placeholder(R.drawable.transparent)
+                .error(R.drawable.transparent)
+                .into(ivBackground)
+        }
+    }
+
+    private fun finishTimeout(data: Bundle?, duration: Long) {
+        val currentSystemTime = System.currentTimeMillis()
+        val timeStartCall =
+            data?.getLong(CallkitNotificationManager.EXTRA_TIME_START_CALL, currentSystemTime)
+                ?: currentSystemTime
+
+        val timeOut = duration - abs(currentSystemTime - timeStartCall)
+        Handler(Looper.getMainLooper()).postDelayed({
+            if (!isFinishing) {
+                finishTask()
+            }
+        }, timeOut)
+    }
+
+    private fun initView() {
+        ivBackground = findViewById(R.id.ivBackground)
+        llBackgroundAnimation = findViewById(R.id.llBackgroundAnimation)
+        llBackgroundAnimation.layoutParams.height =
+            Utils.getScreenWidth() + Utils.getStatusBarHeight(this@CallkitIncomingActivity)
+        llBackgroundAnimation.startRippleAnimation()
+
+        tvNameCaller = findViewById(R.id.tvNameCaller)
+        tvNumber = findViewById(R.id.tvNumber)
+        ivLogo = findViewById(R.id.ivLogo)
+        ivAvatar = findViewById(R.id.ivAvatar)
+
+        llAction = findViewById(R.id.llAction)
+
+        val params = llAction.layoutParams as MarginLayoutParams
+        params.setMargins(0, 0, 0, Utils.getNavigationBarHeight(this@CallkitIncomingActivity))
+        llAction.layoutParams = params
+
+        ivAcceptCall = findViewById(R.id.ivAcceptCall)
+        tvAccept = findViewById(R.id.tvAccept)
+        ivDeclineCall = findViewById(R.id.ivDeclineCall)
+        tvDecline = findViewById(R.id.tvDecline)
+        animateAcceptCall()
+
+        ivAcceptCall.setOnClickListener {
+            onAcceptClick()
+        }
+        ivDeclineCall.setOnClickListener {
+            onDeclineClick()
+        }
+    }
+
+    private fun animateAcceptCall() {
+        val shakeAnimation =
+            AnimationUtils.loadAnimation(this@CallkitIncomingActivity, R.anim.shake_anim)
+        ivAcceptCall.animation = shakeAnimation
+    }
+
+
+    private fun onAcceptClick() {
+        val data = intent.extras?.getBundle(CallkitConstants.EXTRA_CALLKIT_INCOMING_DATA)
+        val acceptIntent =
+            TransparentActivity.getIntent(this, CallkitConstants.ACTION_CALL_ACCEPT, data)
+        startActivity(acceptIntent)
+
+        dismissKeyguard()
+        finish()
+    }
+
+    private fun dismissKeyguard() {
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+            val keyguardManager = getSystemService(Context.KEYGUARD_SERVICE) as KeyguardManager
+            keyguardManager.requestDismissKeyguard(this, null)
+        }
+    }
+
+    private fun onDeclineClick() {
+        val data = intent.extras?.getBundle(CallkitConstants.EXTRA_CALLKIT_INCOMING_DATA)
+        val intent =
+            CallkitIncomingBroadcastReceiver.getIntentDecline(this@CallkitIncomingActivity, data)
+        sendBroadcast(intent)
+        finishTask()
+    }
+
+    private fun finishDelayed() {
+        Handler(Looper.getMainLooper()).postDelayed({
+            finishTask()
+        }, 1000)
+    }
+
+    private fun finishTask() {
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+            finishAndRemoveTask()
+        } else {
+            finish()
+        }
+    }
+
+    private fun getPicassoInstance(context: Context, headers: HashMap<String, Any?>): Picasso {
+        val client = OkHttpClient.Builder()
+            .addNetworkInterceptor { chain ->
+                val newRequestBuilder: okhttp3.Request.Builder = chain.request().newBuilder()
+                for ((key, value) in headers) {
+                    newRequestBuilder.addHeader(key, value.toString())
+                }
+                chain.proceed(newRequestBuilder.build())
+            }
+            .build()
+        return Picasso.Builder(context)
+            .downloader(OkHttp3Downloader(client))
+            .build()
+    }
+
+    override fun onDestroy() {
+        unregisterReceiver(endedCallkitIncomingBroadcastReceiver)
+        super.onDestroy()
+    }
+
+    override fun onBackPressed() {}
+}

+ 249 - 0
android/src/main/kotlin/com/hiennv/flutter_callkit_incoming/CallkitIncomingBroadcastReceiver.kt

@@ -0,0 +1,249 @@
+package com.hiennv.flutter_callkit_incoming
+
+import android.annotation.SuppressLint
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import android.os.Build
+import android.os.Bundle
+import android.util.Log
+
+class CallkitIncomingBroadcastReceiver : BroadcastReceiver() {
+
+    companion object {
+        private const val TAG = "CallkitIncomingReceiver"
+        var silenceEvents = false
+
+        fun getIntent(context: Context, action: String, data: Bundle?) =
+            Intent(context, CallkitIncomingBroadcastReceiver::class.java).apply {
+                this.action = "${context.packageName}.${action}"
+                putExtra(CallkitConstants.EXTRA_CALLKIT_INCOMING_DATA, data)
+            }
+
+        fun getIntentIncoming(context: Context, data: Bundle?) =
+            Intent(context, CallkitIncomingBroadcastReceiver::class.java).apply {
+                action = "${context.packageName}.${CallkitConstants.ACTION_CALL_INCOMING}"
+                putExtra(CallkitConstants.EXTRA_CALLKIT_INCOMING_DATA, data)
+            }
+
+        fun getIntentStart(context: Context, data: Bundle?) =
+            Intent(context, CallkitIncomingBroadcastReceiver::class.java).apply {
+                action = "${context.packageName}.${CallkitConstants.ACTION_CALL_START}"
+                putExtra(CallkitConstants.EXTRA_CALLKIT_INCOMING_DATA, data)
+            }
+
+        fun getIntentAccept(context: Context, data: Bundle?) =
+            Intent(context, CallkitIncomingBroadcastReceiver::class.java).apply {
+                action = "${context.packageName}.${CallkitConstants.ACTION_CALL_ACCEPT}"
+                putExtra(CallkitConstants.EXTRA_CALLKIT_INCOMING_DATA, data)
+            }
+
+        fun getIntentDecline(context: Context, data: Bundle?) =
+            Intent(context, CallkitIncomingBroadcastReceiver::class.java).apply {
+                action = "${context.packageName}.${CallkitConstants.ACTION_CALL_DECLINE}"
+                putExtra(CallkitConstants.EXTRA_CALLKIT_INCOMING_DATA, data)
+            }
+
+        fun getIntentEnded(context: Context, data: Bundle?) =
+            Intent(context, CallkitIncomingBroadcastReceiver::class.java).apply {
+                action = "${context.packageName}.${CallkitConstants.ACTION_CALL_ENDED}"
+                putExtra(CallkitConstants.EXTRA_CALLKIT_INCOMING_DATA, data)
+            }
+
+        fun getIntentTimeout(context: Context, data: Bundle?) =
+            Intent(context, CallkitIncomingBroadcastReceiver::class.java).apply {
+                action = "${context.packageName}.${CallkitConstants.ACTION_CALL_TIMEOUT}"
+                putExtra(CallkitConstants.EXTRA_CALLKIT_INCOMING_DATA, data)
+            }
+
+        fun getIntentCallback(context: Context, data: Bundle?) =
+            Intent(context, CallkitIncomingBroadcastReceiver::class.java).apply {
+                action = "${context.packageName}.${CallkitConstants.ACTION_CALL_CALLBACK}"
+                putExtra(CallkitConstants.EXTRA_CALLKIT_INCOMING_DATA, data)
+            }
+
+        fun getIntentHeldByCell(context: Context, data: Bundle?) =
+            Intent(context, CallkitIncomingBroadcastReceiver::class.java).apply {
+                action = "${context.packageName}.${CallkitConstants.ACTION_CALL_HELD}"
+                putExtra(CallkitConstants.EXTRA_CALLKIT_INCOMING_DATA, data)
+            }
+
+        fun getIntentUnHeldByCell(context: Context, data: Bundle?) =
+            Intent(context, CallkitIncomingBroadcastReceiver::class.java).apply {
+                action = "${context.packageName}.${CallkitConstants.ACTION_CALL_UNHELD}"
+                putExtra(CallkitConstants.EXTRA_CALLKIT_INCOMING_DATA, data)
+            }
+    }
+
+
+    @SuppressLint("MissingPermission")
+    override fun onReceive(context: Context, intent: Intent) {
+        val callkitNotificationManager = CallkitNotificationManager(context)
+        val action = intent.action ?: return
+        val data = intent.extras?.getBundle(CallkitConstants.EXTRA_CALLKIT_INCOMING_DATA) ?: return
+        when (action) {
+            "${context.packageName}.${CallkitConstants.ACTION_CALL_INCOMING}" -> {
+                try {
+                    callkitNotificationManager.createNotificationChanel(data)
+                    callkitNotificationManager.showIncomingNotification(data)
+                    sendEventFlutter(CallkitConstants.ACTION_CALL_INCOMING, data)
+                    addCall(context, Data.fromBundle(data))
+                    if (callkitNotificationManager.incomingChannelEnabled()) {
+                        val soundPlayerServiceIntent =
+                            Intent(context, CallkitSoundPlayerService::class.java)
+                        soundPlayerServiceIntent.putExtras(data)
+                        context.startService(soundPlayerServiceIntent)
+                    }
+                } catch (error: Exception) {
+                    Log.e(TAG, null, error)
+                }
+            }
+
+            "${context.packageName}.${CallkitConstants.ACTION_CALL_START}" -> {
+                try {
+                    callkitNotificationManager.createNotificationChanel(data)
+                    sendEventFlutter(CallkitConstants.ACTION_CALL_START, data)
+                    if (data.getBoolean(CallkitConstants.EXTRA_CALLKIT_CALLING_SHOW, true)) {
+                        val onGoingNotificationIntent =
+                            Intent(context, OngoingNotificationService::class.java);
+                        onGoingNotificationIntent.putExtras(data)
+                        context.startService(onGoingNotificationIntent)
+                    }
+                    addCall(context, Data.fromBundle(data), true)
+                } catch (error: Exception) {
+                    Log.e(TAG, null, error)
+                }
+            }
+
+            "${context.packageName}.${CallkitConstants.ACTION_CALL_ACCEPT}" -> {
+                try {
+                    sendEventFlutter(CallkitConstants.ACTION_CALL_ACCEPT, data)
+                    context.stopService(Intent(context, CallkitSoundPlayerService::class.java))
+                    callkitNotificationManager.clearIncomingNotification(data, true)
+                    // show ongoing call when call is accepted
+                    if (data.getBoolean(CallkitConstants.EXTRA_CALLKIT_CALLING_SHOW, true)) {
+                        val onGoingNotificationIntent =
+                            Intent(context, OngoingNotificationService::class.java);
+                        onGoingNotificationIntent.putExtras(data)
+                        context.startService(onGoingNotificationIntent)
+                    }
+                    addCall(context, Data.fromBundle(data), true)
+                } catch (error: Exception) {
+                    Log.e(TAG, null, error)
+                }
+            }
+
+            "${context.packageName}.${CallkitConstants.ACTION_CALL_DECLINE}" -> {
+                try {
+                    sendEventFlutter(CallkitConstants.ACTION_CALL_DECLINE, data)
+                    context.stopService(Intent(context, CallkitSoundPlayerService::class.java))
+                    callkitNotificationManager.clearIncomingNotification(data, false)
+                    removeCall(context, Data.fromBundle(data))
+                } catch (error: Exception) {
+                    Log.e(TAG, null, error)
+                }
+            }
+
+            "${context.packageName}.${CallkitConstants.ACTION_CALL_ENDED}" -> {
+                try {
+                    sendEventFlutter(CallkitConstants.ACTION_CALL_ENDED, data)
+                    context.stopService(Intent(context, OngoingNotificationService::class.java))
+                    context.stopService(Intent(context, CallkitSoundPlayerService::class.java))
+                    callkitNotificationManager.clearIncomingNotification(data, false)
+                    removeCall(context, Data.fromBundle(data))
+                } catch (error: Exception) {
+                    Log.e(TAG, null, error)
+                }
+            }
+
+            "${context.packageName}.${CallkitConstants.ACTION_CALL_TIMEOUT}" -> {
+                try {
+                    sendEventFlutter(CallkitConstants.ACTION_CALL_TIMEOUT, data)
+                    context.stopService(Intent(context, CallkitSoundPlayerService::class.java))
+                    if (data.getBoolean(CallkitConstants.EXTRA_CALLKIT_MISSED_CALL_SHOW, true)) {
+                        callkitNotificationManager.showMissCallNotification(data)
+                    }
+                    removeCall(context, Data.fromBundle(data))
+                } catch (error: Exception) {
+                    Log.e(TAG, null, error)
+                }
+            }
+
+            "${context.packageName}.${CallkitConstants.ACTION_CALL_CALLBACK}" -> {
+                try {
+                    callkitNotificationManager.clearMissCallNotification(data)
+                    sendEventFlutter(CallkitConstants.ACTION_CALL_CALLBACK, data)
+                    if (Build.VERSION.SDK_INT < Build.VERSION_CODES.S) {
+                        val closeNotificationPanel = Intent(Intent.ACTION_CLOSE_SYSTEM_DIALOGS)
+                        context.sendBroadcast(closeNotificationPanel)
+                    }
+                } catch (error: Exception) {
+                    Log.e(TAG, null, error)
+                }
+            }
+        }
+    }
+
+    private fun sendEventFlutter(event: String, data: Bundle) {
+        if (silenceEvents) return
+
+        val android = mapOf(
+            "isCustomNotification" to data.getBoolean(
+                CallkitConstants.EXTRA_CALLKIT_IS_CUSTOM_NOTIFICATION,
+                false
+            ),
+            "isCustomSmallExNotification" to data.getBoolean(
+                CallkitConstants.EXTRA_CALLKIT_IS_CUSTOM_SMALL_EX_NOTIFICATION,
+                false
+            ),
+            "ringtonePath" to data.getString(CallkitConstants.EXTRA_CALLKIT_RINGTONE_PATH, ""),
+            "backgroundColor" to data.getString(
+                CallkitConstants.EXTRA_CALLKIT_BACKGROUND_COLOR,
+                ""
+            ),
+            "backgroundUrl" to data.getString(CallkitConstants.EXTRA_CALLKIT_BACKGROUND_URL, ""),
+            "actionColor" to data.getString(CallkitConstants.EXTRA_CALLKIT_ACTION_COLOR, ""),
+            "textColor" to data.getString(CallkitConstants.EXTRA_CALLKIT_TEXT_COLOR, ""),
+            "incomingCallNotificationChannelName" to data.getString(
+                CallkitConstants.EXTRA_CALLKIT_INCOMING_CALL_NOTIFICATION_CHANNEL_NAME,
+                ""
+            ),
+            "missedCallNotificationChannelName" to data.getString(
+                CallkitConstants.EXTRA_CALLKIT_MISSED_CALL_NOTIFICATION_CHANNEL_NAME,
+                ""
+            ),
+            "isImportant" to data.getBoolean(CallkitConstants.EXTRA_CALLKIT_IS_IMPORTANT, false),
+            "isBot" to data.getBoolean(CallkitConstants.EXTRA_CALLKIT_IS_BOT, false),
+        )
+        val notification = mapOf(
+            "id" to data.getInt(CallkitConstants.EXTRA_CALLKIT_MISSED_CALL_ID),
+            "showNotification" to data.getBoolean(CallkitConstants.EXTRA_CALLKIT_MISSED_CALL_SHOW),
+            "count" to data.getInt(CallkitConstants.EXTRA_CALLKIT_MISSED_CALL_COUNT),
+            "subtitle" to data.getString(CallkitConstants.EXTRA_CALLKIT_MISSED_CALL_SUBTITLE),
+            "callbackText" to data.getString(CallkitConstants.EXTRA_CALLKIT_MISSED_CALL_CALLBACK_TEXT),
+            "isShowCallback" to data.getBoolean(CallkitConstants.EXTRA_CALLKIT_MISSED_CALL_CALLBACK_SHOW),
+        )
+        val callingNotification = mapOf(
+            "id" to data.getInt(CallkitConstants.EXTRA_CALLKIT_CALLING_ID),
+            "showNotification" to data.getBoolean(CallkitConstants.EXTRA_CALLKIT_CALLING_SHOW),
+            "subtitle" to data.getString(CallkitConstants.EXTRA_CALLKIT_CALLING_SUBTITLE),
+            "callbackText" to data.getString(CallkitConstants.EXTRA_CALLKIT_CALLING_HANG_UP_TEXT),
+            "isShowCallback" to data.getBoolean(CallkitConstants.EXTRA_CALLKIT_CALLING_HANG_UP_SHOW),
+        )
+        val forwardData = mapOf(
+            "id" to data.getString(CallkitConstants.EXTRA_CALLKIT_ID, ""),
+            "nameCaller" to data.getString(CallkitConstants.EXTRA_CALLKIT_NAME_CALLER, ""),
+            "avatar" to data.getString(CallkitConstants.EXTRA_CALLKIT_AVATAR, ""),
+            "number" to data.getString(CallkitConstants.EXTRA_CALLKIT_HANDLE, ""),
+            "type" to data.getInt(CallkitConstants.EXTRA_CALLKIT_TYPE, 0),
+            "duration" to data.getLong(CallkitConstants.EXTRA_CALLKIT_DURATION, 0L),
+            "textAccept" to data.getString(CallkitConstants.EXTRA_CALLKIT_TEXT_ACCEPT, ""),
+            "textDecline" to data.getString(CallkitConstants.EXTRA_CALLKIT_TEXT_DECLINE, ""),
+            "extra" to data.getSerializable(CallkitConstants.EXTRA_CALLKIT_EXTRA)!!,
+            "missedCallNotification" to notification,
+            "callingNotification" to callingNotification,
+            "android" to android
+        )
+        FlutterCallkitIncomingPlugin.sendEvent(event, forwardData)
+    }
+}

+ 664 - 0
android/src/main/kotlin/com/hiennv/flutter_callkit_incoming/CallkitNotificationManager.kt

@@ -0,0 +1,664 @@
+package com.hiennv.flutter_callkit_incoming
+
+import android.Manifest
+import android.annotation.SuppressLint
+import android.app.Activity
+import android.app.Notification
+import android.app.NotificationChannel
+import android.app.NotificationManager
+import android.app.PendingIntent
+import android.content.Context
+import android.content.DialogInterface
+import android.content.Intent
+import android.content.pm.PackageManager
+import android.graphics.Bitmap
+import android.graphics.Color
+import android.graphics.drawable.Drawable
+import android.media.RingtoneManager
+import android.net.Uri
+import android.os.Build
+import android.os.Bundle
+import android.os.Handler
+import android.os.Looper
+import android.provider.Settings
+import android.text.TextUtils
+import android.view.View
+import android.widget.RemoteViews
+import androidx.appcompat.app.AlertDialog
+import androidx.core.app.ActivityCompat
+import androidx.core.app.NotificationCompat
+import androidx.core.app.NotificationManagerCompat
+import androidx.core.app.Person
+import com.hiennv.flutter_callkit_incoming.widgets.CircleTransform
+import com.squareup.picasso.OkHttp3Downloader
+import com.squareup.picasso.Picasso
+import com.squareup.picasso.Target
+import okhttp3.OkHttpClient
+
+
+class CallkitNotificationManager(private val context: Context) {
+
+    companion object {
+        const val PERMISSION_NOTIFICATION_REQUEST_CODE = 6969
+
+        const val EXTRA_TIME_START_CALL = "EXTRA_TIME_START_CALL"
+
+        private const val NOTIFICATION_CHANNEL_ID_INCOMING = "callkit_incoming_channel_id"
+        const val NOTIFICATION_CHANNEL_ID_ONGOING = "callkit_ongoing_channel_id"
+        private const val NOTIFICATION_CHANNEL_ID_MISSED = "callkit_missed_channel_id"
+    }
+
+    private lateinit var notificationBuilder: NotificationCompat.Builder
+    private var notificationViews: RemoteViews? = null
+    private var notificationSmallViews: RemoteViews? = null
+    private var notificationId: Int = 9696
+    private var dataNotificationPermission: Map<String, Any> = HashMap()
+
+    @SuppressLint("MissingPermission")
+    private fun createAvatarTargetDefault(notificationId: Int): Target {
+        return object : Target {
+            override fun onBitmapLoaded(bitmap: Bitmap?, from: Picasso.LoadedFrom?) {
+                notificationBuilder.setLargeIcon(bitmap)
+                getNotificationManager().notify(notificationId, notificationBuilder.build())
+            }
+
+            override fun onBitmapFailed(e: Exception?, errorDrawable: Drawable?) {
+            }
+
+            override fun onPrepareLoad(placeHolderDrawable: Drawable?) {
+            }
+        }
+
+    }
+
+    @SuppressLint("MissingPermission")
+    private fun createAvatarTargetCustom(notificationId: Int): Target {
+        return object : Target {
+            override fun onBitmapLoaded(bitmap: Bitmap?, from: Picasso.LoadedFrom?) {
+                notificationViews?.setImageViewBitmap(R.id.ivAvatar, bitmap)
+                notificationViews?.setViewVisibility(R.id.ivAvatar, View.VISIBLE)
+                notificationSmallViews?.setImageViewBitmap(R.id.ivAvatar, bitmap)
+                notificationSmallViews?.setViewVisibility(R.id.ivAvatar, View.VISIBLE)
+                getNotificationManager().notify(notificationId, notificationBuilder.build())
+            }
+
+            override fun onBitmapFailed(e: Exception?, errorDrawable: Drawable?) {
+            }
+
+            override fun onPrepareLoad(placeHolderDrawable: Drawable?) {
+            }
+        }
+    }
+
+    @SuppressLint("MissingPermission")
+    fun showIncomingNotification(data: Bundle) {
+        data.putLong(EXTRA_TIME_START_CALL, System.currentTimeMillis())
+
+        notificationId =
+            data.getString(CallkitConstants.EXTRA_CALLKIT_ID, "callkit_incoming").hashCode()
+        createNotificationChanel(data)
+        notificationBuilder = NotificationCompat.Builder(context, NOTIFICATION_CHANNEL_ID_INCOMING)
+        notificationBuilder.setAutoCancel(false)
+        notificationBuilder.setChannelId(NOTIFICATION_CHANNEL_ID_INCOMING)
+        notificationBuilder.setDefaults(NotificationCompat.DEFAULT_VIBRATE)
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+            notificationBuilder.setCategory(NotificationCompat.CATEGORY_CALL)
+            notificationBuilder.priority = NotificationCompat.PRIORITY_MAX
+        }
+        notificationBuilder.setVisibility(NotificationCompat.VISIBILITY_PUBLIC)
+        notificationBuilder.setOngoing(true)
+        notificationBuilder.setWhen(0)
+        notificationBuilder.setTimeoutAfter(
+            data.getLong(
+                CallkitConstants.EXTRA_CALLKIT_DURATION,
+                0L
+            )
+        )
+        notificationBuilder.setOnlyAlertOnce(true)
+        notificationBuilder.setSound(null)
+        notificationBuilder.setFullScreenIntent(
+            getActivityPendingIntent(notificationId, data), true
+        )
+        notificationBuilder.setContentIntent(getActivityPendingIntent(notificationId, data))
+        notificationBuilder.setDeleteIntent(getTimeOutPendingIntent(notificationId, data))
+        val typeCall = data.getInt(CallkitConstants.EXTRA_CALLKIT_TYPE, -1)
+        var smallIcon = context.applicationInfo.icon
+        if (typeCall > 0) {
+            smallIcon = R.drawable.ic_video
+        } else {
+            if (smallIcon >= 0) {
+                smallIcon = R.drawable.ic_accept
+            }
+        }
+        notificationBuilder.setSmallIcon(smallIcon)
+        val actionColor = data.getString(CallkitConstants.EXTRA_CALLKIT_ACTION_COLOR, "#4CAF50")
+        try {
+            notificationBuilder.color = Color.parseColor(actionColor)
+        } catch (_: Exception) {
+        }
+        notificationBuilder.setChannelId(NOTIFICATION_CHANNEL_ID_INCOMING)
+        notificationBuilder.priority = NotificationCompat.PRIORITY_MAX
+        val isCustomNotification =
+            data.getBoolean(CallkitConstants.EXTRA_CALLKIT_IS_CUSTOM_NOTIFICATION, false)
+        val isCustomSmallExNotification =
+            data.getBoolean(CallkitConstants.EXTRA_CALLKIT_IS_CUSTOM_SMALL_EX_NOTIFICATION, false)
+        if (isCustomNotification) {
+            notificationViews =
+                RemoteViews(context.packageName, R.layout.layout_custom_notification)
+            initNotificationViews(notificationViews!!, data)
+
+            if ((Build.MANUFACTURER.equals(
+                    "Samsung",
+                    ignoreCase = true
+                ) && Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) || isCustomSmallExNotification
+            ) {
+                notificationSmallViews =
+                    RemoteViews(context.packageName, R.layout.layout_custom_small_ex_notification)
+                initNotificationViews(notificationSmallViews!!, data)
+            } else {
+                notificationSmallViews =
+                    RemoteViews(context.packageName, R.layout.layout_custom_small_notification)
+                initNotificationViews(notificationSmallViews!!, data)
+            }
+
+            notificationBuilder.setStyle(NotificationCompat.DecoratedCustomViewStyle())
+            notificationBuilder.setCustomContentView(notificationSmallViews)
+            notificationBuilder.setCustomBigContentView(notificationViews)
+            notificationBuilder.setCustomHeadsUpContentView(notificationSmallViews)
+        } else {
+            notificationBuilder.setContentText(
+                data.getString(
+                    CallkitConstants.EXTRA_CALLKIT_HANDLE,
+                    ""
+                )
+            )
+            val avatarUrl = data.getString(CallkitConstants.EXTRA_CALLKIT_AVATAR, "")
+            if (avatarUrl != null && avatarUrl.isNotEmpty()) {
+                val headers =
+                    data.getSerializable(CallkitConstants.EXTRA_CALLKIT_HEADERS) as HashMap<String, Any?>
+                getPicassoInstance(context, headers).load(avatarUrl)
+                    .into(createAvatarTargetDefault(notificationId))
+            }
+            val caller = data.getString(CallkitConstants.EXTRA_CALLKIT_NAME_CALLER, "")
+            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.P) {
+                val person = Person.Builder()
+                    .setName(caller)
+                    .setImportant(
+                        data.getBoolean(CallkitConstants.EXTRA_CALLKIT_IS_IMPORTANT, false)
+                    )
+                    .setBot(data.getBoolean(CallkitConstants.EXTRA_CALLKIT_IS_BOT, false))
+                    .build()
+                notificationBuilder.setStyle(
+                    NotificationCompat.CallStyle.forIncomingCall(
+                        person,
+                        getDeclinePendingIntent(notificationId, data),
+                        getAcceptPendingIntent(notificationId, data),
+                    )
+                        .setIsVideo(typeCall > 0)
+                )
+            } else {
+                notificationBuilder.setContentTitle(caller)
+                val textDecline = data.getString(CallkitConstants.EXTRA_CALLKIT_TEXT_DECLINE, "")
+                val declineAction: NotificationCompat.Action = NotificationCompat.Action.Builder(
+                    R.drawable.ic_decline,
+                    if (TextUtils.isEmpty(textDecline)) context.getString(R.string.text_decline) else textDecline,
+                    getDeclinePendingIntent(notificationId, data)
+                ).build()
+                notificationBuilder.addAction(declineAction)
+                val textAccept = data.getString(CallkitConstants.EXTRA_CALLKIT_TEXT_ACCEPT, "")
+                val acceptAction: NotificationCompat.Action = NotificationCompat.Action.Builder(
+                    R.drawable.ic_accept,
+                    if (TextUtils.isEmpty(textDecline)) context.getString(R.string.text_accept) else textAccept,
+                    getAcceptPendingIntent(notificationId, data)
+                ).build()
+                notificationBuilder.addAction(acceptAction)
+            }
+        }
+        val notification = notificationBuilder.build()
+        notification.flags = Notification.FLAG_INSISTENT
+        getNotificationManager().notify(notificationId, notification)
+    }
+
+    private fun initNotificationViews(remoteViews: RemoteViews, data: Bundle) {
+        remoteViews.setTextViewText(
+            R.id.tvNameCaller,
+            data.getString(CallkitConstants.EXTRA_CALLKIT_NAME_CALLER, "")
+        )
+        val isShowCallID = data?.getBoolean(CallkitConstants.EXTRA_CALLKIT_IS_SHOW_CALL_ID, false)
+        if (isShowCallID == true) {
+            remoteViews.setTextViewText(
+                R.id.tvNumber,
+                data.getString(CallkitConstants.EXTRA_CALLKIT_HANDLE, "")
+            )
+        }
+        remoteViews.setOnClickPendingIntent(
+            R.id.llDecline,
+            getDeclinePendingIntent(notificationId, data)
+        )
+        val textDecline = data.getString(CallkitConstants.EXTRA_CALLKIT_TEXT_DECLINE, "")
+        remoteViews.setTextViewText(
+            R.id.tvDecline,
+            if (TextUtils.isEmpty(textDecline)) context.getString(R.string.text_decline) else textDecline
+        )
+        remoteViews.setOnClickPendingIntent(
+            R.id.llAccept,
+            getAcceptPendingIntent(notificationId, data)
+        )
+        val textAccept = data.getString(CallkitConstants.EXTRA_CALLKIT_TEXT_ACCEPT, "")
+        remoteViews.setTextViewText(
+            R.id.tvAccept,
+            if (TextUtils.isEmpty(textAccept)) context.getString(R.string.text_accept) else textAccept
+        )
+        val avatarUrl = data.getString(CallkitConstants.EXTRA_CALLKIT_AVATAR, "")
+        if (avatarUrl != null && avatarUrl.isNotEmpty()) {
+            val headers =
+                data.getSerializable(CallkitConstants.EXTRA_CALLKIT_HEADERS) as HashMap<String, Any?>
+            getPicassoInstance(context, headers).load(avatarUrl)
+                .transform(CircleTransform())
+                .into(createAvatarTargetCustom(notificationId))
+        }
+    }
+
+    @SuppressLint("MissingPermission")
+    fun showMissCallNotification(data: Bundle) {
+        val missedNotificationId = data.getInt(
+            CallkitConstants.EXTRA_CALLKIT_MISSED_CALL_ID,
+            data.getString(CallkitConstants.EXTRA_CALLKIT_ID, "callkit_incoming").hashCode() + 1
+        )
+        createNotificationChanel(data);
+        val missedCallSound: Uri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_NOTIFICATION)
+        val typeCall = data.getInt(CallkitConstants.EXTRA_CALLKIT_TYPE, -1)
+        var smallIcon = context.applicationInfo.icon
+        if (typeCall > 0) {
+            smallIcon = R.drawable.ic_video_missed
+        } else {
+            if (smallIcon >= 0) {
+                smallIcon = R.drawable.ic_call_missed
+            }
+        }
+        notificationBuilder = NotificationCompat.Builder(context, NOTIFICATION_CHANNEL_ID_MISSED)
+        notificationBuilder.setChannelId(NOTIFICATION_CHANNEL_ID_MISSED)
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
+                notificationBuilder.setCategory(Notification.CATEGORY_MISSED_CALL)
+            }
+        }
+        val textMissedCall = data.getString(CallkitConstants.EXTRA_CALLKIT_MISSED_CALL_SUBTITLE, "")
+        notificationBuilder.setSubText(if (TextUtils.isEmpty(textMissedCall)) context.getString(R.string.text_missed_call) else textMissedCall)
+        notificationBuilder.setSmallIcon(smallIcon)
+        val isCustomNotification =
+            data.getBoolean(CallkitConstants.EXTRA_CALLKIT_IS_CUSTOM_NOTIFICATION, false)
+        val count = data.getInt(CallkitConstants.EXTRA_CALLKIT_MISSED_CALL_COUNT, 1)
+        if (count > 1) {
+            notificationBuilder.setNumber(count)
+        }
+        if (isCustomNotification) {
+            notificationViews =
+                RemoteViews(context.packageName, R.layout.layout_custom_miss_notification)
+            notificationViews?.setTextViewText(
+                R.id.tvNameCaller,
+                data.getString(CallkitConstants.EXTRA_CALLKIT_NAME_CALLER, "")
+            )
+            val isShowCallID =
+                data?.getBoolean(CallkitConstants.EXTRA_CALLKIT_IS_SHOW_CALL_ID, false)
+            if (isShowCallID == true) {
+                notificationViews?.setTextViewText(
+                    R.id.tvNumber,
+                    data.getString(CallkitConstants.EXTRA_CALLKIT_HANDLE, "")
+                )
+            }
+            notificationViews?.setOnClickPendingIntent(
+                R.id.llCallback,
+                getCallbackPendingIntent(missedNotificationId, data)
+            )
+            val isShowCallback = data.getBoolean(
+                CallkitConstants.EXTRA_CALLKIT_MISSED_CALL_CALLBACK_SHOW,
+                true
+            )
+            notificationViews?.setViewVisibility(
+                R.id.llCallback,
+                if (isShowCallback) View.VISIBLE else View.GONE
+            )
+            val textCallback =
+                data.getString(CallkitConstants.EXTRA_CALLKIT_MISSED_CALL_CALLBACK_TEXT, "")
+            notificationViews?.setTextViewText(
+                R.id.tvCallback,
+                if (TextUtils.isEmpty(textCallback)) context.getString(R.string.text_call_back) else textCallback
+            )
+
+            val avatarUrl = data.getString(CallkitConstants.EXTRA_CALLKIT_AVATAR, "")
+            if (avatarUrl != null && avatarUrl.isNotEmpty()) {
+                val headers =
+                    data.getSerializable(CallkitConstants.EXTRA_CALLKIT_HEADERS) as HashMap<String, Any?>
+
+                getPicassoInstance(context, headers).load(avatarUrl)
+                    .transform(CircleTransform())
+                    .into(createAvatarTargetCustom(missedNotificationId))
+            }
+            notificationBuilder.setStyle(NotificationCompat.DecoratedCustomViewStyle())
+            notificationBuilder.setCustomContentView(notificationViews)
+            notificationBuilder.setCustomBigContentView(notificationViews)
+        } else {
+            notificationBuilder.setContentTitle(
+                data.getString(
+                    CallkitConstants.EXTRA_CALLKIT_NAME_CALLER,
+                    ""
+                )
+            )
+            notificationBuilder.setContentText(
+                data.getString(
+                    CallkitConstants.EXTRA_CALLKIT_HANDLE,
+                    ""
+                )
+            )
+            val avatarUrl = data.getString(CallkitConstants.EXTRA_CALLKIT_AVATAR, "")
+            if (avatarUrl != null && avatarUrl.isNotEmpty()) {
+                val headers =
+                    data.getSerializable(CallkitConstants.EXTRA_CALLKIT_HEADERS) as HashMap<String, Any?>
+
+                getPicassoInstance(context, headers).load(avatarUrl)
+                    .into(createAvatarTargetDefault(missedNotificationId))
+            }
+            val isShowCallback = data.getBoolean(
+                CallkitConstants.EXTRA_CALLKIT_MISSED_CALL_CALLBACK_SHOW,
+                true
+            )
+            if (isShowCallback) {
+                val textCallback =
+                    data.getString(CallkitConstants.EXTRA_CALLKIT_MISSED_CALL_CALLBACK_TEXT, "")
+                val callbackAction: NotificationCompat.Action = NotificationCompat.Action.Builder(
+                    R.drawable.ic_accept,
+                    if (TextUtils.isEmpty(textCallback)) context.getString(R.string.text_call_back) else textCallback,
+                    getCallbackPendingIntent(missedNotificationId, data)
+                ).build()
+                notificationBuilder.addAction(callbackAction)
+            }
+        }
+        notificationBuilder.priority = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
+            NotificationManager.IMPORTANCE_HIGH
+        } else {
+            Notification.PRIORITY_HIGH
+        }
+        notificationBuilder.setSound(missedCallSound)
+        notificationBuilder.setContentIntent(getAppPendingIntent(missedNotificationId, data))
+        val actionColor = data.getString(CallkitConstants.EXTRA_CALLKIT_ACTION_COLOR, "#4CAF50")
+        try {
+            notificationBuilder.color = Color.parseColor(actionColor)
+        } catch (_: Exception) {
+        }
+        val notification = notificationBuilder.build()
+        getNotificationManager().notify(missedNotificationId, notification)
+    }
+
+
+    fun clearIncomingNotification(data: Bundle, isAccepted: Boolean) {
+        context.sendBroadcast(CallkitIncomingActivity.getIntentEnded(context, isAccepted))
+        notificationId =
+            data.getString(CallkitConstants.EXTRA_CALLKIT_ID, "callkit_incoming").hashCode()
+        getNotificationManager().cancel(notificationId)
+    }
+
+    fun clearMissCallNotification(data: Bundle) {
+        val missedNotificationId = data.getInt(
+            CallkitConstants.EXTRA_CALLKIT_MISSED_CALL_ID,
+            data.getString(CallkitConstants.EXTRA_CALLKIT_ID, "callkit_incoming").hashCode() + 1
+        )
+        getNotificationManager().cancel(missedNotificationId)
+    }
+
+    fun incomingChannelEnabled(): Boolean = getNotificationManager().run {
+        val channel = getNotificationChannel(NOTIFICATION_CHANNEL_ID_INCOMING)
+
+        return areNotificationsEnabled() &&
+                (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O &&
+                        channel != null &&
+                        channel.importance > NotificationManagerCompat.IMPORTANCE_NONE) ||
+                Build.VERSION.SDK_INT < Build.VERSION_CODES.O
+    }
+
+    public fun createNotificationChanel(data: Bundle) {
+        val incomingCallChannelName = data.getString(
+            CallkitConstants.EXTRA_CALLKIT_INCOMING_CALL_NOTIFICATION_CHANNEL_NAME,
+            "Incoming Call"
+        )
+        val missedCallChannelName = data.getString(
+            CallkitConstants.EXTRA_CALLKIT_MISSED_CALL_NOTIFICATION_CHANNEL_NAME,
+            "Missed Call"
+        )
+        val ongoingCallChannelName = data.getString(
+            CallkitConstants.EXTRA_CALLKIT_ONGOING_CALL_NOTIFICATION_CHANNEL_NAME,
+            "Ongoing Call"
+        );
+
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+            getNotificationManager().apply {
+                var channelCall = getNotificationChannel(NOTIFICATION_CHANNEL_ID_INCOMING)
+                if (channelCall != null) {
+                    channelCall.setSound(null, null)
+                } else {
+                    channelCall = NotificationChannel(
+                        NOTIFICATION_CHANNEL_ID_INCOMING,
+                        incomingCallChannelName,
+                        NotificationManager.IMPORTANCE_HIGH
+                    ).apply {
+                        description = ""
+                        vibrationPattern =
+                            longArrayOf(0, 1000, 500, 1000, 500)
+                        lightColor = Color.RED
+                        enableLights(true)
+                        enableVibration(true)
+                        setSound(null, null)
+                    }
+                }
+                channelCall.lockscreenVisibility = Notification.VISIBILITY_PUBLIC
+
+                channelCall.importance = NotificationManager.IMPORTANCE_HIGH
+
+                createNotificationChannel(channelCall)
+
+                val channelMissedCall = NotificationChannel(
+                    NOTIFICATION_CHANNEL_ID_MISSED,
+                    missedCallChannelName,
+                    NotificationManager.IMPORTANCE_DEFAULT
+                ).apply {
+                    description = ""
+                    vibrationPattern = longArrayOf(0, 1000)
+                    lightColor = Color.RED
+                    enableLights(true)
+                    enableVibration(true)
+                }
+                channelMissedCall.importance = NotificationManager.IMPORTANCE_DEFAULT
+                createNotificationChannel(channelMissedCall)
+
+                val channelOngoingCall = NotificationChannel(
+                    NOTIFICATION_CHANNEL_ID_ONGOING,
+                    ongoingCallChannelName,
+                    NotificationManager.IMPORTANCE_LOW // disables notification popup for ongoing call
+                )
+                createNotificationChannel(channelOngoingCall)
+            }
+        }
+    }
+
+    private fun getAcceptPendingIntent(id: Int, data: Bundle): PendingIntent {
+        val intentTransparent = TransparentActivity.getIntent(
+            context,
+            CallkitConstants.ACTION_CALL_ACCEPT,
+            data
+        )
+        return PendingIntent.getActivity(context, id, intentTransparent, getFlagPendingIntent())
+    }
+
+    private fun getDeclinePendingIntent(id: Int, data: Bundle): PendingIntent {
+        val declineIntent = CallkitIncomingBroadcastReceiver.getIntentDecline(context, data)
+        return PendingIntent.getBroadcast(context, id, declineIntent, getFlagPendingIntent())
+    }
+
+    private fun getTimeOutPendingIntent(id: Int, data: Bundle): PendingIntent {
+        val timeOutIntent = CallkitIncomingBroadcastReceiver.getIntentTimeout(context, data)
+        return PendingIntent.getBroadcast(context, id, timeOutIntent, getFlagPendingIntent())
+    }
+
+    private fun getCallbackPendingIntent(id: Int, data: Bundle): PendingIntent {
+        val intentTransparent = TransparentActivity.getIntent(
+            context,
+            CallkitConstants.ACTION_CALL_CALLBACK,
+            data
+        )
+        return PendingIntent.getActivity(context, id, intentTransparent, getFlagPendingIntent())
+    }
+
+    private fun getActivityPendingIntent(id: Int, data: Bundle): PendingIntent {
+        val intent = CallkitIncomingActivity.getIntent(context, data)
+        return PendingIntent.getActivity(context, id, intent, getFlagPendingIntent())
+    }
+
+    private fun getAppPendingIntent(id: Int, data: Bundle): PendingIntent {
+        val intent: Intent? = AppUtils.getAppIntent(context, data = data)
+        return PendingIntent.getActivity(context, id, intent, getFlagPendingIntent())
+    }
+
+    private fun getHangUpIntent(id: Int, data: Bundle): PendingIntent {
+        val intent = CallkitIncomingActivity.getIntentEnded(context, true)
+        return PendingIntent.getActivity(context, id, intent, getFlagPendingIntent())
+    }
+
+    private fun getFlagPendingIntent(): Int {
+        return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+            PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
+        } else {
+            PendingIntent.FLAG_UPDATE_CURRENT
+        }
+    }
+
+
+    private fun getNotificationManager(): NotificationManagerCompat {
+        return NotificationManagerCompat.from(context)
+    }
+
+
+    private fun getPicassoInstance(context: Context, headers: HashMap<String, Any?>): Picasso {
+        val client = OkHttpClient.Builder()
+            .addNetworkInterceptor { chain ->
+                val newRequestBuilder: okhttp3.Request.Builder = chain.request().newBuilder()
+                for ((key, value) in headers) {
+                    newRequestBuilder.addHeader(key, value.toString())
+                }
+                chain.proceed(newRequestBuilder.build())
+            }
+            .build()
+        return Picasso.Builder(context)
+            .downloader(OkHttp3Downloader(client))
+            .build()
+    }
+
+
+    fun requestNotificationPermission(activity: Activity?, map: Map<String, Any>) {
+        this.dataNotificationPermission = map
+        if (Build.VERSION.SDK_INT > 32) {
+            activity?.let {
+                ActivityCompat.requestPermissions(
+                    it,
+                    arrayOf(Manifest.permission.POST_NOTIFICATIONS),
+                    PERMISSION_NOTIFICATION_REQUEST_CODE
+                )
+            }
+        }
+    }
+
+    fun requestFullIntentPermission(activity: Activity?) {
+        val canUseFullScreenIntent = getNotificationManager().canUseFullScreenIntent();
+        if (!canUseFullScreenIntent && Build.VERSION.SDK_INT > 33) {
+            val intent = Intent(Settings.ACTION_MANAGE_APP_USE_FULL_SCREEN_INTENT).apply {
+                data = Uri.fromParts("package", activity?.packageName, null)
+            }
+            activity?.startActivity(intent)
+        }
+    }
+
+    fun onRequestPermissionsResult(activity: Activity?, requestCode: Int, grantResults: IntArray) {
+        when (requestCode) {
+            PERMISSION_NOTIFICATION_REQUEST_CODE -> {
+                if (grantResults.isNotEmpty() &&
+                    grantResults[0] === PackageManager.PERMISSION_GRANTED
+                ) {
+                    // allow
+                } else {
+                    //deny
+                    activity?.let {
+                        if (ActivityCompat.shouldShowRequestPermissionRationale(
+                                it,
+                                Manifest.permission.POST_NOTIFICATIONS
+                            )
+                        ) {
+                            //showDialogPermissionRationale()
+                            if (this.dataNotificationPermission["rationaleMessagePermission"] != null) {
+                                showDialogMessage(
+                                    it,
+                                    this.dataNotificationPermission["rationaleMessagePermission"] as String
+                                ) { dialog, _ ->
+                                    dialog?.dismiss()
+                                    requestNotificationPermission(
+                                        activity,
+                                        this.dataNotificationPermission
+                                    )
+                                }
+                            } else {
+                                requestNotificationPermission(
+                                    activity,
+                                    this.dataNotificationPermission
+                                )
+                            }
+                        } else {
+                            //Open Setting
+                            if (this.dataNotificationPermission["postNotificationMessageRequired"] != null) {
+                                showDialogMessage(
+                                    it,
+                                    this.dataNotificationPermission["postNotificationMessageRequired"] as String
+                                ) { dialog, _ ->
+                                    dialog?.dismiss()
+                                    val intent = Intent(
+                                        Settings.ACTION_APPLICATION_DETAILS_SETTINGS,
+                                        Uri.fromParts("package", it.packageName, null)
+                                    )
+                                    intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+                                    it.startActivity(intent)
+                                }
+                            } else {
+                                showDialogMessage(
+                                    it,
+                                    it.resources.getString(R.string.text_post_notification_message_required)
+                                ) { dialog, _ ->
+                                    dialog?.dismiss()
+                                    val intent = Intent(
+                                        Settings.ACTION_APPLICATION_DETAILS_SETTINGS,
+                                        Uri.fromParts("package", it.packageName, null)
+                                    )
+                                    intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+                                    it.startActivity(intent)
+                                }
+                            }
+                        }
+                    }
+                }
+            }
+        }
+    }
+
+    private fun showDialogMessage(
+        activity: Activity?,
+        message: String,
+        okListener: DialogInterface.OnClickListener
+    ) {
+        activity?.let {
+            AlertDialog.Builder(it, R.style.DialogTheme)
+                .setMessage(message)
+                .setPositiveButton(android.R.string.ok, okListener)
+                .setNegativeButton(android.R.string.cancel, null)
+                .create()
+                .show()
+        }
+    }
+}
+
+

+ 170 - 0
android/src/main/kotlin/com/hiennv/flutter_callkit_incoming/CallkitSoundPlayerService.kt

@@ -0,0 +1,170 @@
+package com.hiennv.flutter_callkit_incoming
+
+import android.app.Service
+import android.content.Context
+import android.content.Intent
+import android.media.AudioAttributes
+import android.media.AudioManager
+import android.media.MediaPlayer
+import android.media.RingtoneManager
+import android.net.Uri
+import android.os.*
+import android.text.TextUtils
+
+class CallkitSoundPlayerService : Service() {
+
+    private var vibrator: Vibrator? = null
+    private var audioManager: AudioManager? = null
+
+    private var mediaPlayer: MediaPlayer? = null
+    private var data: Bundle? = null
+
+    override fun onBind(p0: Intent?): IBinder? {
+        return null
+    }
+
+    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
+        this.prepare()
+        this.playSound(intent)
+        this.playVibrator()
+        return START_STICKY;
+    }
+
+    override fun onDestroy() {
+        super.onDestroy()
+        mediaPlayer?.stop()
+        mediaPlayer?.release()
+        vibrator?.cancel()
+
+        mediaPlayer = null
+        vibrator = null
+    }
+
+    private fun prepare() {
+        mediaPlayer?.stop()
+        mediaPlayer?.release()
+        vibrator?.cancel()
+    }
+
+    private fun playVibrator() {
+        vibrator = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
+            val vibratorManager =
+                this.getSystemService(Context.VIBRATOR_MANAGER_SERVICE) as VibratorManager
+            vibratorManager.defaultVibrator
+        } else {
+            getSystemService(VIBRATOR_SERVICE) as Vibrator
+        }
+        audioManager = this.getSystemService(AUDIO_SERVICE) as AudioManager
+        when (audioManager?.ringerMode) {
+            AudioManager.RINGER_MODE_SILENT -> {
+            }
+
+            else -> {
+                if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
+                    vibrator?.vibrate(
+                        VibrationEffect.createWaveform(
+                            longArrayOf(0L, 1000L, 1000L),
+                            0
+                        )
+                    )
+                } else {
+                    vibrator?.vibrate(longArrayOf(0L, 1000L, 1000L), 0)
+                }
+            }
+        }
+    }
+
+    private fun playSound(intent: Intent?) {
+        this.data = intent?.extras
+        val sound = this.data?.getString(
+            CallkitConstants.EXTRA_CALLKIT_RINGTONE_PATH,
+            ""
+        )
+        var uri = sound?.let { getRingtoneUri(it) }
+        if (uri == null) {
+            // Failed to get ringtone url, can't play sound
+            return
+        }
+        try {
+            mediaPlayer(uri!!)
+        } catch (e: Exception) {
+            e.printStackTrace()
+        }
+    }
+
+    private fun mediaPlayer(uri: Uri) {
+        mediaPlayer = MediaPlayer()
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+            val attribution = AudioAttributes.Builder()
+                .setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
+                .setUsage(AudioAttributes.USAGE_NOTIFICATION_RINGTONE)
+                .setLegacyStreamType(AudioManager.STREAM_RING)
+                .build()
+            mediaPlayer?.setAudioAttributes(attribution)
+        } else {
+            mediaPlayer?.setAudioStreamType(AudioManager.STREAM_RING)
+        }
+        setDataSource(uri)
+        mediaPlayer?.prepare()
+        mediaPlayer?.isLooping = true
+        mediaPlayer?.start()
+    }
+
+    private fun setDataSource(uri: Uri) {
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
+            val assetFileDescriptor =
+                applicationContext.contentResolver.openAssetFileDescriptor(uri, "r")
+            if (assetFileDescriptor != null) {
+                mediaPlayer?.setDataSource(assetFileDescriptor)
+            }
+            return
+        }
+        mediaPlayer?.setDataSource(applicationContext, uri)
+    }
+
+    private fun getRingtoneUri(fileName: String): Uri? {
+        if (TextUtils.isEmpty(fileName)) {
+            return getDefaultRingtoneUri()
+        }
+        
+        // If system_ringtone_default is explicitly requested, bypass resource check
+        if (fileName.equals("system_ringtone_default", true)) {
+            return getDefaultRingtoneUri(useSystemDefault = true)
+        }
+
+        try {
+            val resId = resources.getIdentifier(fileName, "raw", packageName)
+            if (resId != 0) {
+                return Uri.parse("android.resource://${packageName}/$resId")
+            }
+
+            // For any other unresolved filename, return the default ringtone
+            return getDefaultRingtoneUri()
+        } catch (e: Exception) {
+            // If anything fails, try to return the system default ringtone
+            return getDefaultRingtoneUri()
+        }
+    }
+
+    private fun getDefaultRingtoneUri(useSystemDefault: Boolean = false): Uri? {
+        try {
+            if (!useSystemDefault) {
+                // First try to use ringtone_default resource if it exists
+                val resId = resources.getIdentifier("ringtone_default", "raw", packageName)
+                if (resId != 0) {
+                    return Uri.parse("android.resource://${packageName}/$resId")
+                }
+            }
+
+            // Fall back to system default ringtone
+            return RingtoneManager.getActualDefaultRingtoneUri(
+                this@CallkitSoundPlayerService,
+                RingtoneManager.TYPE_RINGTONE
+            )
+        } catch (e: Exception) {
+            // getActualDefaultRingtoneUri can throw an exception on some devices
+            // for custom ringtones
+            return null
+        }
+    }
+}

+ 368 - 0
android/src/main/kotlin/com/hiennv/flutter_callkit_incoming/FlutterCallkitIncomingPlugin.kt

@@ -0,0 +1,368 @@
+package com.hiennv.flutter_callkit_incoming
+
+import android.annotation.SuppressLint
+import android.app.Activity
+import android.content.Context
+import android.content.Intent
+import android.os.Build
+import android.os.Handler
+import android.os.Looper
+import android.util.Log
+import androidx.annotation.NonNull
+import com.hiennv.flutter_callkit_incoming.Utils.Companion.reapCollection
+import io.flutter.embedding.engine.plugins.FlutterPlugin
+import io.flutter.embedding.engine.plugins.activity.ActivityAware
+import io.flutter.embedding.engine.plugins.activity.ActivityPluginBinding
+import io.flutter.plugin.common.*
+import io.flutter.plugin.common.MethodChannel.MethodCallHandler
+import io.flutter.plugin.common.MethodChannel.Result
+import java.lang.ref.WeakReference
+
+
+/** FlutterCallkitIncomingPlugin */
+class FlutterCallkitIncomingPlugin : FlutterPlugin, MethodCallHandler, ActivityAware,
+    PluginRegistry.RequestPermissionsResultListener {
+    companion object {
+
+        const val EXTRA_CALLKIT_CALL_DATA = "EXTRA_CALLKIT_CALL_DATA"
+
+        @SuppressLint("StaticFieldLeak")
+        private lateinit var instance: FlutterCallkitIncomingPlugin
+
+        public fun getInstance(): FlutterCallkitIncomingPlugin {
+            return instance
+        }
+
+        public fun hasInstance(): Boolean {
+            return ::instance.isInitialized
+        }
+
+        private val methodChannels = mutableMapOf<BinaryMessenger, MethodChannel>()
+        private val eventChannels = mutableMapOf<BinaryMessenger, EventChannel>()
+        private val eventHandlers = mutableListOf<WeakReference<EventCallbackHandler>>()
+
+        fun sendEvent(event: String, body: Map<String, Any>) {
+            eventHandlers.reapCollection().forEach {
+                it.get()?.send(event, body)
+            }
+        }
+
+        public fun sendEventCustom(event: String, body: Map<String, Any>) {
+            eventHandlers.reapCollection().forEach {
+                it.get()?.send(event, body)
+            }
+        }
+
+
+        fun sharePluginWithRegister(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
+            initSharedInstance(
+                flutterPluginBinding.applicationContext,
+                flutterPluginBinding.binaryMessenger
+            )
+        }
+
+        fun initSharedInstance(context: Context, binaryMessenger: BinaryMessenger) {
+            if (!::instance.isInitialized) {
+                instance = FlutterCallkitIncomingPlugin()
+                instance.callkitNotificationManager = CallkitNotificationManager(context)
+                instance.context = context
+            }
+
+            val channel = MethodChannel(binaryMessenger, "flutter_callkit_incoming")
+            methodChannels[binaryMessenger] = channel
+            channel.setMethodCallHandler(instance)
+
+            val events = EventChannel(binaryMessenger, "flutter_callkit_incoming_events")
+            eventChannels[binaryMessenger] = events
+            val handler = EventCallbackHandler()
+            eventHandlers.add(WeakReference(handler))
+            events.setStreamHandler(handler)
+
+        }
+
+    }
+
+    /// The MethodChannel that will the communication between Flutter and native Android
+    ///
+    /// This local reference serves to register the plugin with the Flutter Engine and unregister it
+    /// when the Flutter Engine is detached from the Activity
+    private var activity: Activity? = null
+    private var context: Context? = null
+    private var callkitNotificationManager: CallkitNotificationManager? = null
+
+    override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) {
+        sharePluginWithRegister(flutterPluginBinding)
+    }
+
+    public fun showIncomingNotification(data: Data) {
+        data.from = "notification"
+        callkitNotificationManager?.showIncomingNotification(data.toBundle())
+        //send BroadcastReceiver
+        context?.sendBroadcast(
+            CallkitIncomingBroadcastReceiver.getIntentIncoming(
+                requireNotNull(context),
+                data.toBundle()
+            )
+        )
+    }
+
+    public fun showMissCallNotification(data: Data) {
+        callkitNotificationManager?.showIncomingNotification(data.toBundle())
+    }
+
+    public fun startCall(data: Data) {
+        context?.sendBroadcast(
+            CallkitIncomingBroadcastReceiver.getIntentStart(
+                requireNotNull(context),
+                data.toBundle()
+            )
+        )
+    }
+
+    public fun endCall(data: Data) {
+        context?.sendBroadcast(
+            CallkitIncomingBroadcastReceiver.getIntentEnded(
+                requireNotNull(context),
+                data.toBundle()
+            )
+        )
+    }
+
+    public fun endAllCalls() {
+        val calls = getDataActiveCalls(context)
+        calls.forEach {
+            context?.sendBroadcast(
+                CallkitIncomingBroadcastReceiver.getIntentEnded(
+                    requireNotNull(context),
+                    it.toBundle()
+                )
+            )
+        }
+        removeAllCalls(context)
+    }
+
+    public fun sendEventCustom(body: Map<String, Any>) {
+        eventHandlers.reapCollection().forEach {
+            it.get()?.send(CallkitConstants.ACTION_CALL_CUSTOM, body)
+        }
+    }
+
+    override fun onMethodCall(@NonNull call: MethodCall, @NonNull result: Result) {
+        try {
+            when (call.method) {
+                "showCallkitIncoming" -> {
+                    val data = Data(call.arguments() ?: HashMap())
+                    data.from = "notification"
+                    //send BroadcastReceiver
+                    context?.sendBroadcast(
+                        CallkitIncomingBroadcastReceiver.getIntentIncoming(
+                            requireNotNull(context),
+                            data.toBundle()
+                        )
+                    )
+
+                    result.success("OK")
+                }
+
+                "showCallkitIncomingSilently" -> {
+                    val data = Data(call.arguments() ?: HashMap())
+                    data.from = "notification"
+
+                    result.success("OK")
+                }
+
+                "showMissCallNotification" -> {
+                    val data = Data(call.arguments() ?: HashMap())
+                    data.from = "notification"
+                    callkitNotificationManager?.showMissCallNotification(data.toBundle())
+                    result.success("OK")
+                }
+
+                "startCall" -> {
+                    val data = Data(call.arguments() ?: HashMap())
+                    context?.sendBroadcast(
+                        CallkitIncomingBroadcastReceiver.getIntentStart(
+                            requireNotNull(context),
+                            data.toBundle()
+                        )
+                    )
+
+                    result.success("OK")
+                }
+
+                "muteCall" -> {
+                    val map = buildMap {
+                        val args = call.arguments
+                        if (args is Map<*, *>) {
+                            putAll(args as Map<String, Any>)
+                        }
+                    }
+                    sendEvent(CallkitConstants.ACTION_CALL_TOGGLE_MUTE, map)
+
+                    result.success("OK")
+                }
+
+                "holdCall" -> {
+                    val map = buildMap {
+                        val args = call.arguments
+                        if (args is Map<*, *>) {
+                            putAll(args as Map<String, Any>)
+                        }
+                    }
+                    sendEvent(CallkitConstants.ACTION_CALL_TOGGLE_HOLD, map)
+
+                    result.success("OK")
+                }
+
+                "isMuted" -> {
+                    result.success(false)
+                }
+
+                "endCall" -> {
+                    val data = Data(call.arguments() ?: HashMap())
+                    context?.sendBroadcast(
+                        CallkitIncomingBroadcastReceiver.getIntentEnded(
+                            requireNotNull(context),
+                            data.toBundle()
+                        )
+                    )
+
+                    result.success("OK")
+                }
+
+                "callConnected" -> {
+                    result.success("OK")
+                }
+
+                "endAllCalls" -> {
+                    val calls = getDataActiveCalls(context)
+                    calls.forEach {
+                        if (it.isAccepted) {
+                            context?.sendBroadcast(
+                                CallkitIncomingBroadcastReceiver.getIntentEnded(
+                                    requireNotNull(context),
+                                    it.toBundle()
+                                )
+                            )
+                        } else {
+                            context?.sendBroadcast(
+                                CallkitIncomingBroadcastReceiver.getIntentDecline(
+                                    requireNotNull(context),
+                                    it.toBundle()
+                                )
+                            )
+                        }
+                    }
+                    removeAllCalls(context)
+                    result.success("OK")
+                }
+
+                "activeCalls" -> {
+                    result.success(getDataActiveCallsForFlutter(context))
+                }
+
+                "getDevicePushTokenVoIP" -> {
+                    result.success("")
+                }
+
+                "silenceEvents" -> {
+                    val silence = call.arguments as? Boolean ?: false
+                    CallkitIncomingBroadcastReceiver.silenceEvents = silence
+                    result.success("")
+                }
+
+                "requestNotificationPermission" -> {
+                    val map = buildMap {
+                        val args = call.arguments
+                        if (args is Map<*, *>) {
+                            putAll(args as Map<String, Any>)
+                        }
+                    }
+                    callkitNotificationManager?.requestNotificationPermission(activity, map)
+                }
+
+                "requestFullIntentPermission" -> {
+                    callkitNotificationManager?.requestFullIntentPermission(activity)
+                }
+                // EDIT - clear the incoming notification/ring (after accept/decline/timeout)
+                "hideCallkitIncoming" -> {
+                    val data = Data(call.arguments() ?: HashMap())
+                    context?.stopService(Intent(context, CallkitSoundPlayerService::class.java))
+                    callkitNotificationManager?.clearIncomingNotification(data.toBundle(), false)
+                }
+
+                "endNativeSubsystemOnly" -> {
+
+                }
+
+                "setAudioRoute" -> {
+
+                }
+            }
+        } catch (error: Exception) {
+            result.error("error", error.message, "")
+        }
+    }
+
+    override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) {
+        methodChannels.remove(binding.binaryMessenger)?.setMethodCallHandler(null)
+        eventChannels.remove(binding.binaryMessenger)?.setStreamHandler(null)
+    }
+
+    override fun onAttachedToActivity(binding: ActivityPluginBinding) {
+        instance.context = binding.activity.applicationContext
+        instance.activity = binding.activity
+        binding.addRequestPermissionsResultListener(this)
+    }
+
+    override fun onDetachedFromActivityForConfigChanges() {
+    }
+
+    override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) {
+        instance.context = binding.activity.applicationContext
+        instance.activity = binding.activity
+        binding.addRequestPermissionsResultListener(this)
+    }
+
+    override fun onDetachedFromActivity() {
+
+    }
+
+    class EventCallbackHandler : EventChannel.StreamHandler {
+
+        private var eventSink: EventChannel.EventSink? = null
+
+        override fun onListen(arguments: Any?, sink: EventChannel.EventSink) {
+            eventSink = sink
+        }
+
+        fun send(event: String, body: Map<String, Any>) {
+            val data = mapOf(
+                "event" to event,
+                "body" to body
+            )
+            Handler(Looper.getMainLooper()).post {
+                eventSink?.success(data)
+            }
+        }
+
+        override fun onCancel(arguments: Any?) {
+            eventSink = null
+        }
+    }
+
+    override fun onRequestPermissionsResult(
+        requestCode: Int,
+        permissions: Array<out String>,
+        grantResults: IntArray
+    ): Boolean {
+        instance.callkitNotificationManager?.onRequestPermissionsResult(
+            instance.activity,
+            requestCode,
+            grantResults
+        )
+        return true
+    }
+
+
+}

+ 267 - 0
android/src/main/kotlin/com/hiennv/flutter_callkit_incoming/OngoingNotificationService.kt

@@ -0,0 +1,267 @@
+package com.hiennv.flutter_callkit_incoming
+
+import android.annotation.SuppressLint
+import android.app.Notification
+import android.app.NotificationChannel
+import android.app.NotificationManager
+import android.app.PendingIntent
+import android.app.Service
+import android.content.Context
+import android.content.Intent
+import android.content.pm.ServiceInfo
+import android.graphics.Bitmap
+import android.graphics.Color
+import android.graphics.drawable.Drawable
+import android.media.RingtoneManager
+import android.net.Uri
+import android.os.Build
+import android.os.Bundle
+import android.os.IBinder
+import android.text.TextUtils
+import android.view.View
+import android.widget.RemoteViews
+import androidx.core.app.NotificationCompat
+import androidx.core.app.NotificationManagerCompat
+import androidx.core.app.Person
+import com.hiennv.flutter_callkit_incoming.widgets.CircleTransform
+import com.squareup.picasso.OkHttp3Downloader
+import com.squareup.picasso.Picasso
+import com.squareup.picasso.Target
+import okhttp3.OkHttpClient
+
+class OngoingNotificationService : Service() {
+
+
+    private lateinit var notificationBuilder: NotificationCompat.Builder
+    private var notificationViews: RemoteViews? = null
+
+
+    override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
+        showOngoingCallNotification(intent?.extras!!)
+        return START_STICKY
+    }
+
+    @SuppressLint("MissingPermission")
+    private fun showOngoingCallNotification(data: Bundle) {
+
+        val onGoingNotificationId = data.getInt(
+            CallkitConstants.EXTRA_CALLKIT_MISSED_CALL_ID,
+            data.getString(CallkitConstants.EXTRA_CALLKIT_ID, "callkit_incoming").hashCode() + 999
+        )
+
+        notificationBuilder = NotificationCompat.Builder(
+            this,
+            CallkitNotificationManager.NOTIFICATION_CHANNEL_ID_ONGOING
+        )
+        notificationBuilder.setChannelId(CallkitNotificationManager.NOTIFICATION_CHANNEL_ID_ONGOING)
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
+            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
+                notificationBuilder.setCategory(Notification.CATEGORY_CALL)
+            }
+        }
+        val textCalling = data.getString(CallkitConstants.EXTRA_CALLKIT_CALLING_SUBTITLE, "")
+        notificationBuilder.setSubText(if (TextUtils.isEmpty(textCalling)) getString(R.string.text_calling) else textCalling)
+        notificationBuilder.setSmallIcon(R.drawable.ic_accept)
+        val isCustomNotification =
+            data.getBoolean(CallkitConstants.EXTRA_CALLKIT_IS_CUSTOM_NOTIFICATION, false)
+        if (isCustomNotification) {
+            notificationViews =
+                RemoteViews(packageName, R.layout.layout_custom_ongoing_notification)
+            notificationViews?.setTextViewText(
+                R.id.tvNameCaller,
+                data.getString(CallkitConstants.EXTRA_CALLKIT_NAME_CALLER, "")
+            )
+            val isShowCallID =
+                data?.getBoolean(CallkitConstants.EXTRA_CALLKIT_IS_SHOW_CALL_ID, false)
+            if (isShowCallID == true) {
+                notificationViews?.setTextViewText(
+                    R.id.tvNumber,
+                    String.format(
+                        " • %1s",
+                        data.getString(CallkitConstants.EXTRA_CALLKIT_HANDLE, "")
+                    )
+                )
+            }
+            notificationViews?.setOnClickPendingIntent(
+                R.id.llHangup,
+                getHangupPendingIntent(onGoingNotificationId, data)
+            )
+            val isShowHangup = data.getBoolean(
+                CallkitConstants.EXTRA_CALLKIT_CALLING_HANG_UP_SHOW,
+                true
+            )
+            notificationViews?.setViewVisibility(
+                R.id.llHangup,
+                if (isShowHangup) View.VISIBLE else View.GONE
+            )
+            val textHangup =
+                data.getString(CallkitConstants.EXTRA_CALLKIT_CALLING_HANG_UP_TEXT, "")
+            notificationViews?.setTextViewText(
+                R.id.tvHangUp,
+                if (TextUtils.isEmpty(textHangup)) getString(R.string.text_hang_up) else textHangup
+            )
+            val textTapOpen =
+                data.getString(CallkitConstants.EXTRA_CALLKIT_CALLING_TAP_OPEN_TEXT, "")
+            notificationViews?.setTextViewText(
+                R.id.tvTapOpen,
+                if (TextUtils.isEmpty(textTapOpen)) getString(R.string.text_tab_open) else textTapOpen
+            )
+
+            val avatarUrl = data.getString(CallkitConstants.EXTRA_CALLKIT_AVATAR, "")
+            if (avatarUrl != null && avatarUrl.isNotEmpty()) {
+                val headers =
+                    data.getSerializable(CallkitConstants.EXTRA_CALLKIT_HEADERS) as HashMap<String, Any?>
+
+                getPicassoInstance(this@OngoingNotificationService, headers).load(avatarUrl)
+                    .transform(CircleTransform())
+                    .into(createAvatarTargetCustom(onGoingNotificationId))
+            }
+            notificationBuilder.setStyle(NotificationCompat.DecoratedCustomViewStyle())
+            notificationBuilder.setCustomContentView(notificationViews)
+            notificationBuilder.setCustomBigContentView(notificationViews)
+        } else {
+            notificationBuilder.setContentTitle(
+                data.getString(
+                    CallkitConstants.EXTRA_CALLKIT_NAME_CALLER,
+                    ""
+                )
+            )
+            notificationBuilder.setContentText(
+                data.getString(
+                    CallkitConstants.EXTRA_CALLKIT_HANDLE,
+                    ""
+                )
+            )
+            val avatarUrl = data.getString(CallkitConstants.EXTRA_CALLKIT_AVATAR, "")
+            if (avatarUrl != null && avatarUrl.isNotEmpty()) {
+                val headers =
+                    data.getSerializable(CallkitConstants.EXTRA_CALLKIT_HEADERS) as HashMap<String, Any?>
+
+                getPicassoInstance(this@OngoingNotificationService, headers).load(avatarUrl)
+                    .into(createAvatarTargetDefault(onGoingNotificationId))
+            }
+            val isShowHangup = data.getBoolean(
+                CallkitConstants.EXTRA_CALLKIT_CALLING_HANG_UP_SHOW,
+                true
+            )
+            if (isShowHangup) {
+                val textHangup =
+                    data.getString(CallkitConstants.EXTRA_CALLKIT_CALLING_HANG_UP_TEXT, "")
+                val hangUpAction: NotificationCompat.Action = NotificationCompat.Action.Builder(
+                    R.drawable.transparent,
+                    if (TextUtils.isEmpty(textHangup)) this.getString(R.string.text_hang_up) else textHangup,
+                    getHangupPendingIntent(onGoingNotificationId, data)
+                ).build()
+                notificationBuilder.addAction(hangUpAction)
+            }
+        }
+        notificationBuilder.priority = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) {
+            NotificationManager.IMPORTANCE_LOW
+        } else {
+            Notification.PRIORITY_LOW
+        }
+        notificationBuilder.setSound(null)
+        notificationBuilder.setContentIntent(getAppPendingIntent(onGoingNotificationId, data))
+        val actionColor = data.getString(CallkitConstants.EXTRA_CALLKIT_ACTION_COLOR, "#4CAF50")
+        try {
+            notificationBuilder.color = Color.parseColor(actionColor)
+        } catch (_: Exception) {
+        }
+        notificationBuilder.setOngoing(true)
+        val notification = notificationBuilder.build()
+
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
+            startForeground(
+                onGoingNotificationId,
+                notification,
+                ServiceInfo.FOREGROUND_SERVICE_TYPE_PHONE_CALL
+            )
+        } else {
+            startForeground(onGoingNotificationId, notification)
+        }
+
+    }
+
+    private fun getHangupPendingIntent(notificationId: Int, data: Bundle): PendingIntent {
+        val endedIntent = CallkitIncomingBroadcastReceiver.getIntentEnded(this, data)
+        return PendingIntent.getBroadcast(this, notificationId, endedIntent, getFlagPendingIntent())
+    }
+
+
+    private fun getAppPendingIntent(notificationId: Int, data: Bundle): PendingIntent {
+        val intent: Intent? = AppUtils.getAppIntent(this@OngoingNotificationService, data = data)
+        return PendingIntent.getActivity(
+            this@OngoingNotificationService,
+            notificationId,
+            intent,
+            getFlagPendingIntent()
+        )
+    }
+
+    override fun onBind(p0: Intent?): IBinder? {
+        return null;
+    }
+
+    private fun getFlagPendingIntent(): Int {
+        return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.M) {
+            PendingIntent.FLAG_IMMUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
+        } else {
+            PendingIntent.FLAG_UPDATE_CURRENT
+        }
+    }
+
+    private fun getPicassoInstance(context: Context, headers: HashMap<String, Any?>): Picasso {
+        val client = OkHttpClient.Builder()
+            .addNetworkInterceptor { chain ->
+                val newRequestBuilder: okhttp3.Request.Builder = chain.request().newBuilder()
+                for ((key, value) in headers) {
+                    newRequestBuilder.addHeader(key, value.toString())
+                }
+                chain.proceed(newRequestBuilder.build())
+            }
+            .build()
+        return Picasso.Builder(context)
+            .downloader(OkHttp3Downloader(client))
+            .build()
+    }
+
+    @SuppressLint("MissingPermission")
+    private fun createAvatarTargetCustom(notificationId: Int): Target {
+        return object : Target {
+            override fun onBitmapLoaded(bitmap: Bitmap?, from: Picasso.LoadedFrom?) {
+                notificationViews?.setImageViewBitmap(R.id.ivAvatar, bitmap)
+                notificationViews?.setViewVisibility(R.id.ivAvatar, View.VISIBLE)
+                getNotificationManager().notify(notificationId, notificationBuilder.build())
+            }
+
+            override fun onBitmapFailed(e: Exception?, errorDrawable: Drawable?) {
+            }
+
+            override fun onPrepareLoad(placeHolderDrawable: Drawable?) {
+            }
+        }
+    }
+
+    @SuppressLint("MissingPermission")
+    private fun createAvatarTargetDefault(notificationId: Int): Target {
+        return object : Target {
+            override fun onBitmapLoaded(bitmap: Bitmap?, from: Picasso.LoadedFrom?) {
+                notificationBuilder.setLargeIcon(bitmap)
+                getNotificationManager().notify(notificationId, notificationBuilder.build())
+            }
+
+            override fun onBitmapFailed(e: Exception?, errorDrawable: Drawable?) {
+            }
+
+            override fun onPrepareLoad(placeHolderDrawable: Drawable?) {
+            }
+        }
+
+    }
+
+    private fun getNotificationManager(): NotificationManagerCompat {
+        return NotificationManagerCompat.from(this@OngoingNotificationService)
+    }
+
+}
+

+ 75 - 0
android/src/main/kotlin/com/hiennv/flutter_callkit_incoming/SharedPreferencesUtils.kt

@@ -0,0 +1,75 @@
+package com.hiennv.flutter_callkit_incoming
+
+import android.content.Context
+import android.content.SharedPreferences
+import android.util.Log
+import com.fasterxml.jackson.core.type.TypeReference
+
+
+private const val CALLKIT_PREFERENCES_FILE_NAME = "flutter_callkit_incoming"
+private var prefs: SharedPreferences? = null
+private var editor: SharedPreferences.Editor? = null
+
+private fun initInstance(context: Context) {
+    prefs = context.getSharedPreferences(CALLKIT_PREFERENCES_FILE_NAME, Context.MODE_PRIVATE)
+    editor = prefs?.edit()
+}
+
+
+fun addCall(context: Context?, data: Data, isAccepted: Boolean = false) {
+    val json = getString(context, "ACTIVE_CALLS", "[]")
+    val arrayData: ArrayList<Data> = Utils.getGsonInstance()
+        .readValue(json, object : TypeReference<ArrayList<Data>>() {})
+    val currentData = arrayData.find { it == data }
+    if(currentData != null) {
+        currentData.isAccepted = isAccepted
+    }else {
+        arrayData.add(data)
+    }
+    putString(context, "ACTIVE_CALLS", Utils.getGsonInstance().writeValueAsString(arrayData))
+}
+
+fun removeCall(context: Context?, data: Data) {
+    val json = getString(context, "ACTIVE_CALLS", "[]")
+    Log.d("JSON", json!!)
+    val arrayData: ArrayList<Data> = Utils.getGsonInstance()
+        .readValue(json, object : TypeReference<ArrayList<Data>>() {})
+    arrayData.remove(data)
+    putString(context, "ACTIVE_CALLS", Utils.getGsonInstance().writeValueAsString(arrayData))
+}
+
+fun removeAllCalls(context: Context?) {
+    putString(context, "ACTIVE_CALLS", "[]")
+    remove(context, "ACTIVE_CALLS")
+}
+
+fun getDataActiveCalls(context: Context?): ArrayList<Data> {
+    val json = getString(context, "ACTIVE_CALLS", "[]")
+    return Utils.getGsonInstance()
+        .readValue(json, object : TypeReference<ArrayList<Data>>() {})
+}
+
+fun getDataActiveCallsForFlutter(context: Context?): ArrayList<Map<String, Any?>> {
+    val json = getString(context, "ACTIVE_CALLS", "[]")
+    return Utils.getGsonInstance().readValue(json, object : TypeReference<ArrayList<Map<String, Any?>>>() {})
+}
+
+fun putString(context: Context?, key: String, value: String?) {
+    if (context == null) return
+    initInstance(context)
+    editor?.putString(key, value)
+    editor?.commit()
+}
+
+fun getString(context: Context?, key: String, defaultValue: String = ""): String? {
+    if (context == null) return null
+    initInstance(context)
+    return prefs?.getString(key, defaultValue)
+}
+
+fun remove(context: Context?, key: String) {
+    if (context == null) return
+    initInstance(context)
+    editor?.remove(key)
+    editor?.commit()
+}

+ 45 - 0
android/src/main/kotlin/com/hiennv/flutter_callkit_incoming/TransparentActivity.kt

@@ -0,0 +1,45 @@
+package com.hiennv.flutter_callkit_incoming
+
+import android.app.Activity
+import android.content.Context
+import android.content.Intent
+import android.os.Bundle
+
+class TransparentActivity : Activity() {
+
+    companion object {
+        fun getIntent(context: Context, action: String, data: Bundle?): Intent {
+            val intent = Intent(context, TransparentActivity::class.java)
+            intent.action = action
+            intent.putExtra("data", data)
+            intent.addFlags(Intent.FLAG_ACTIVITY_NO_ANIMATION)
+            intent.addFlags(Intent.FLAG_ACTIVITY_NO_HISTORY)
+            intent.addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT)
+            intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP)
+            intent.addFlags(Intent.FLAG_ACTIVITY_SINGLE_TOP)
+            return intent
+        }
+    }
+
+
+    override fun onStart() {
+        super.onStart()
+        setVisible(false)
+    }
+
+    override fun onCreate(savedInstanceState: Bundle?) {
+        super.onCreate(savedInstanceState)
+
+        val data = intent.getBundleExtra("data")
+
+        val broadcastIntent = CallkitIncomingBroadcastReceiver.getIntent(this, intent.action!!, data)
+        broadcastIntent.addFlags(Intent.FLAG_RECEIVER_FOREGROUND)
+        sendBroadcast(broadcastIntent)
+
+        val activityIntent = AppUtils.getAppIntent(this, intent.action, data)
+        startActivity(activityIntent)
+
+        finish()
+        overridePendingTransition(0, 0)
+    }
+}

+ 82 - 0
android/src/main/kotlin/com/hiennv/flutter_callkit_incoming/Utils.kt

@@ -0,0 +1,82 @@
+package com.hiennv.flutter_callkit_incoming
+
+import android.content.Context
+import android.content.Intent
+import android.content.res.Resources
+import com.fasterxml.jackson.databind.ObjectMapper
+import java.lang.ref.WeakReference
+
+
+class Utils {
+
+    companion object {
+
+        private var mapper: ObjectMapper? = null
+
+
+        fun getGsonInstance(): ObjectMapper {
+            if (mapper == null) {
+                mapper = ObjectMapper()
+            }
+            return mapper!!
+        }
+
+        @JvmStatic
+        fun dpToPx(dp: Float): Float {
+            return dp * Resources.getSystem().displayMetrics.density
+        }
+
+        @JvmStatic
+        fun pxToDp(px: Float): Float {
+            return px / Resources.getSystem().displayMetrics.density
+        }
+
+        @JvmStatic
+        fun getScreenWidth(): Int {
+            return Resources.getSystem().displayMetrics.widthPixels
+        }
+
+        @JvmStatic
+        fun getScreenHeight(): Int {
+            return Resources.getSystem().displayMetrics.heightPixels
+        }
+
+        fun getNavigationBarHeight(context: Context): Int {
+            val resources = context.resources
+            val id = resources.getIdentifier(
+                    "navigation_bar_height", "dimen", "android"
+            )
+            return if (id > 0) {
+                resources.getDimensionPixelSize(id)
+            } else 0
+        }
+
+        fun getStatusBarHeight(context: Context): Int {
+            val resources = context.resources
+            val id: Int =
+                    resources.getIdentifier("status_bar_height", "dimen", "android")
+            return if (id > 0) {
+                resources.getDimensionPixelSize(id)
+            } else 0
+        }
+
+        fun backToForeground(context: Context) {
+            val packageName = context.packageName
+            val intent = context.packageManager.getLaunchIntentForPackage(packageName)?.cloneFilter()
+            intent?.addFlags(Intent.FLAG_ACTIVITY_REORDER_TO_FRONT)
+            intent?.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
+            context.startActivity(intent)
+        }
+
+        fun <T, C : MutableCollection<WeakReference<T>>> C.reapCollection(): C {
+            this.removeAll {
+                it.get() == null
+            }
+            return this
+        }
+
+        fun isTablet(context: Context): Boolean {
+            return context.resources.getBoolean(R.bool.isTablet)
+        }
+    }
+}

+ 36 - 0
android/src/main/kotlin/com/hiennv/flutter_callkit_incoming/widgets/CircleTransform.kt

@@ -0,0 +1,36 @@
+package com.hiennv.flutter_callkit_incoming.widgets
+
+import android.graphics.*
+import com.squareup.picasso.Transformation
+import kotlin.math.min
+
+
+class CircleTransform : Transformation {
+    override fun transform(source: Bitmap): Bitmap {
+        val size = min(source.width, source.height)
+        val x = (source.width - size) / 2
+        val y = (source.height - size) / 2
+        val squaredBitmap = Bitmap.createBitmap(source, x, y, size, size)
+        if (squaredBitmap != source) {
+            source.recycle()
+        }
+        val config = source.config ?: Bitmap.Config.ARGB_8888
+        val bitmap = Bitmap.createBitmap(size, size, config)
+        val canvas = Canvas(bitmap)
+        val paint = Paint()
+        val shader = BitmapShader(
+            squaredBitmap,
+            Shader.TileMode.CLAMP, Shader.TileMode.CLAMP
+        )
+        paint.shader = shader
+        paint.isAntiAlias = true
+        val r = size / 2f
+        canvas.drawCircle(r, r, r, paint)
+        squaredBitmap.recycle()
+        return bitmap
+    }
+
+    override fun key(): String {
+        return "circle"
+    }
+}

+ 146 - 0
android/src/main/kotlin/com/hiennv/flutter_callkit_incoming/widgets/RippleRelativeLayout.kt

@@ -0,0 +1,146 @@
+package com.hiennv.flutter_callkit_incoming.widgets
+
+import android.animation.Animator
+import android.animation.AnimatorSet
+import android.animation.ObjectAnimator
+import android.annotation.SuppressLint
+import android.content.Context
+import android.content.res.TypedArray
+import android.graphics.Canvas
+import android.graphics.Color
+import android.graphics.Paint
+import android.util.AttributeSet
+import android.view.View
+import android.view.animation.AccelerateDecelerateInterpolator
+import android.widget.RelativeLayout
+import com.hiennv.flutter_callkit_incoming.R
+import com.hiennv.flutter_callkit_incoming.Utils
+import kotlin.math.min
+
+
+class RippleRelativeLayout : RelativeLayout {
+    private var rippleColor = 0
+    private var rippleRadius = 0f
+    private var rippleDurationTime = 0
+    private var rippleAmount = 0
+    private var rippleDelay = 0
+    private var rippleScale = 0f
+    private var paint: Paint = Paint()
+    var isRippleAnimationRunning = false
+        private set
+    private var animatorSet: AnimatorSet? = null
+    private var animatorList: ArrayList<Animator>? = null
+    private var rippleParams: LayoutParams? = null
+    private val rippleViewList = ArrayList<RippleView>()
+
+    constructor(context: Context?) : super(context) {}
+    constructor(context: Context, attrs: AttributeSet?) : super(context, attrs) {
+        init(context, attrs)
+    }
+
+    constructor(context: Context, attrs: AttributeSet?, defStyleAttr: Int) : super(
+        context,
+        attrs,
+        defStyleAttr
+    ) {
+        init(context, attrs)
+    }
+
+    @SuppressLint("CustomViewStyleable")
+    private fun init(context: Context, attrs: AttributeSet?) {
+        if (isInEditMode) return
+
+        val typedArray: TypedArray =
+            context.obtainStyledAttributes(attrs, R.styleable.ripple_relativeLayout)
+        rippleColor = typedArray.getColor(
+            R.styleable.ripple_relativeLayout_ripple_color,
+            resources.getColor(R.color.ripple_main_color)
+        )
+        rippleRadius = typedArray.getDimension(
+            R.styleable.ripple_relativeLayout_ripple_radius,
+            Utils.dpToPx(30f)
+        )
+        rippleDurationTime = typedArray.getInt(
+            R.styleable.ripple_relativeLayout_ripple_duration,
+            DEFAULT_DURATION_TIME
+        )
+        rippleAmount =
+            typedArray.getInt(R.styleable.ripple_relativeLayout_ripple_amount, DEFAULT_RIPPLE_COUNT)
+        rippleScale =
+            typedArray.getFloat(R.styleable.ripple_relativeLayout_ripple_scale, DEFAULT_SCALE)
+        typedArray.recycle()
+
+        rippleDelay = rippleDurationTime / rippleAmount
+        paint = Paint()
+        paint.isAntiAlias = true
+        paint.style = Paint.Style.FILL
+        paint.color = rippleColor
+        rippleParams = LayoutParams(
+            (2 * (rippleRadius)).toInt(),
+            (2 * (rippleRadius)).toInt()
+        )
+        rippleParams!!.addRule(CENTER_IN_PARENT, TRUE)
+        animatorSet = AnimatorSet()
+        animatorSet!!.interpolator = AccelerateDecelerateInterpolator()
+        animatorList = ArrayList()
+        for (i in 0 until rippleAmount) {
+            val rippleView: RippleView = RippleView(getContext())
+            addView(rippleView, rippleParams)
+            rippleViewList.add(rippleView)
+            val scaleXAnimator = ObjectAnimator.ofFloat(rippleView, "ScaleX", 1.0f, rippleScale)
+            scaleXAnimator.repeatCount = ObjectAnimator.INFINITE
+            scaleXAnimator.repeatMode = ObjectAnimator.RESTART
+            scaleXAnimator.startDelay = (i * rippleDelay).toLong()
+            scaleXAnimator.duration = rippleDurationTime.toLong()
+            animatorList!!.add(scaleXAnimator)
+            val scaleYAnimator = ObjectAnimator.ofFloat(rippleView, "ScaleY", 1.0f, rippleScale)
+            scaleYAnimator.repeatCount = ObjectAnimator.INFINITE
+            scaleYAnimator.repeatMode = ObjectAnimator.RESTART
+            scaleYAnimator.startDelay = (i * rippleDelay).toLong()
+            scaleYAnimator.duration = rippleDurationTime.toLong()
+            animatorList!!.add(scaleYAnimator)
+            val alphaAnimator = ObjectAnimator.ofFloat(rippleView, "Alpha", 1.0f, 0f)
+            alphaAnimator.repeatCount = ObjectAnimator.INFINITE
+            alphaAnimator.repeatMode = ObjectAnimator.RESTART
+            alphaAnimator.startDelay = (i * rippleDelay).toLong()
+            alphaAnimator.duration = rippleDurationTime.toLong()
+            animatorList!!.add(alphaAnimator)
+        }
+        animatorSet!!.playTogether(animatorList)
+        startRippleAnimation()
+    }
+
+    private inner class RippleView(context: Context?) : View(context) {
+        override fun onDraw(canvas: Canvas) {
+            val radius = min(width, height) / 2f
+            canvas.drawCircle(radius, radius, radius, paint!!)
+        }
+
+        init {
+            visibility = INVISIBLE
+        }
+    }
+
+    fun startRippleAnimation() {
+        if (!isRippleAnimationRunning) {
+            for (rippleView in rippleViewList) {
+                rippleView.visibility = VISIBLE
+            }
+            animatorSet!!.start()
+            isRippleAnimationRunning = true
+        }
+    }
+
+    fun stopRippleAnimation() {
+        if (isRippleAnimationRunning) {
+            animatorSet!!.end()
+            isRippleAnimationRunning = false
+        }
+    }
+
+    companion object {
+        private const val DEFAULT_RIPPLE_COUNT = 5
+        private const val DEFAULT_DURATION_TIME = 6000
+        private const val DEFAULT_SCALE = 6.0f
+    }
+}

+ 9 - 0
android/src/main/res/anim/shake_anim.xml

@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<rotate xmlns:android="http://schemas.android.com/apk/res/android"
+    android:duration="200"
+    android:fromDegrees="-15"
+    android:pivotX="50%"
+    android:pivotY="50%"
+    android:repeatCount="infinite"
+    android:repeatMode="reverse"
+    android:toDegrees="15" />

BIN
android/src/main/res/drawable-hdpi/ic_accept.png


BIN
android/src/main/res/drawable-hdpi/ic_call_missed.png


BIN
android/src/main/res/drawable-hdpi/ic_decline.png


BIN
android/src/main/res/drawable-hdpi/ic_video.png


BIN
android/src/main/res/drawable-hdpi/ic_video_missed.png


BIN
android/src/main/res/drawable-mdpi/ic_accept.png


BIN
android/src/main/res/drawable-mdpi/ic_call_missed.png


BIN
android/src/main/res/drawable-mdpi/ic_decline.png


BIN
android/src/main/res/drawable-mdpi/ic_video.png


BIN
android/src/main/res/drawable-mdpi/ic_video_missed.png


+ 9 - 0
android/src/main/res/drawable-v21/bg_button_accept.xml

@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<ripple xmlns:android="http://schemas.android.com/apk/res/android"
+    android:color="@color/accept_ripple">
+    <item>
+        <shape android:shape="oval">
+            <solid android:color="@color/accept"/>
+        </shape>
+    </item>
+</ripple>

+ 9 - 0
android/src/main/res/drawable-v21/bg_button_decline.xml

@@ -0,0 +1,9 @@
+<?xml version="1.0" encoding="utf-8"?>
+<ripple xmlns:android="http://schemas.android.com/apk/res/android"
+    android:color="@color/decline_ripple">
+    <item>
+        <shape android:shape="oval">
+            <solid android:color="@color/decline"/>
+        </shape>
+    </item>
+</ripple>

+ 10 - 0
android/src/main/res/drawable-v21/rounded_button_accept.xml

@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<ripple xmlns:android="http://schemas.android.com/apk/res/android"
+    android:color="@color/accept_ripple">
+    <item>
+        <shape android:shape="rectangle">
+            <solid android:color="@color/accept" />
+            <corners android:radius="@dimen/size_button_x2" />
+        </shape>
+    </item>
+</ripple>

+ 10 - 0
android/src/main/res/drawable-v21/rounded_button_decline.xml

@@ -0,0 +1,10 @@
+<?xml version="1.0" encoding="utf-8"?>
+<ripple xmlns:android="http://schemas.android.com/apk/res/android"
+    android:color="@color/decline_ripple">
+    <item>
+        <shape android:shape="rectangle">
+            <solid android:color="@color/decline" />
+            <corners android:radius="@dimen/size_button_x2" />
+        </shape>
+    </item>
+</ripple>

BIN
android/src/main/res/drawable-xhdpi/ic_accept.png


BIN
android/src/main/res/drawable-xhdpi/ic_call_missed.png


BIN
android/src/main/res/drawable-xhdpi/ic_decline.png


BIN
android/src/main/res/drawable-xhdpi/ic_video.png


BIN
android/src/main/res/drawable-xhdpi/ic_video_missed.png


BIN
android/src/main/res/drawable-xxhdpi/ic_accept.png


BIN
android/src/main/res/drawable-xxhdpi/ic_call_missed.png


BIN
android/src/main/res/drawable-xxhdpi/ic_decline.png


BIN
android/src/main/res/drawable-xxhdpi/ic_video.png


BIN
android/src/main/res/drawable-xxhdpi/ic_video_missed.png


BIN
android/src/main/res/drawable-xxxhdpi/ic_accept.png


BIN
android/src/main/res/drawable-xxxhdpi/ic_call_missed.png


BIN
android/src/main/res/drawable-xxxhdpi/ic_decline.png


BIN
android/src/main/res/drawable-xxxhdpi/ic_default_avatar.png


BIN
android/src/main/res/drawable-xxxhdpi/ic_logo.png


BIN
android/src/main/res/drawable-xxxhdpi/ic_video.png


BIN
android/src/main/res/drawable-xxxhdpi/ic_video_missed.png


+ 5 - 0
android/src/main/res/drawable/bg_button_accept.xml

@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+    android:shape="oval">
+    <solid android:color="@color/accept" />
+</shape>

+ 5 - 0
android/src/main/res/drawable/bg_button_decline.xml

@@ -0,0 +1,5 @@
+<?xml version="1.0" encoding="utf-8"?>
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+    android:shape="oval">
+    <solid android:color="@color/decline" />
+</shape>

+ 6 - 0
android/src/main/res/drawable/rounded_button_accept.xml

@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+    android:shape="rectangle">
+    <solid android:color="@color/accept" />
+    <corners android:radius="@dimen/size_button_x2" />
+</shape>

+ 6 - 0
android/src/main/res/drawable/rounded_button_decline.xml

@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+    android:shape="rectangle">
+    <solid android:color="@color/decline" />
+    <corners android:radius="@dimen/size_button_x2" />
+</shape>

BIN
android/src/main/res/drawable/transparent.png


+ 201 - 0
android/src/main/res/layout-w600dp-land/activity_callkit_incoming.xml

@@ -0,0 +1,201 @@
+<?xml version="1.0" encoding="utf-8"?>
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:orientation="vertical">
+
+    <ImageView
+        android:id="@+id/ivBackground"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:background="#0955fa"
+        android:scaleType="centerCrop"
+        tools:ignore="ContentDescription" />
+
+
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:fitsSystemWindows="false"
+        android:orientation="vertical">
+
+
+        <com.hiennv.flutter_callkit_incoming.widgets.RippleRelativeLayout
+            android:id="@+id/llBackgroundAnimation"
+            android:layout_width="match_parent"
+            android:layout_height="0dp"
+            android:layout_marginTop="@dimen/base_margin_revert_x5"
+            android:layout_weight="1"
+            android:fitsSystemWindows="false">
+
+            <ImageView
+                android:id="@+id/ivLogo"
+                android:layout_width="match_parent"
+                android:layout_height="@dimen/base_margin_x2_5"
+                android:layout_above="@id/ivAvatar"
+                android:layout_centerHorizontal="true"
+                android:adjustViewBounds="true"
+                android:visibility="invisible"
+                android:scaleType="fitCenter"
+                android:layout_marginBottom="@dimen/base_margin_x5"
+                android:src="@drawable/ic_logo"
+                tools:ignore="ContentDescription" />
+
+
+            <de.hdodenhof.circleimageview.CircleImageView
+                android:id="@+id/ivAvatar"
+                android:layout_width="@dimen/size_avatar"
+                android:layout_height="@dimen/size_avatar"
+                android:visibility="invisible"
+                android:layout_centerInParent="true"
+                android:src="@drawable/ic_default_avatar"
+                app:civ_border_color="#80ffffff"
+                app:civ_border_width="1dp" />
+
+
+            <LinearLayout
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_below="@id/ivAvatar"
+                android:layout_centerHorizontal="true"
+                android:layout_marginTop="@dimen/base_margin_x2"
+                android:gravity="center"
+                android:orientation="vertical">
+
+                <TextView
+                    android:id="@+id/tvNameCaller"
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:autoSizeMaxTextSize="@dimen/size_text_name"
+                    android:autoSizeMinTextSize="12sp"
+                    android:autoSizeStepGranularity="2sp"
+                    android:autoSizeTextType="uniform"
+                    android:ellipsize="end"
+                    android:maxLines="1"
+                    android:textColor="@android:color/white"
+                    android:textSize="@dimen/size_text_name"
+                    app:autoSizeMaxTextSize="@dimen/size_text_name"
+                    app:autoSizeMinTextSize="12sp"
+                    app:autoSizeStepGranularity="2sp"
+                    app:autoSizeTextType="uniform"
+                    tools:ignore="MissingPrefix"
+                    tools:targetApi="o"
+                    tools:text="Caller Name" />
+
+                <TextView
+                    android:id="@+id/tvNumber"
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:layout_marginTop="4dp"
+                    android:ellipsize="end"
+                    android:maxLines="1"
+                    android:textColor="@color/action_text"
+                    android:textSize="@dimen/size_text_action"
+                    tools:text="Some info" />
+
+            </LinearLayout>
+
+        </com.hiennv.flutter_callkit_incoming.widgets.RippleRelativeLayout>
+
+
+        <LinearLayout
+            android:id="@+id/llAction"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:layout_gravity="bottom"
+            android:layout_marginBottom="0dp"
+            android:fitsSystemWindows="true"
+            android:gravity="center"
+            android:orientation="horizontal">
+
+            <LinearLayout
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:orientation="vertical"
+                tools:ignore="UseCompoundDrawables">
+
+                <com.hiennv.flutter_callkit_incoming.widgets.RippleRelativeLayout
+                    android:layout_width="@dimen/size_button_x2_5"
+                    android:layout_height="@dimen/size_button_x2_5"
+                    app:ripple_amount="4"
+                    app:ripple_radius="@dimen/base_margin_x1_5"
+                    app:ripple_scale="4.5">
+
+                    <ImageView
+                        android:id="@+id/ivDeclineCall"
+                        android:layout_width="@dimen/size_button"
+                        android:layout_height="@dimen/size_button"
+                        android:layout_centerInParent="true"
+                        android:background="@drawable/bg_button_decline"
+                        android:padding="@dimen/base_margin_x1_5"
+                        android:src="@drawable/ic_decline"
+                        tools:ignore="ContentDescription" />
+
+                    <TextView
+                        android:id="@+id/tvDecline"
+                        android:layout_width="match_parent"
+                        android:layout_height="match_parent"
+                        android:layout_below="@id/ivDeclineCall"
+                        android:layout_marginBottom="@dimen/base_margin"
+                        android:gravity="center"
+                        android:text="@string/text_decline"
+                        android:textColor="@color/action_text"
+                        android:textSize="@dimen/size_text_action" />
+
+                </com.hiennv.flutter_callkit_incoming.widgets.RippleRelativeLayout>
+
+            </LinearLayout>
+
+            <Space
+                android:layout_width="@dimen/base_margin_x5"
+                android:layout_height="0dp" />
+
+            <LinearLayout
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_gravity="end"
+                android:orientation="vertical"
+                tools:ignore="UseCompoundDrawables">
+
+
+                <com.hiennv.flutter_callkit_incoming.widgets.RippleRelativeLayout
+                    android:layout_width="@dimen/size_button_x2_5"
+                    android:layout_height="@dimen/size_button_x2_5"
+                    app:ripple_amount="4"
+                    app:ripple_radius="@dimen/base_margin_x1_5"
+                    app:ripple_scale="4.5">
+
+
+                    <ImageView
+                        android:id="@+id/ivAcceptCall"
+                        android:layout_width="@dimen/size_button"
+                        android:layout_height="@dimen/size_button"
+                        android:layout_centerInParent="true"
+                        android:background="@drawable/bg_button_accept"
+                        android:padding="@dimen/base_margin_x1_5"
+                        android:src="@drawable/ic_accept"
+                        tools:ignore="ContentDescription" />
+
+                    <TextView
+                        android:id="@+id/tvAccept"
+                        android:layout_width="match_parent"
+                        android:layout_height="match_parent"
+                        android:layout_below="@id/ivAcceptCall"
+                        android:layout_marginBottom="@dimen/base_margin"
+                        android:gravity="center"
+                        android:text="@string/text_accept"
+                        android:textColor="@color/action_text"
+                        android:textSize="@dimen/size_text_action" />
+
+
+                </com.hiennv.flutter_callkit_incoming.widgets.RippleRelativeLayout>
+
+            </LinearLayout>
+
+        </LinearLayout>
+
+    </LinearLayout>
+
+</RelativeLayout>

+ 206 - 0
android/src/main/res/layout/activity_callkit_incoming.xml

@@ -0,0 +1,206 @@
+<?xml version="1.0" encoding="utf-8"?>
+<RelativeLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="match_parent"
+    android:layout_height="match_parent"
+    android:orientation="vertical">
+
+    <ImageView
+        android:id="@+id/ivBackground"
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:background="#0955fa"
+        android:scaleType="centerCrop"
+        tools:ignore="ContentDescription" />
+
+
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="match_parent"
+        android:fitsSystemWindows="false"
+        android:orientation="vertical">
+
+
+        <com.hiennv.flutter_callkit_incoming.widgets.RippleRelativeLayout
+            android:id="@+id/llBackgroundAnimation"
+            android:layout_width="match_parent"
+            android:layout_height="0dp"
+            android:layout_marginTop="@dimen/base_margin_revert_x5"
+            android:layout_weight="1"
+            android:fitsSystemWindows="false">
+
+            <ImageView
+                android:id="@+id/ivLogo"
+                android:layout_width="match_parent"
+                android:layout_height="@dimen/base_margin_x2_5"
+                android:layout_above="@id/ivAvatar"
+                android:layout_centerHorizontal="true"
+                android:adjustViewBounds="true"
+                android:visibility="invisible"
+                android:scaleType="fitCenter"
+                android:layout_marginBottom="@dimen/base_margin_x5"
+                android:src="@drawable/ic_logo"
+                tools:ignore="ContentDescription" />
+
+
+            <de.hdodenhof.circleimageview.CircleImageView
+                android:id="@+id/ivAvatar"
+                android:layout_width="@dimen/size_avatar"
+                android:layout_height="@dimen/size_avatar"
+                android:visibility="invisible"
+                android:layout_centerInParent="true"
+                android:src="@drawable/ic_default_avatar"
+                app:civ_border_color="#80ffffff"
+                app:civ_border_width="1dp" />
+
+
+            <LinearLayout
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_below="@id/ivAvatar"
+                android:layout_centerHorizontal="true"
+                android:layout_marginTop="@dimen/base_margin_x2"
+                android:gravity="center"
+                android:orientation="vertical">
+
+                <TextView
+                    android:id="@+id/tvNameCaller"
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:autoSizeMaxTextSize="@dimen/size_text_name"
+                    android:autoSizeMinTextSize="12sp"
+                    android:autoSizeStepGranularity="2sp"
+                    android:autoSizeTextType="uniform"
+                    android:ellipsize="end"
+                    android:maxLines="1"
+                    android:textColor="@android:color/white"
+                    android:textSize="@dimen/size_text_name"
+                    app:autoSizeMaxTextSize="@dimen/size_text_name"
+                    app:autoSizeMinTextSize="12sp"
+                    app:autoSizeStepGranularity="2sp"
+                    app:autoSizeTextType="uniform"
+                    tools:ignore="MissingPrefix"
+                    tools:targetApi="o"
+                    tools:text="Caller Name" />
+
+                <TextView
+                    android:id="@+id/tvNumber"
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:layout_marginTop="4dp"
+                    android:ellipsize="end"
+                    android:maxLines="1"
+                    android:textColor="@color/action_text"
+                    android:textSize="@dimen/size_text_action"
+                    tools:text="Some info" />
+
+            </LinearLayout>
+
+        </com.hiennv.flutter_callkit_incoming.widgets.RippleRelativeLayout>
+
+
+        <Space
+            android:layout_width="match_parent"
+            android:layout_height="0dp"
+            android:layout_weight="1" />
+
+        <LinearLayout
+            android:id="@+id/llAction"
+            android:layout_width="match_parent"
+            android:layout_height="wrap_content"
+            android:layout_gravity="bottom"
+            android:layout_marginBottom="0dp"
+            android:fitsSystemWindows="true"
+            android:orientation="horizontal">
+
+            <LinearLayout
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:orientation="vertical"
+                tools:ignore="UseCompoundDrawables">
+
+                <com.hiennv.flutter_callkit_incoming.widgets.RippleRelativeLayout
+                    android:layout_width="@dimen/size_button_x2_5"
+                    android:layout_height="@dimen/size_button_x2_5"
+                    app:ripple_amount="4"
+                    app:ripple_radius="@dimen/base_margin_x1_5"
+                    app:ripple_scale="4.5">
+
+                    <ImageView
+                        android:id="@+id/ivDeclineCall"
+                        android:layout_width="@dimen/size_button"
+                        android:layout_height="@dimen/size_button"
+                        android:layout_centerInParent="true"
+                        android:background="@drawable/bg_button_decline"
+                        android:padding="@dimen/base_margin_x1_5"
+                        android:src="@drawable/ic_decline"
+                        tools:ignore="ContentDescription" />
+
+                    <TextView
+                        android:id="@+id/tvDecline"
+                        android:layout_width="match_parent"
+                        android:layout_height="match_parent"
+                        android:layout_below="@id/ivDeclineCall"
+                        android:layout_marginBottom="@dimen/base_margin"
+                        android:gravity="center"
+                        android:text="@string/text_decline"
+                        android:textColor="@color/action_text"
+                        android:textSize="@dimen/size_text_action" />
+
+                </com.hiennv.flutter_callkit_incoming.widgets.RippleRelativeLayout>
+
+            </LinearLayout>
+
+            <Space
+                android:layout_width="0dp"
+                android:layout_height="0dp"
+                android:layout_weight="1" />
+
+            <LinearLayout
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_gravity="end"
+                android:orientation="vertical"
+                tools:ignore="UseCompoundDrawables">
+
+
+                <com.hiennv.flutter_callkit_incoming.widgets.RippleRelativeLayout
+                    android:layout_width="@dimen/size_button_x2_5"
+                    android:layout_height="@dimen/size_button_x2_5"
+                    app:ripple_amount="4"
+                    app:ripple_radius="@dimen/base_margin_x1_5"
+                    app:ripple_scale="4.5">
+
+
+                    <ImageView
+                        android:id="@+id/ivAcceptCall"
+                        android:layout_width="@dimen/size_button"
+                        android:layout_height="@dimen/size_button"
+                        android:layout_centerInParent="true"
+                        android:background="@drawable/bg_button_accept"
+                        android:padding="@dimen/base_margin_x1_5"
+                        android:src="@drawable/ic_accept"
+                        tools:ignore="ContentDescription" />
+
+                    <TextView
+                        android:id="@+id/tvAccept"
+                        android:layout_width="match_parent"
+                        android:layout_height="match_parent"
+                        android:layout_below="@id/ivAcceptCall"
+                        android:layout_marginBottom="@dimen/base_margin"
+                        android:gravity="center"
+                        android:text="@string/text_accept"
+                        android:textColor="@color/action_text"
+                        android:textSize="@dimen/size_text_action" />
+
+
+                </com.hiennv.flutter_callkit_incoming.widgets.RippleRelativeLayout>
+
+            </LinearLayout>
+
+        </LinearLayout>
+
+    </LinearLayout>
+
+</RelativeLayout>

+ 90 - 0
android/src/main/res/layout/layout_custom_miss_notification.xml

@@ -0,0 +1,90 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:orientation="vertical">
+
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content">
+
+        <LinearLayout
+            android:layout_width="0dp"
+            android:layout_weight="1"
+            android:layout_height="wrap_content"
+            android:orientation="vertical">
+
+            <TextView
+                android:id="@+id/tvNameCaller"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:textAppearance="@style/TextAppearance.Compat.Notification.Title"
+                android:textSize="@dimen/size_text_action" />
+
+
+            <TextView
+                android:id="@+id/tvNumber"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:textAppearance="@style/TextAppearance.Compat.Notification.Line2"
+                android:textSize="@dimen/size_text_action" />
+
+        </LinearLayout>
+
+
+        <ImageView
+            android:id="@+id/ivAvatar"
+            android:layout_width="@dimen/base_margin_x4"
+            android:layout_height="@dimen/base_margin_x4"
+            android:scaleType="centerCrop"
+            android:visibility="visible"
+            android:src="@drawable/ic_default_avatar"/>
+    </LinearLayout>
+
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_marginTop="@dimen/base_margin"
+        android:baselineAligned="false"
+        android:orientation="horizontal">
+
+
+        <LinearLayout
+            android:id="@+id/llCallback"
+            android:layout_width="0dp"
+            android:layout_height="wrap_content"
+            android:layout_weight="0.5"
+            android:background="@drawable/rounded_button_accept"
+            android:clickable="true"
+            android:focusable="true"
+            android:gravity="center"
+            android:orientation="horizontal">
+
+            <TextView
+                android:id="@+id/tvCallback"
+                style="@style/Widget.Compat.NotificationActionText"
+                android:layout_width="wrap_content"
+                android:layout_height="@dimen/base_margin_x4"
+                android:gravity="center"
+                android:text="@string/text_call_back"
+                android:textAllCaps="false"
+                android:textColor="@android:color/white" />
+
+        </LinearLayout>
+
+        <LinearLayout
+            android:layout_width="0dp"
+            android:layout_height="wrap_content"
+            android:layout_weight="0.5"
+            android:background="@drawable/rounded_button_accept"
+            android:clickable="true"
+            android:focusable="true"
+            android:gravity="center"
+            android:orientation="horizontal"/>
+
+    </LinearLayout>
+
+
+</LinearLayout>
+
+    <!--style="@style/Widget.Compat.NotificationActionText"-->

+ 131 - 0
android/src/main/res/layout/layout_custom_notification.xml

@@ -0,0 +1,131 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:orientation="vertical">
+
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content">
+
+        <LinearLayout
+            android:layout_width="0dp"
+            android:layout_height="wrap_content"
+            android:layout_weight="1"
+            android:orientation="vertical">
+
+            <TextView
+                android:id="@+id/tvNameCaller"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:textAppearance="@style/TextAppearance.Compat.Notification.Title"
+                android:textSize="@dimen/size_text_action" />
+
+
+            <TextView
+                android:id="@+id/tvNumber"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:textAppearance="@style/TextAppearance.Compat.Notification.Line2"
+                android:textSize="@dimen/size_text_action" />
+
+        </LinearLayout>
+
+
+        <ImageView
+            android:id="@+id/ivAvatar"
+            android:layout_width="@dimen/base_margin_x4_8"
+            android:layout_height="@dimen/base_margin_x4_8"
+            android:scaleType="centerCrop"
+            android:src="@drawable/ic_default_avatar"
+            android:visibility="invisible" />
+    </LinearLayout>
+
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_marginTop="@dimen/base_margin_half"
+        android:orientation="horizontal">
+
+
+        <LinearLayout
+            android:id="@+id/llDecline"
+            android:layout_width="0dp"
+            android:layout_height="@dimen/base_margin_x4_8"
+            android:layout_weight="1"
+            android:background="@drawable/rounded_button_decline"
+            android:clickable="true"
+            android:focusable="true"
+            android:gravity="center"
+            android:orientation="horizontal">
+
+            <ImageView
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_marginRight="@dimen/base_margin_half"
+                android:layout_marginLeft="@dimen/base_margin_half"
+                android:src="@drawable/ic_decline"
+                android:tint="@android:color/white"
+                app:tint="@android:color/white"
+                tools:ignore="UseAppTint" />
+
+            <TextView
+                android:id="@+id/tvDecline"
+                style="@style/Widget.Compat.NotificationActionText"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:gravity="center"
+                android:text="@string/text_decline"
+                android:textAllCaps="false"
+                android:layout_marginRight="@dimen/base_margin_half"
+                android:layout_marginLeft="@dimen/base_margin_half"
+                android:textColor="@color/action_text" />
+
+        </LinearLayout>
+
+        <LinearLayout
+            android:layout_width="@dimen/base_margin"
+            android:layout_height="match_parent" />
+
+        <LinearLayout
+            android:id="@+id/llAccept"
+            android:layout_width="0dp"
+            android:layout_height="@dimen/base_margin_x4_8"
+            android:layout_weight="1"
+            android:background="@drawable/rounded_button_accept"
+            android:clickable="true"
+            android:focusable="true"
+            android:gravity="center"
+            android:orientation="horizontal">
+
+            <ImageView
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_marginRight="@dimen/base_margin_half"
+                android:layout_marginLeft="@dimen/base_margin_half"
+                android:src="@drawable/ic_accept"
+                android:tint="@android:color/white"
+                app:tint="@android:color/white"
+                tools:ignore="UseAppTint" />
+
+            <TextView
+                android:id="@+id/tvAccept"
+                style="@style/Widget.Compat.NotificationActionText"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:gravity="center"
+                android:text="@string/text_accept"
+                android:textAllCaps="false"
+                android:layout_marginRight="@dimen/base_margin_half"
+                android:layout_marginLeft="@dimen/base_margin_half"
+                android:textColor="@color/action_text" />
+
+        </LinearLayout>
+
+
+    </LinearLayout>
+
+
+</LinearLayout>

+ 103 - 0
android/src/main/res/layout/layout_custom_ongoing_notification.xml

@@ -0,0 +1,103 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:orientation="vertical">
+
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content">
+
+        <LinearLayout
+            android:layout_width="0dp"
+            android:layout_weight="1"
+            android:layout_height="wrap_content"
+            android:orientation="vertical">
+
+            <LinearLayout
+                android:layout_width="match_parent"
+                android:layout_height="wrap_content"
+                android:orientation="horizontal">
+                <TextView
+                    android:id="@+id/tvNameCaller"
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:textAppearance="@style/TextAppearance.Compat.Notification.Title"
+                    android:textSize="@dimen/size_text_action" />
+
+
+                <TextView
+                    android:id="@+id/tvNumber"
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:textAppearance="@style/TextAppearance.Compat.Notification.Title"
+                    android:textSize="@dimen/size_text_action" />
+            </LinearLayout>
+
+
+            <TextView
+                android:id="@+id/tvTapOpen"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:textAppearance="@style/TextAppearance.Compat.Notification.Line2"
+                android:textSize="@dimen/size_text_sub" />
+
+        </LinearLayout>
+
+
+        <ImageView
+            android:id="@+id/ivAvatar"
+            android:layout_width="@dimen/base_margin_x4"
+            android:layout_height="@dimen/base_margin_x4"
+            android:scaleType="centerCrop"
+            android:visibility="visible"
+            android:src="@drawable/ic_default_avatar"/>
+    </LinearLayout>
+
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="wrap_content"
+        android:layout_marginTop="@dimen/base_margin"
+        android:baselineAligned="false"
+        android:orientation="horizontal">
+
+
+        <LinearLayout
+            android:id="@+id/llHangup"
+            android:layout_width="0dp"
+            android:layout_height="wrap_content"
+            android:layout_weight="0.5"
+            android:background="@drawable/rounded_button_accept"
+            android:clickable="true"
+            android:focusable="true"
+            android:gravity="center"
+            android:orientation="horizontal">
+
+            <TextView
+                android:id="@+id/tvHangUp"
+                style="@style/Widget.Compat.NotificationActionText"
+                android:layout_width="wrap_content"
+                android:layout_height="@dimen/base_margin_x4"
+                android:gravity="center"
+                android:text="@string/text_hang_up"
+                android:textAllCaps="false"
+                android:textColor="@android:color/white" />
+
+        </LinearLayout>
+
+        <LinearLayout
+            android:layout_width="0dp"
+            android:layout_height="wrap_content"
+            android:layout_weight="0.5"
+            android:background="@drawable/rounded_button_accept"
+            android:clickable="true"
+            android:focusable="true"
+            android:gravity="center"
+            android:orientation="horizontal"/>
+
+    </LinearLayout>
+
+
+</LinearLayout>
+
+    <!--style="@style/Widget.Compat.NotificationActionText"-->

+ 143 - 0
android/src/main/res/layout/layout_custom_small_ex_notification.xml

@@ -0,0 +1,143 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:orientation="vertical">
+
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_gravity="center_vertical"
+        android:layout_height="wrap_content">
+
+        <ImageView
+            android:id="@+id/ivAvatar"
+            android:layout_width="@dimen/base_margin_x4"
+            android:layout_height="@dimen/base_margin_x4"
+            android:scaleType="centerCrop"
+            android:src="@drawable/ic_default_avatar"
+            android:visibility="invisible" />
+
+        <LinearLayout
+            android:layout_width="@dimen/base_margin_half"
+            android:layout_height="match_parent" />
+
+        <LinearLayout
+            android:layout_width="0dp"
+            android:layout_height="match_parent"
+            android:layout_weight="1"
+            android:orientation="vertical">
+
+            <TextView
+                android:id="@+id/tvNameCaller"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:singleLine="true"
+                android:maxLines="1"
+                android:textAppearance="@style/TextAppearance.Compat.Notification.Title"
+                android:textSize="@dimen/size_text_action" />
+
+
+            <TextView
+                android:id="@+id/tvNumber"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:singleLine="true"
+                android:maxLines="1"
+                android:textAppearance="@style/TextAppearance.Compat.Notification.Line2"
+                android:textSize="@dimen/size_text_action" />
+
+
+
+
+        </LinearLayout>
+
+
+        <LinearLayout
+            android:layout_width="wrap_content"
+            android:layout_height="wrap_content"
+            android:layout_gravity="center_vertical"
+            android:orientation="horizontal">
+
+
+            <LinearLayout
+                android:id="@+id/llDecline"
+                android:layout_width="@dimen/base_margin_x4"
+                android:layout_height="@dimen/base_margin_x4"
+                android:background="@drawable/rounded_button_decline"
+                android:clickable="true"
+                android:focusable="true"
+                android:gravity="center"
+                android:orientation="horizontal">
+
+                <ImageView
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:layout_marginLeft="@dimen/base_margin_half"
+                    android:layout_marginRight="@dimen/base_margin_half"
+                    android:src="@drawable/ic_decline"
+                    android:tint="@android:color/white"
+                    app:tint="@android:color/white"
+                    tools:ignore="UseAppTint" />
+
+                <TextView
+                    android:id="@+id/tvDecline"
+                    style="@style/Widget.Compat.NotificationActionText"
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:layout_marginLeft="@dimen/base_margin_half"
+                    android:layout_marginRight="@dimen/base_margin_half"
+                    android:visibility="gone"
+                    android:gravity="center"
+                    android:text="@string/text_decline"
+                    android:textAllCaps="false"
+                    android:textColor="@color/action_text" />
+
+            </LinearLayout>
+
+            <LinearLayout
+                android:layout_width="@dimen/base_margin_x2"
+                android:layout_height="match_parent" />
+
+            <LinearLayout
+                android:id="@+id/llAccept"
+                android:layout_width="@dimen/base_margin_x4"
+                android:layout_height="@dimen/base_margin_x4"
+                android:background="@drawable/rounded_button_accept"
+                android:clickable="true"
+                android:focusable="true"
+                android:gravity="center"
+                android:orientation="horizontal">
+
+                <ImageView
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:layout_marginLeft="@dimen/base_margin_half"
+                    android:layout_marginRight="@dimen/base_margin_half"
+                    android:src="@drawable/ic_accept"
+                    android:tint="@android:color/white"
+                    app:tint="@android:color/white"
+                    tools:ignore="UseAppTint" />
+
+                <TextView
+                    android:id="@+id/tvAccept"
+                    style="@style/Widget.Compat.NotificationActionText"
+                    android:layout_width="wrap_content"
+                    android:layout_height="wrap_content"
+                    android:layout_marginLeft="@dimen/base_margin_half"
+                    android:layout_marginRight="@dimen/base_margin_half"
+                    android:gravity="center"
+                    android:text="@string/text_accept"
+                    android:visibility="gone"
+                    android:textAllCaps="false"
+                    android:textColor="@color/action_text" />
+
+            </LinearLayout>
+
+
+        </LinearLayout>
+    </LinearLayout>
+
+
+</LinearLayout>

+ 138 - 0
android/src/main/res/layout/layout_custom_small_notification.xml

@@ -0,0 +1,138 @@
+<?xml version="1.0" encoding="utf-8"?>
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+    xmlns:app="http://schemas.android.com/apk/res-auto"
+    xmlns:tools="http://schemas.android.com/tools"
+    android:layout_width="match_parent"
+    android:layout_height="wrap_content"
+    android:layout_weight="1"
+    android:orientation="vertical"
+    android:paddingLeft="4dp"
+    android:paddingTop="4dp"
+    android:paddingRight="4dp"
+    android:paddingBottom="12dp">
+
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="0dp"
+        android:layout_weight="1">
+
+        <LinearLayout
+            android:layout_width="0dp"
+            android:layout_height="wrap_content"
+            android:layout_weight="1"
+            android:orientation="vertical">
+
+            <TextView
+                android:id="@+id/tvNameCaller"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:textAppearance="@style/TextAppearance.Compat.Notification.Title"
+                android:textSize="@dimen/size_text_action" />
+
+
+            <TextView
+                android:id="@+id/tvNumber"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:textAppearance="@style/TextAppearance.Compat.Notification.Line2"
+                android:textSize="@dimen/size_text_action" />
+
+        </LinearLayout>
+
+
+        <ImageView
+            android:id="@+id/ivAvatar"
+            android:layout_width="@dimen/base_margin_x4"
+            android:layout_height="@dimen/base_margin_x4"
+            android:scaleType="centerCrop"
+            android:src="@drawable/ic_default_avatar"
+            android:visibility="invisible" />
+    </LinearLayout>
+
+    <LinearLayout
+        android:layout_width="match_parent"
+        android:layout_height="0dp"
+        android:layout_marginTop="@dimen/base_margin"
+        android:layout_weight="1"
+        android:orientation="horizontal">
+
+
+        <LinearLayout
+            android:id="@+id/llDecline"
+            android:layout_width="0dp"
+            android:layout_height="@dimen/base_margin_x3_5"
+            android:layout_weight="1"
+            android:background="@drawable/rounded_button_decline"
+            android:clickable="true"
+            android:focusable="true"
+            android:gravity="center"
+            android:orientation="horizontal">
+
+            <ImageView
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_marginLeft="@dimen/base_margin_half"
+                android:layout_marginRight="@dimen/base_margin_half"
+                android:src="@drawable/ic_decline"
+                android:tint="@android:color/white"
+                app:tint="@android:color/white"
+                tools:ignore="UseAppTint" />
+
+            <TextView
+                android:id="@+id/tvDecline"
+                style="@style/Widget.Compat.NotificationActionText"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_marginLeft="@dimen/base_margin_half"
+                android:layout_marginRight="@dimen/base_margin_half"
+                android:gravity="center"
+                android:text="@string/text_decline"
+                android:textAllCaps="false"
+                android:textColor="@color/action_text" />
+
+        </LinearLayout>
+
+        <LinearLayout
+            android:layout_width="@dimen/base_margin"
+            android:layout_height="match_parent" />
+
+        <LinearLayout
+            android:id="@+id/llAccept"
+            android:layout_width="0dp"
+            android:layout_height="@dimen/base_margin_x3_5"
+            android:layout_weight="1"
+            android:background="@drawable/rounded_button_accept"
+            android:clickable="true"
+            android:focusable="true"
+            android:gravity="center"
+            android:orientation="horizontal">
+
+            <ImageView
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_marginLeft="@dimen/base_margin_half"
+                android:layout_marginRight="@dimen/base_margin_half"
+                android:src="@drawable/ic_accept"
+                android:tint="@android:color/white"
+                app:tint="@android:color/white"
+                tools:ignore="UseAppTint" />
+
+            <TextView
+                android:id="@+id/tvAccept"
+                style="@style/Widget.Compat.NotificationActionText"
+                android:layout_width="wrap_content"
+                android:layout_height="wrap_content"
+                android:layout_marginLeft="@dimen/base_margin_half"
+                android:layout_marginRight="@dimen/base_margin_half"
+                android:gravity="center"
+                android:text="@string/text_accept"
+                android:textAllCaps="false"
+                android:textColor="@color/action_text" />
+
+        </LinearLayout>
+
+
+    </LinearLayout>
+
+
+</LinearLayout>

BIN
android/src/main/res/raw/ringtone_default.mp3


+ 4 - 0
android/src/main/res/values-sw600dp/dimens.xml

@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <bool name="isTablet">true</bool>
+</resources>

+ 11 - 0
android/src/main/res/values/attrs.xml

@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+    <declare-styleable name="ripple_relativeLayout">
+        <attr name="ripple_color" format="color" />
+        <attr name="ripple_radius" format="dimension"/>
+        <attr name="ripple_duration" format="integer"/>
+        <attr name="ripple_amount" format="integer"/>
+        <attr name="ripple_scale" format="float"/>
+    </declare-styleable>
+</resources>

+ 11 - 0
android/src/main/res/values/colors.xml

@@ -0,0 +1,11 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+    <color name="accept">#4CAF50</color>
+    <color name="accept_ripple">#A6FFA9</color>
+    <color name="decline">#F44336</color>
+    <color name="decline_ripple">#FB8D85</color>
+
+    <color name="action_text">#FFFFFF</color>
+    <color name="ripple_main_color">#80ffffff</color>
+</resources>

+ 25 - 0
android/src/main/res/values/dimens.xml

@@ -0,0 +1,25 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+    <bool name="isTablet">false</bool>
+    <dimen name="base_margin_half">5dp</dimen>
+    <dimen name="base_margin">10dp</dimen>
+    <dimen name="base_margin_x1_5">15dp</dimen>
+    <dimen name="base_margin_x2">20dp</dimen>
+    <dimen name="base_margin_x2_5">25dp</dimen>
+    <dimen name="base_margin_x3">30dp</dimen>
+    <dimen name="base_margin_x3_5">35dp</dimen>
+    <dimen name="base_margin_x4">40dp</dimen>
+    <dimen name="base_margin_x4_8">48dp</dimen>
+    <dimen name="base_margin_x5">50dp</dimen>
+    <dimen name="base_margin_x6">60dp</dimen>
+
+    <dimen name="base_margin_revert_x5">-50dp</dimen>
+    <dimen name="size_avatar">120dp</dimen>
+    <dimen name="size_button">60dp</dimen>
+    <dimen name="size_button_x2">120dp</dimen>
+    <dimen name="size_button_x2_5">150dp</dimen>
+
+    <dimen name="size_text_name">24sp</dimen>
+    <dimen name="size_text_action">14sp</dimen>
+    <dimen name="size_text_sub">12sp</dimen>
+</resources>

+ 12 - 0
android/src/main/res/values/strings.xml

@@ -0,0 +1,12 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+    <string name="text_accept">Accept</string>
+    <string name="text_decline">Decline</string>
+    <string name="text_missed_call">Missed call</string>
+    <string name="text_call_back">Call back</string>
+    <string name="text_calling">Calling…</string>
+    <string name="text_hang_up">Hang up</string>
+    <string name="text_tab_open">Tap to open the app.</string>
+    <string name="text_post_notification_message_required">Notification permission is required, Please allow notification permission from setting.</string>
+</resources>

+ 31 - 0
android/src/main/res/values/styles.xml

@@ -0,0 +1,31 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources>
+
+
+    <style name="CallkitIncomingTheme" parent="@style/Theme.AppCompat">
+
+        <item name="android:windowTranslucentStatus">true</item>
+        <item name="android:windowTranslucentNavigation">true</item>
+
+    </style>
+
+    <style name="DialogTheme" parent="@style/Theme.AppCompat.Light.Dialog.Alert">
+
+    </style>
+
+
+    <style name="TranslucentTheme" parent="@android:style/Theme.Translucent.NoTitleBar">
+        <item name="android:background">@android:color/transparent</item>
+        <item name="background">@android:color/transparent</item>
+        <item name="android:windowContentOverlay">@null</item>
+        <item name="android:windowNoTitle">true</item>
+        <item name="windowNoTitle">true</item>
+        <item name="android:windowBackground">@android:color/transparent</item>
+        <item name="android:colorBackgroundCacheHint">@null</item>
+        <item name="android:windowIsTranslucent">true</item>
+        <item name="android:windowAnimationStyle">@android:style/Animation</item>
+        <item name="android:windowDisablePreview">true</item>
+        <item name="android:windowNoDisplay">true</item>
+    </style>
+
+</resources>

+ 32 - 0
flutter_callkit_incoming.iml

@@ -0,0 +1,32 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<module type="JAVA_MODULE" version="4">
+  <component name="NewModuleRootManager" inherit-compiler-output="true">
+    <exclude-output />
+    <content url="file://$MODULE_DIR$">
+      <excludeFolder url="file://$MODULE_DIR$/.dart_tool" />
+      <excludeFolder url="file://$MODULE_DIR$/.pub" />
+      <excludeFolder url="file://$MODULE_DIR$/build" />
+      <excludeFolder url="file://$MODULE_DIR$/example/.dart_tool" />
+      <excludeFolder url="file://$MODULE_DIR$/example/.pub" />
+      <excludeFolder url="file://$MODULE_DIR$/example/build" />
+      <excludeFolder url="file://$MODULE_DIR$/example/ios/.symlinks/plugins/flutter_callkit_incoming/.dart_tool" />
+      <excludeFolder url="file://$MODULE_DIR$/example/ios/.symlinks/plugins/flutter_callkit_incoming/.pub" />
+      <excludeFolder url="file://$MODULE_DIR$/example/ios/.symlinks/plugins/flutter_callkit_incoming/build" />
+      <excludeFolder url="file://$MODULE_DIR$/example/ios/.symlinks/plugins/firebase_core/example/build" />
+      <excludeFolder url="file://$MODULE_DIR$/example/ios/.symlinks/plugins/firebase_core/example/.dart_tool" />
+      <excludeFolder url="file://$MODULE_DIR$/example/ios/.symlinks/plugins/firebase_core/example/.pub" />
+      <excludeFolder url="file://$MODULE_DIR$/example/ios/.symlinks/plugins/firebase_messaging/.dart_tool" />
+      <excludeFolder url="file://$MODULE_DIR$/example/ios/.symlinks/plugins/firebase_messaging/.pub" />
+      <excludeFolder url="file://$MODULE_DIR$/example/ios/.symlinks/plugins/firebase_messaging/build" />
+      <excludeFolder url="file://$MODULE_DIR$/example/ios/.symlinks/plugins/firebase_core/.pub" />
+      <excludeFolder url="file://$MODULE_DIR$/example/ios/.symlinks/plugins/firebase_core/.dart_tool" />
+      <excludeFolder url="file://$MODULE_DIR$/example/ios/.symlinks/plugins/firebase_core/build" />
+      <excludeFolder url="file://$MODULE_DIR$/example/ios/.symlinks/plugins/firebase_messaging/example/.dart_tool" />
+      <excludeFolder url="file://$MODULE_DIR$/example/ios/.symlinks/plugins/firebase_messaging/example/.pub" />
+      <excludeFolder url="file://$MODULE_DIR$/example/ios/.symlinks/plugins/firebase_messaging/example/build" />
+    </content>
+    <orderEntry type="sourceFolder" forTests="false" />
+    <orderEntry type="library" name="Dart SDK" level="project" />
+    <orderEntry type="library" name="Flutter Plugins" level="project" />
+  </component>
+</module>

BIN
images/KeychainAccess.png


BIN
images/Setting.png


BIN
images/TestingApp.png


BIN
images/VoIPServicesCertificate.png


BIN
images/Xcode-S1.png


BIN
images/Xcode-S2.png


BIN
images/Xcode-S3.png


BIN
images/image1.png


BIN
images/image2.png


BIN
images/image3.png


BIN
images/image4.jpg


BIN
images/image5.jpg


Một số tệp đã không được hiển thị bởi vì quá nhiều tập tin thay đổi trong này khác