> For the complete documentation index, see [llms.txt](https://docs.etelecom.vn/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.etelecom.vn/tich-hop-api/tong-dai/voip-sdk/flutter.md).

# Flutter

<figure><img src="/files/mybPg9A84P2De26fk5Ry" alt=""><figcaption><p>Platform channels</p></figcaption></figure>

### Cấu hình cho iOS

Trong file `AppDelegate.swift`:

````swift
```swift
import UIKit
import Flutter
import PushKit
import UserNotifications
import PortSIPVoIPSDK

@UIApplicationMain class AppDelegate: FlutterAppDelegate, PKPushRegistryDelegate {
  
  private let channel = "name_channel"
  
  private var portsipService: PortsipService! = PortsipService()
  
  override func application(
    _ application: UIApplication,
    didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?
  ) -> Bool {
  
    UserDefaults.standard.register(defaults: ["CallKit": true])
    UserDefaults.standard.register(defaults: ["PushNotification": true])
    UserDefaults.standard.register(defaults: ["ForceBackground": true])
  
    fController = window?.rootViewController as? FlutterViewController
    
    FlutterMethodChannel(name: channel, binaryMessenger: fController.binaryMessenger).setMethodCallHandler({
      [weak self] (call: FlutterMethodCall, result: FlutterResult) -> Void in
      // Note: this method is invoked on the UI thread.
      
      switch call.method {
      case "registerPortsip":
        let args = call.arguments as? [String: Any]
        let username = args?["username"] as? String
        let password = args?["password"] as? String
        let domain = args?["domain"] as? String
        let server = args?["sipServer"] as? String
        
        if username != nil && password != nil && domain != nil {
          let res = self?.registerPortSip(username: username!, password: password!, domain: domain!, sipServer: server!)
          if res == 0 || res == -60021 || res == -60095 || res == -60098 {
            result(Int32(res!))
          } else {
            result(FlutterError(code: "REGISTER FAILED", message: "Kết nối không thành công.", details: nil))
          }
        } else {
          result(FlutterError(code: "NOT AUTHENTICATED", message: "Username và password không đúng.", details: nil))
        }
        
      case "unregisterPortsip":
        if (self?.portsipService != nil) {
          self?.updatePushStatusToSipServer(willPush: false)
          self?.portsipService.unregisterPortsip()
        }
        
      case "callOut":
        let args = call.arguments as? [String: Any]
        let phoneNumber = args?["phoneNumber"] as? String
        let videoCall = args?["videoCall"] as? Bool
        
        if phoneNumber != nil {
          let res = self?.portsipService.callOut(phoneNumber: phoneNumber!, videoCall: videoCall ?? false)
          if res! {
            result(nil)
          } else {
            result(FlutterError(code: "CALL FAILED", message: "Không thể thực hiện cuộc gọi", details: nil))
          }
        }
      case "hangUp":
        let res = self?.portsipService.hangUp()
        if res! {
          result(nil)
        } else {
          result(FlutterError(code: "HANGUP FAILED", message: "Không thể kết thúc cuộc gọi", details: nil))
        }
      case "answerCall":
        let res = self?.portsipService.answerCall()
        if res! {
          result(nil)
        } else {
          result(FlutterError(code: "ANSWER_CALL FAILED", message: "Không thể trả lời cuộc gọi", details: nil))
        }
      case "rejectCall":
        let res = self?.portsipService.rejectCall()
        if res! {
          result(nil)
        } else {
          result(FlutterError(code: "REJECT_CALL FAILED", message: "Không thể từ chối cuộc gọi", details: nil))
        }
      case "hold":
        let res = self?.portsipService.hold()
        if res! {
          result(nil)
        } else {
          result(FlutterError(code: "HOLD_CALL FAILED", message: "Không thể giữ máy", details: nil))
        }
      case "unHold":
        let res = self?.portsipService.unHold()
        if res! {
          result(nil)
        } else {
          result(FlutterError(code: "UNHOLD_CALL FAILED", message: "Không thể tiếp tục cuộc gọi", details: nil))
        }
      case "speakerOn":
        self?.portsipService.speakerOn()
        result(nil)
      case "speakerOff":
        self?.portsipService.speakerOff()
        result(nil)
      case "microphoneOn":
        self?.portsipService.turnOnMicrophone()
        result(nil)
      case "microphoneOff":
        self?.portsipService.turnOffMicrophone()
        result(nil)
      case "frontCamera":
        self?.portsipService.switchToFrontCamera()
        result(nil)
      case "backCamera":
        self?.portsipService.switchToBackCamera()
        result(nil)
      case "cameraOn":
        self?.portsipService.turnOnCamera()
        result(nil)
      case "cameraOff":
        self?.portsipService.turnOffCamera()
        result(nil)
        
      default:
        result(FlutterMethodNotImplemented)
      }
    })
    
    GeneratedPluginRegistrant.register(with: self)
    return super.application(application, didFinishLaunchingWithOptions: launchOptions)
  }
  
  private func registerPortSip(username: String, password: String, domain: String, sipServer: String) -> Int32 {
    PortsipService.fController = fController
    
    if !CallManager.portsipRegistered {
      CallManager.portsipExtension = username
      CallManager.portsipPassword = password
      CallManager.portsipDomain = domain
      CallManager.portsipServer = sipServer
      
      portsipService.registerPortsip()
    } else {
      portsipService.refreshRegistrationPortsip()
    }
    
    return 0
  }
  
}

```
````

Tham khảo thêm các function tại đây: <https://docs.etelecom.vn/tong-dai/sdk/ios>

### Cấu hình cho Android

````kotlin
```kotlin

import android.Manifest
import android.content.Intent
import android.content.pm.PackageManager
import android.os.Build
import android.util.Log
import android.view.KeyEvent
import androidx.annotation.NonNull
import androidx.annotation.RequiresApi
import androidx.core.app.ActivityCompat
import androidx.core.content.ContextCompat
import com.portsip.PortSipErrorcode
import io.flutter.embedding.android.FlutterActivity
import io.flutter.embedding.engine.FlutterEngine
import io.flutter.plugin.common.MethodChannel

@RequiresApi(Build.VERSION_CODES.O)
class MainActivity : FlutterActivity() {
  
  companion object {
    lateinit var shared: MainActivity
    const val channel = "name_channel"
  }
  
  override fun configureFlutterEngine(@NonNull flutterEngine: FlutterEngine) {
    super.configureFlutterEngine(flutterEngine)
    
    shared = this
    PortsipService.engineF = flutterEngine
  
    MethodChannel(flutterEngine.dartExecutor.binaryMessenger, channel).setMethodCallHandler { call, result ->
      // Note: this method is invoked on the main thread.
      when (call.method) {
        "registerPortsip" -> {
          val username = call.argument<String>("username")
          val password = call.argument<String>("password")
          val domain = call.argument<String>("domain")
          val server = call.argument<String>("sipServer")
    
          if (username != null && password != null && domain != null && server != null) {
            val res = registerPortsip(username, password, domain, server)
            if (res == 0 || res == PortSipErrorcode.ECoreAlreadyRegistered ||
              res == PortSipErrorcode.ECoreAllowOnlyOneUser ||
              res == PortSipErrorcode.ECoreCreateTransportFailure
            ) {
              result.success(res)
            } else {
              result.error("REGISTER FAILED", "Kết nối không thành công.", null)
            }
          } else {
            result.error("NOT AUTHENTICATED", "Username và password không đúng.", null)
          }
        }
        "unregisterPortsip" -> {
          unregisterPortsip()
        }
        "callOut" -> {
          val phoneNumber = call.argument<String>("phoneNumber")
          val videoCall = call.argument<Boolean>("videoCall")

          if (phoneNumber != null) {
            val res = callOut(phoneNumber, videoCall ?: false)
            if (res) {
              result.success(null)
            } else {
              result.error("CALL FAILED", "Không thể thực hiện cuộc gọi", null)
            }
          }
        }
        "hangUp" -> {
          val res = hangUp()
          if (res) {
            result.success(null)
          } else {
            result.error("HANGUP FAILED", "Không thể kết thúc cuộc gọi", null)
          }
        }
        "answerCall" -> {
          val res = answerCall()
          if (res) {
            result.success(null)
          } else {
            result.error("ANSWER_CALL FAILED", "Không thể trả lời cuộc gọi", null)
          }
        }
        "rejectCall" -> {
          val res = rejectCall()
          if (res) {
            result.success(null)
          } else {
            result.error("REJECT_CALL FAILED", "Không thể từ chối cuộc gọi", null)
          }
        }
        "hold" -> {
          val res = hold()
          if (res) {
            result.success(null)
          } else {
            result.error("HOLD FAILED", "Không thể giữ máy", null)
          }
        }
        "unHold" -> {
          val res = unHold()
          if (res) {
            result.success(null)
          } else {
            result.error("UN_HOLD FAILED", "Không thể tiếp tục giữ máy", null)
          }
        }
        "speakerOn" -> {
          PortsipService.shared?.speakerOn()
          result.success(null)
        }
        "speakerOff" -> {
          PortsipService.shared?.speakerOff()
          result.success(null)
        }
        "microphoneOn" -> {
          PortsipService.shared?.turnOnMicrophone()
          result.success(null)
        }
        "microphoneOff" -> {
          PortsipService.shared?.turnOffMicrophone()
          result.success(null)
        }
        "frontCamera" -> {
          PortsipService.shared?.switchToFrontCamera()
          result.success(null)
        }
        "backCamera" -> {
          PortsipService.shared?.switchToBackCamera()
          result.success(null)
        }
        "cameraOn" -> {
          PortsipService.shared?.turnOnCamera()
          result.success(null)
        }
        "cameraOff" -> {
          PortsipService.shared?.turnOffCamera()
          result.success(null)
        }
        "finishDisposingCamera" -> {
            val intent = Intent(this, VideoCallActivity::class.java)
            intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
            startActivity(intent)
        }
        else -> {
          result.notImplemented()
        }
      }
    }
  }
  
  
  private fun registerPortsip(username: String, password: String, domain: String, server: String): Int {

    val sharedPref = getSharedPreferences("CallManager", MODE_PRIVATE) ?: return 0
    with(sharedPref.edit()) {
      putString(getString(R.string.call_manager_ps_extension), username)
      putString(getString(R.string.call_manager_ps_password), password)
      putString(getString(R.string.call_manager_ps_domain), domain)
      putString(getString(R.string.call_manager_ps_server), server)
      apply()
    }

    Intent(this, PortsipService::class.java).also { intent ->
      startService(intent)
    }

    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
      if (PackageManager.PERMISSION_GRANTED != ContextCompat.checkSelfPermission(
          activity, Manifest.permission.BLUETOOTH_CONNECT)
      ) {
        ActivityCompat.requestPermissions(
          activity,
          arrayOf(Manifest.permission.BLUETOOTH_CONNECT),
          0
        )
      }
    }
    
    return 0

  }
  
  private fun unregisterPortsip() {
    if (PortsipService.shared != null) {
      PortsipService.shared!!.unregisterPortsip()
    }
  }
  
  private fun callOut(phoneNumber: String, videoCall: Boolean = false): Boolean {
    return PortsipService.shared?.callOut(phoneNumber, videoCall) ?: false
  }
  
  private fun hangUp(): Boolean {
    return PortsipService.shared?.hangUp() ?: false
  }
  
  private fun answerCall(): Boolean {
    return PortsipService.shared?.answerCall() ?: false
  }
  
  private fun rejectCall(): Boolean {
    return PortsipService.shared?.rejectCall() ?: false
  }
  
  private fun hold(): Boolean {
    return PortsipService.shared?.hold() ?: false
  }
  
  private fun unHold(): Boolean {
    return PortsipService.shared?.unHold() ?: false
  }
  
}

```
````

Tham khảo thêm các function tại đây: <https://docs.etelecom.vn/tong-dai/sdk/android>

### Cấu hình đối với Flutter UI

```dart
const CHANNEL = 'name_channel';
const platform = const MethodChannel(CHANNEL);

static Future<void> portsipRegister() async {
    try {
      await platform.invokeMethod("registerPortsip", {
        'username': extensionNumber,
        'password': extensionPassword,
        'domain': tenantDomain,
        'sipServer': sipServer
      });
    } on PlatformException catch (e) {
       throw e;
    }
  }
  

static Future<void> callOut(String phoneNumber, bool isVideoCall) async {
    try {
      await platform.invokeMethod("callOut", {
        'phoneNumber': phoneNumber, 'videoCall': isVideoCall
      });
    } on PlatformException catch(e) {
      throw e;
    }
  }
  
 static Future<void> portsipHangUp() async {
    try {
      await platform.invokeMethod("hangUp");
    } on PlatformException catch (e) {
      throw e;
    }
  }

  static Future<void> portsipAnswer() async {
    try {
      await platform.invokeMethod("answerCall");
    } on PlatformException catch (e) {
      throw e;
    }
  }

  static Future<void> portsipReject() async {
    try {
     await platform.invokeMethod("rejectCall");
    } on PlatformException catch (e) {
      throw e;
    }
  }

  static Future<void> portsipHold() async {
    try {
      await platform.invokeMethod("hold");
    } on PlatformException catch (e) {
      throw e;
    }
  }

  static Future<void> portsipUnHold() async {
    try {
      await platform.invokeMethod("unHold");
    } on PlatformException catch (e) {
      throw e;
    }
  }

  static Future<void> portsipSpeakerOn() async {
    try {
      await platform.invokeMethod("speakerOn");
    } on PlatformException catch (e) {
      throw e;
    }
  }

  static Future<void> portsipSpeakerOff() async {
    try {
      await platform.invokeMethod("speakerOff");
    } on PlatformException catch (e) {
      throw e;
    }
  }

  static Future<void> microphoneOn() async {
    try {
      await platform.invokeMethod("microphoneOn");
    } on PlatformException catch (e) {
      throw e;
    }
  }

  static Future<void> microphoneOff() async {
    try {
      await platform.invokeMethod("microphoneOff");
    } on PlatformException catch (e) {
      throw e;
    }
  }

  static Future<void> switchToFrontCamera() async {
    try {
      await platform.invokeMethod("frontCamera");
    } on PlatformException catch (e) {
      throw e;
    }
  }

  static Future<void> switchToBackCamera() async {
    try {
      await platform.invokeMethod("backCamera");
    } on PlatformException catch (e) {
      throw e;
    }
  }

  static Future<void> cameraOn() async {
    try {
      await platform.invokeMethod("cameraOn");
    } on PlatformException catch (e) {
      throw e;
    }
  }

  static Future<void> cameraOff() async {
    try {
      await platform.invokeMethod("cameraOff");
    } on PlatformException catch (e) {
      throw e;
    }
  }

  static void hookNativeEvents(BuildContext context) {

    platform.setMethodCallHandler((call) async {
      try {
        switch (call.method) {
          case 'onRegisterFailure':

            String code = jsonDecode(call.arguments)["code"] ?? "";

            if (code != callsReader.portsipRegisterFailureCode) {
              //todo if onRegisterFailure
            }

            return;

          case 'callIn':
            String? caller = jsonDecode(call.arguments)["caller"];
            String callingNumber = caller!.split('sip:')[1].split('@')[0];

            bool? isVideoCall = jsonDecode(call.arguments)["isVideo"];

            portsipCallIn(
                phoneNumber: callingNumber,
                isVideoCall: isVideoCall == null ? false : isVideoCall
            );

            return;

          case 'callAnswered':
            if (call.arguments != null && call.arguments.length > 0) {
              String? caller = jsonDecode(call.arguments)["caller"];
            }

            return;

          case 'callEnded':
            //code here

            return;

          case 'sipMessageResponse':
            if (call.arguments == null || call.arguments.length == 0) {
              return;
            }

            String sipMessage = jsonDecode(call.arguments)['sipMessage'];
            String _splitMessage = sipMessage.split('X-Session-Id:')[1];
            if (_splitMessage.isEmpty) {
              return;
            }

            String xSessionId = _splitMessage.substring(0, 18);

            return;

          case 'disposeCamera':
            await callsReader.disposeCamera();

            try {
              await platform.invokeMethod("finishDisposingCamera");
            } on PlatformException catch (e) {
              
            }

            return;

        }
      } catch (e) {
        hookNativeEvents(context);
      }
      return;
    });
  }

```


---

# Agent Instructions
This documentation is published with GitBook. GitBook is the documentation platform designed so that both humans and AI agents can read, navigate, and reason over technical content effectively. Learn more at gitbook.com.

## Querying This Documentation
If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://docs.etelecom.vn/tich-hop-api/tong-dai/voip-sdk/flutter.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
