commit 96a7e211a0147539837249e0a9d3aca73a597e24 Author: Andrew nuark G Date: Sat Aug 30 18:46:02 2025 +0700 feat: Working VoIP calling implementation (Flutter + Android) Working video and audio calls, as well as android integration diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b9d7f25 --- /dev/null +++ b/.gitignore @@ -0,0 +1,33 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.build/ +.buildlog/ +.history +.svn/ +.swiftpm/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock. +/pubspec.lock +**/doc/api/ +.dart_tool/ +.flutter-plugins-dependencies +/build/ +/coverage/ diff --git a/.metadata b/.metadata new file mode 100644 index 0000000..3af4229 --- /dev/null +++ b/.metadata @@ -0,0 +1,30 @@ +# 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: "c0f2a1dd60414de7bef59318dc2554a6bb75d4ad" + channel: "beta" + +project_type: plugin + +# Tracks metadata for the flutter migrate command +migration: + platforms: + - platform: root + create_revision: c0f2a1dd60414de7bef59318dc2554a6bb75d4ad + base_revision: c0f2a1dd60414de7bef59318dc2554a6bb75d4ad + - platform: android + create_revision: c0f2a1dd60414de7bef59318dc2554a6bb75d4ad + base_revision: c0f2a1dd60414de7bef59318dc2554a6bb75d4ad + + # User provided section + + # List of Local paths (relative to this file) that should be + # ignored by the migrate tool. + # + # Files that are not part of the templates will be ignored by default. + unmanaged_files: + - 'lib/main.dart' + - 'ios/Runner.xcodeproj/project.pbxproj' diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..41cc7d8 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,3 @@ +## 0.0.1 + +* TODO: Describe initial release. diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..ba75c69 --- /dev/null +++ b/LICENSE @@ -0,0 +1 @@ +TODO: Add your license here. diff --git a/README.md b/README.md new file mode 100644 index 0000000..1215aa9 --- /dev/null +++ b/README.md @@ -0,0 +1,15 @@ +# liblinphone_flutter + +A new Flutter plugin project. + +## Getting Started + +This project is a starting point for a Flutter +[plug-in package](https://flutter.dev/to/develop-plugins), +a specialized package that includes platform-specific implementation code for +Android and/or iOS. + +For help getting started with Flutter development, view the +[online documentation](https://docs.flutter.dev), which offers tutorials, +samples, guidance on mobile development, and a full API reference. + diff --git a/analysis_options.yaml b/analysis_options.yaml new file mode 100644 index 0000000..a5744c1 --- /dev/null +++ b/analysis_options.yaml @@ -0,0 +1,4 @@ +include: package:flutter_lints/flutter.yaml + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/android/.gitignore b/android/.gitignore new file mode 100644 index 0000000..161bdcd --- /dev/null +++ b/android/.gitignore @@ -0,0 +1,9 @@ +*.iml +.gradle +/local.properties +/.idea/workspace.xml +/.idea/libraries +.DS_Store +/build +/captures +.cxx diff --git a/android/build.gradle b/android/build.gradle new file mode 100644 index 0000000..fd4273b --- /dev/null +++ b/android/build.gradle @@ -0,0 +1,68 @@ +group = "xyz.nuark.liblinphone_flutter" +version = "1.0-SNAPSHOT" + +buildscript { + ext.kotlin_version = "2.1.0" + repositories { + google() + mavenCentral() + } + + dependencies { + classpath("com.android.tools.build:gradle:8.9.1") + classpath("org.jetbrains.kotlin:kotlin-gradle-plugin:$kotlin_version") + } +} + +allprojects { + repositories { + google() + mavenCentral() + } +} + +apply plugin: "com.android.library" +apply plugin: "kotlin-android" + +android { + namespace = "xyz.nuark.liblinphone_flutter" + + compileSdk = 36 + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } + + kotlinOptions { + jvmTarget = JavaVersion.VERSION_11 + } + + sourceSets { + main.java.srcDirs += "src/main/kotlin" + test.java.srcDirs += "src/test/kotlin" + } + + defaultConfig { + minSdk = 24 + } + + dependencies { + implementation(files("libs/linphone-sdk-android-5.2.0.aar")) + + testImplementation("org.jetbrains.kotlin:kotlin-test") + testImplementation("org.mockito:mockito-core:5.0.0") + } + + testOptions { + unitTests.all { + useJUnitPlatform() + + testLogging { + events "passed", "skipped", "failed", "standardOut", "standardError" + outputs.upToDateWhen {false} + showStandardStreams = true + } + } + } +} diff --git a/android/gradle/wrapper/gradle-wrapper.jar b/android/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..980502d Binary files /dev/null and b/android/gradle/wrapper/gradle-wrapper.jar differ diff --git a/android/gradle/wrapper/gradle-wrapper.properties b/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..128196a --- /dev/null +++ b/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-9.0-milestone-1-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/android/gradlew b/android/gradlew new file mode 100644 index 0000000..faf9300 --- /dev/null +++ b/android/gradlew @@ -0,0 +1,251 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/android/gradlew.bat b/android/gradlew.bat new file mode 100644 index 0000000..9d21a21 --- /dev/null +++ b/android/gradlew.bat @@ -0,0 +1,94 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/android/libs/linphone-sdk-android-5.2.0.aar b/android/libs/linphone-sdk-android-5.2.0.aar new file mode 100644 index 0000000..e40e966 Binary files /dev/null and b/android/libs/linphone-sdk-android-5.2.0.aar differ diff --git a/android/settings.gradle b/android/settings.gradle new file mode 100644 index 0000000..1f4f06d --- /dev/null +++ b/android/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'liblinphone_flutter' diff --git a/android/src/main/AndroidManifest.xml b/android/src/main/AndroidManifest.xml new file mode 100644 index 0000000..29d67b8 --- /dev/null +++ b/android/src/main/AndroidManifest.xml @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + diff --git a/android/src/main/kotlin/xyz/nuark/liblinphone_flutter/LiblinphoneFlutterPlugin.kt b/android/src/main/kotlin/xyz/nuark/liblinphone_flutter/LiblinphoneFlutterPlugin.kt new file mode 100644 index 0000000..55ece49 --- /dev/null +++ b/android/src/main/kotlin/xyz/nuark/liblinphone_flutter/LiblinphoneFlutterPlugin.kt @@ -0,0 +1,228 @@ +package xyz.nuark.liblinphone_flutter + +import android.app.Activity +import android.util.Log +import android.util.SparseArray +import android.view.TextureView +import android.view.View +import androidx.core.util.isNotEmpty +import androidx.core.util.size +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.EventChannel +import io.flutter.plugin.common.MethodCall +import io.flutter.plugin.common.MethodChannel +import io.flutter.plugin.common.MethodChannel.MethodCallHandler +import io.flutter.plugin.common.MethodChannel.Result +import org.linphone.mediastream.video.capture.CaptureTextureView +import xyz.nuark.liblinphone_flutter.views.LocalViewFactory +import xyz.nuark.liblinphone_flutter.views.RemoteViewFactory + +/** LiblinphoneFlutterPlugin */ +class LiblinphoneFlutterPlugin : FlutterPlugin, ActivityAware, MethodCallHandler { + private lateinit var channel: MethodChannel + + private var registrationEventsChannel: EventChannel.EventSink? = null + private var callEventsChannel: EventChannel.EventSink? = null + + private lateinit var activity: Activity + + private var remoteViewCache = SparseArray(1); + private var localViewCache = SparseArray(1); + + private lateinit var linphoneBridge: LinphoneBridge + + override fun onAttachedToEngine(flutterPluginBinding: FlutterPlugin.FlutterPluginBinding) { + flutterPluginBinding + .platformViewRegistry + .registerViewFactory("liblinphone_flutter.nuark.xyz/remote_view", RemoteViewFactory { + Log.i(TAG, "onAttachedToEngine: caching RemoteView ${it.id}") + remoteViewCache.put(0, it) + }) + flutterPluginBinding + .platformViewRegistry + .registerViewFactory("liblinphone_flutter.nuark.xyz/local_view", LocalViewFactory { + Log.i(TAG, "onAttachedToEngine: caching LocalView ${it.id}") + localViewCache.put(0, it) + }) + + channel = MethodChannel(flutterPluginBinding.binaryMessenger, "liblinphone_flutter") + channel.setMethodCallHandler(this) + + EventChannel( + flutterPluginBinding.binaryMessenger, + "liblinphone_flutter.nuark.xyz/registration_events" + ).setStreamHandler(object : EventChannel.StreamHandler { + override fun onListen( + arguments: Any?, + events: EventChannel.EventSink? + ) { + registrationEventsChannel = events + } + + override fun onCancel(arguments: Any?) { + registrationEventsChannel = null + } + + }) + + EventChannel( + flutterPluginBinding.binaryMessenger, + "liblinphone_flutter.nuark.xyz/call_events" + ).setStreamHandler(object : EventChannel.StreamHandler { + override fun onListen( + arguments: Any?, + events: EventChannel.EventSink? + ) { + callEventsChannel = events + } + + override fun onCancel(arguments: Any?) { + callEventsChannel = null + } + }) + } + + override fun onMethodCall( + call: MethodCall, + result: Result + ) { + when (call.method) { + "checkPermissions" -> { + result.success(linphoneBridge.checkPermissions()) + } + + "initialize" -> { + try { + linphoneBridge = LinphoneBridge( + activity, + this::acquireRemoteView, + this::acquireLocalView, + { registrationState -> + registrationEventsChannel?.success(registrationState) + }, + { callState -> + callEventsChannel?.success(callState) + } + ) + linphoneBridge.initializeLinphone() + result.success(true) + } catch (e: Exception) { + Log.e(TAG, "initialize: ${e.message}") + result.error("error", e.message, e) + } + } + + "register" -> { + try { + val username = call.argument("username")!! + val password = call.argument("password")!! + val serverIp = call.argument("serverIp")!! + val serverPort = call.argument("serverPort")!! + linphoneBridge.register(username, password, serverIp, serverPort) + result.success(true) + } catch (e: Exception) { + Log.e(TAG, "register: ${e.message}") + result.error("error", e.message, e) + } + } + + "unregister" -> { + linphoneBridge.unregister() + result.success(true) + } + + "makeCall" -> { + try { + val callTo = call.argument("callTo")!! + val isVideoEnabled = call.argument("isVideoEnabled")!! + linphoneBridge.makeCall(callTo, isVideoEnabled) + result.success(true) + } catch (e: Exception) { + Log.e(TAG, "makeCall: ${e.message}") + result.error("error", e.message, e) + } + } + + "answerCall" -> { + val res = linphoneBridge.answerCall() + result.success(res) + } + + "hangupCall" -> { + val res = linphoneBridge.hangupCall() + result.success(res) + } + + "inCall" -> { + val inCall = linphoneBridge.inCall() + result.success(inCall) + } + + "callType" -> { + val callType = linphoneBridge.callType() + result.success(callType.ordinal) + } + + "toggleVideo" -> { + val enabled = linphoneBridge.toggleVideo() + result.success(enabled) + } + + "toggleMicrophone" -> { + val enabled = linphoneBridge.toggleMicrophone() + result.success(enabled) + } + + "stop" -> { + linphoneBridge.stop() + result.success(true) + } + + else -> { + result.notImplemented() + } + } + } + + override fun onDetachedFromEngine(binding: FlutterPlugin.FlutterPluginBinding) { + channel.setMethodCallHandler(null) + } + + override fun onAttachedToActivity(binding: ActivityPluginBinding) { + activity = binding.activity + } + + override fun onDetachedFromActivityForConfigChanges() { + } + + override fun onReattachedToActivityForConfigChanges(binding: ActivityPluginBinding) { + activity = binding.activity + } + + override fun onDetachedFromActivity() { + } + + internal fun acquireLocalView(): CaptureTextureView? { + Log.i(TAG, "acquireLocalView: ${localViewCache.size}") + if (localViewCache.isNotEmpty()) { + val widget = localViewCache.get(0) + return widget as CaptureTextureView + } + return null + } + + internal fun acquireRemoteView(): TextureView? { + Log.i(TAG, "acquireRemoteView: ${remoteViewCache.size}") + if (remoteViewCache.isNotEmpty()) { + val widget = remoteViewCache.get(0) + return widget as TextureView + } + return null + } + + companion object { + const val TAG = "LiblinphoneFlutterPlugin" + } +} diff --git a/android/src/main/kotlin/xyz/nuark/liblinphone_flutter/LinphoneBridge.kt b/android/src/main/kotlin/xyz/nuark/liblinphone_flutter/LinphoneBridge.kt new file mode 100644 index 0000000..6ca97f3 --- /dev/null +++ b/android/src/main/kotlin/xyz/nuark/liblinphone_flutter/LinphoneBridge.kt @@ -0,0 +1,357 @@ +package xyz.nuark.liblinphone_flutter + +import android.Manifest +import android.app.Activity +import android.content.pm.PackageManager +import android.util.Log +import android.view.TextureView +import androidx.core.content.ContextCompat +import org.linphone.core.Account +import org.linphone.core.Call +import org.linphone.core.Core +import org.linphone.core.CoreListenerStub +import org.linphone.core.Factory +import org.linphone.core.MediaDirection +import org.linphone.core.PayloadType +import org.linphone.core.ProxyConfig +import org.linphone.core.RegistrationState +import org.linphone.mediastream.video.capture.CaptureTextureView +import kotlin.properties.Delegates + +class LinphoneBridge( + private val activity: Activity, + private val remoteViewAcquisitor: () -> TextureView?, + private val localViewAcquisitor: () -> CaptureTextureView?, + private val onRegistrationStateChanged: (Int) -> Unit, + private val onCallStateChanged: (Int) -> Unit +) { + private lateinit var core: Core + private var isRegistered = false + private var currentCall: Call? = null + + private var registrationState: RegistrationState by Delegates.observable(RegistrationState.None) { _, oldValue, newValue -> + Log.i( + LiblinphoneFlutterPlugin.TAG, + "registrationState delegate: oldValue: $oldValue, newValue: $newValue" + ) + onRegistrationStateChanged(newValue.ordinal) + } + private var callState: Call.State by Delegates.observable(Call.State.Idle) { _, oldValue, newValue -> + Log.i( + LiblinphoneFlutterPlugin.TAG, + "callState delegate: oldValue: $oldValue, newValue: $newValue" + ) + onCallStateChanged(newValue.ordinal) + } + + private val coreListener = object : CoreListenerStub() { +// override fun onRegistrationStateChanged( +// core: Core, +// proxyConfig: ProxyConfig, +// state: RegistrationState, +// message: String +// ) { +// registrationState = state +// when (state) { +// RegistrationState.Ok -> { +// isRegistered = true +// } +// +// RegistrationState.Failed -> { +// isRegistered = false +// } +// +// RegistrationState.None -> { +// isRegistered = false +// } +// +// RegistrationState.Progress -> { +// isRegistered = false +// } +// +// else -> {} +// } +// } + + override fun onAccountRegistrationStateChanged( + core: Core, + account: Account, + state: RegistrationState?, + message: String + ) { + registrationState = state ?: RegistrationState.None + when (state) { + RegistrationState.Ok -> { + isRegistered = true + } + + RegistrationState.Failed -> { + isRegistered = false + } + + RegistrationState.None -> { + isRegistered = false + } + + RegistrationState.Progress -> { + isRegistered = false + } + + else -> {} + } + } + + override fun onCallStateChanged( + core: Core, + call: Call, + state: Call.State, + message: String + ) { + callState = state + when (state) { + Call.State.IncomingReceived -> { + currentCall = call + Log.i( + LiblinphoneFlutterPlugin.TAG, + "onCallStateChanged: INCOMING_RECEIVED $call" + ) + } + + Call.State.Connected -> { + Log.i(LiblinphoneFlutterPlugin.TAG, "onCallStateChanged: CONNECTED $call") + + remoteViewAcquisitor()?.let { + call.nativeVideoWindowId = it + } ?: run { + Log.i( + LiblinphoneFlutterPlugin.TAG, + "onCallStateChanged: CONNECTED remoteViewAcquisitor found nothing" + ) + } + localViewAcquisitor()?.let { + core.nativePreviewWindowId = it + } ?: run { + Log.i( + LiblinphoneFlutterPlugin.TAG, + "onCallStateChanged: CONNECTED localViewAcquisitor found nothing" + ) + } + } + + Call.State.StreamsRunning -> { + Log.i(LiblinphoneFlutterPlugin.TAG, "onCallStateChanged: STREAMS_RUNNING $call") + remoteViewAcquisitor()?.let { + call.nativeVideoWindowId = it + } ?: run { + Log.i( + LiblinphoneFlutterPlugin.TAG, + "onCallStateChanged: STREAMS_RUNNING remoteViewAcquisitor found nothing" + ) + } + localViewAcquisitor()?.let { + core.nativePreviewWindowId = it + } ?: run { + Log.i( + LiblinphoneFlutterPlugin.TAG, + "onCallStateChanged: STREAMS_RUNNING localViewAcquisitor found nothing" + ) + } + } + + Call.State.End -> { + currentCall = null + } + + Call.State.Error -> { + currentCall = null + } + + Call.State.Released -> { + currentCall = null + } + + else -> {} + } + } + } + + private val neededPermissions = arrayOf( + Manifest.permission.RECORD_AUDIO, + Manifest.permission.CAMERA, + Manifest.permission.MODIFY_AUDIO_SETTINGS + ) + + fun checkPermissions(): Boolean { + val missingPermissions = neededPermissions.filter { permission -> + ContextCompat.checkSelfPermission( + activity, + permission + ) != PackageManager.PERMISSION_GRANTED + }.toTypedArray() + if (missingPermissions.isNotEmpty()) { + activity.requestPermissions(missingPermissions, 1) + return false + } + + return true + } + + fun initializeLinphone() { + val factory = Factory.instance() + core = factory.createCore(null, null, activity.baseContext) + core.addListener(coreListener) + + // Enable video + core.isVideoCaptureEnabled = true + core.isVideoDisplayEnabled = true + core.videoActivationPolicy.automaticallyInitiate = true + core.videoActivationPolicy.automaticallyAccept = true + + // https://www.cisco.com/c/en/us/td/docs/switches/datacenter/nexus1000/sw/4_0/qos/configuration/guide/nexus1000v_qos/qos_6dscp_val.pdf + core.sipDscp = 26 // AF31 + core.audioDscp = 46 // EF + core.videoDscp = 36 // AF42 + + core.isVideoAdaptiveJittcompEnabled = true +// core.isEchoCancellationEnabled = true // eh results, bad for audio + + // good enough default + core.uploadBandwidth = 512 + core.downloadBandwidth = 1500 + + val preferredAudio = listOf("opus", "pcmu", "pcma") // in order of preference + val preferredVideo = listOf("h264", "vp8") + + core.audioPayloadTypes.forEach { pt: PayloadType -> + val mime = pt.mimeType.lowercase() + val enable = preferredAudio.contains(mime) + pt.enable(enable) + } + core.videoPayloadTypes.forEach { pt: PayloadType -> + val mime = pt.mimeType.lowercase() + val enable = preferredVideo.contains(mime) + pt.enable(enable) + } + + core.getPayloadType("opus", -1, 0)?.let { it.normalBitrate = 32 } // 32 kbps for Opus + core.getPayloadType("h264", -1, 0)?.let { it.normalBitrate = 600 } // 600 kbps for H264 + core.getPayloadType("vp8", -1, 0)?.let { it.normalBitrate = 600 } // 600 kbps for VP8 + + val preferredVidDef = factory.createVideoDefinition(720, 1280) + core.preferredVideoDefinition = preferredVidDef + core.preferredFramerate = 30f + + core.start() + } + + fun register(username: String, password: String, serverIp: String, serverPort: Int) { + val factory = Factory.instance() + val authInfo = factory.createAuthInfo(username, null, password, null, null, serverIp) + core.addAuthInfo(authInfo) + + val identity = factory.createAddress("sip:$username@$serverIp") + val server = factory.createAddress("sip:$serverIp:$serverPort") + + val accountParams = core.createAccountParams().let { + it.identityAddress = identity + it.serverAddress = server + it.isRegisterEnabled = true + it + } + + val account = core.createAccount(accountParams) + core.addAccount(account) + core.defaultAccount = account + } + + fun unregister() { + core.clearAccounts() + core.clearAllAuthInfo() + } + + fun makeCall(callTo: String, isVideoEnabled: Boolean) { + if (!isRegistered) { + throw IllegalStateException("Not registered") + } + + val factory = Factory.instance() + val remoteAddress = factory.createAddress(callTo) + val callParams = core.createCallParams(null) + if (remoteAddress == null) { + throw IllegalStateException("Failed to create remote address") + } + if (callParams == null) { + throw IllegalStateException("Failed to create call params") + } + + callParams.let { + it.isVideoEnabled = isVideoEnabled + it.videoDirection = + if (isVideoEnabled) MediaDirection.SendRecv else MediaDirection.Inactive + } + + currentCall = core.inviteAddressWithParams(remoteAddress, callParams) + } + + fun answerCall(): Boolean { + val res = currentCall?.let { call -> + val callParams = core.createCallParams(call) + callParams?.let { + it.isVideoEnabled = true + it.videoDirection = MediaDirection.SendRecv + } + call.acceptWithParams(callParams) + + return@let true + } + + return res ?: false + } + + fun hangupCall(): Boolean { + return currentCall?.terminate() == 0 + } + + fun toggleVideo(): Boolean { + currentCall?.let { call -> + val params = core.createCallParams(call) + params?.isVideoEnabled = !call.currentParams.isVideoEnabled + call.update(params) + } + + return currentCall?.currentParams?.isVideoEnabled == true + } + + fun toggleMicrophone(): Boolean { + core.isMicEnabled = !core.isMicEnabled + return core.isMicEnabled + } + + fun stop() { + core.stop() + } + + fun inCall(): Boolean { + return currentCall != null + } + + enum class CallType { + Audio, + Video, + Unknown + } + + fun callType(): CallType { + if (currentCall?.currentParams != null) { + if (currentCall?.currentParams?.isVideoEnabled == true) { + return CallType.Video + } else { + return CallType.Audio + } + } + if (currentCall?.currentParams?.isVideoEnabled == true) { + return CallType.Video + } + return CallType.Unknown + } +} \ No newline at end of file diff --git a/android/src/main/kotlin/xyz/nuark/liblinphone_flutter/views/LocalView.kt b/android/src/main/kotlin/xyz/nuark/liblinphone_flutter/views/LocalView.kt new file mode 100644 index 0000000..ae97761 --- /dev/null +++ b/android/src/main/kotlin/xyz/nuark/liblinphone_flutter/views/LocalView.kt @@ -0,0 +1,25 @@ +package xyz.nuark.liblinphone_flutter.views + +import android.content.Context +import android.graphics.Color +import android.view.TextureView +import android.view.View +import io.flutter.plugin.platform.PlatformView +import org.linphone.mediastream.video.capture.CaptureTextureView + +internal class LocalView(context: Context, id: Int, creationParams: Map?) : PlatformView { + private val textureView: CaptureTextureView + + override fun getView(): View { + return textureView + } + + override fun dispose() { + textureView.surfaceTexture?.release() + } + + init { + textureView = CaptureTextureView(context) + textureView.id = id + } +} \ No newline at end of file diff --git a/android/src/main/kotlin/xyz/nuark/liblinphone_flutter/views/LocalViewFactory.kt b/android/src/main/kotlin/xyz/nuark/liblinphone_flutter/views/LocalViewFactory.kt new file mode 100644 index 0000000..8a4e27c --- /dev/null +++ b/android/src/main/kotlin/xyz/nuark/liblinphone_flutter/views/LocalViewFactory.kt @@ -0,0 +1,16 @@ +package xyz.nuark.liblinphone_flutter.views + +import android.content.Context +import android.view.View +import io.flutter.plugin.common.StandardMessageCodec +import io.flutter.plugin.platform.PlatformView +import io.flutter.plugin.platform.PlatformViewFactory + +class LocalViewFactory(internal val cacher: (View) -> Unit) : PlatformViewFactory(StandardMessageCodec.INSTANCE) { + override fun create(context: Context, viewId: Int, args: Any?): PlatformView { + val creationParams = args as Map? + return LocalView(context, viewId, creationParams).also { + cacher(it.view) + } + } +} \ No newline at end of file diff --git a/android/src/main/kotlin/xyz/nuark/liblinphone_flutter/views/RemoteView.kt b/android/src/main/kotlin/xyz/nuark/liblinphone_flutter/views/RemoteView.kt new file mode 100644 index 0000000..1e18e65 --- /dev/null +++ b/android/src/main/kotlin/xyz/nuark/liblinphone_flutter/views/RemoteView.kt @@ -0,0 +1,24 @@ +package xyz.nuark.liblinphone_flutter.views + +import android.content.Context +import android.graphics.Color +import android.view.TextureView +import android.view.View +import io.flutter.plugin.platform.PlatformView + +internal class RemoteView(context: Context, id: Int, creationParams: Map?) : PlatformView { + private val textureView: TextureView + + override fun getView(): View { + return textureView + } + + override fun dispose() { + textureView.surfaceTexture?.release() + } + + init { + textureView = TextureView(context) + textureView.id = id + } +} \ No newline at end of file diff --git a/android/src/main/kotlin/xyz/nuark/liblinphone_flutter/views/RemoteViewFactory.kt b/android/src/main/kotlin/xyz/nuark/liblinphone_flutter/views/RemoteViewFactory.kt new file mode 100644 index 0000000..1aef9da --- /dev/null +++ b/android/src/main/kotlin/xyz/nuark/liblinphone_flutter/views/RemoteViewFactory.kt @@ -0,0 +1,16 @@ +package xyz.nuark.liblinphone_flutter.views + +import android.content.Context +import android.view.View +import io.flutter.plugin.common.StandardMessageCodec +import io.flutter.plugin.platform.PlatformView +import io.flutter.plugin.platform.PlatformViewFactory + +class RemoteViewFactory(internal val cacher: (View) -> Unit) : PlatformViewFactory(StandardMessageCodec.INSTANCE) { + override fun create(context: Context, viewId: Int, args: Any?): PlatformView { + val creationParams = args as Map? + return RemoteView(context, viewId, creationParams).also { + cacher(it.view) + } + } +} \ No newline at end of file diff --git a/android/src/test/kotlin/xyz/nuark/liblinphone_flutter/LiblinphoneFlutterPluginTest.kt b/android/src/test/kotlin/xyz/nuark/liblinphone_flutter/LiblinphoneFlutterPluginTest.kt new file mode 100644 index 0000000..dee7857 --- /dev/null +++ b/android/src/test/kotlin/xyz/nuark/liblinphone_flutter/LiblinphoneFlutterPluginTest.kt @@ -0,0 +1,27 @@ +package com.example.liblinphone_flutter + +import io.flutter.plugin.common.MethodCall +import io.flutter.plugin.common.MethodChannel +import org.mockito.Mockito +import kotlin.test.Test + +/* + * This demonstrates a simple unit test of the Kotlin portion of this plugin's implementation. + * + * Once you have built the plugin's example app, you can run these tests from the command + * line by running `./gradlew testDebugUnitTest` in the `example/android/` directory, or + * you can run them directly from IDEs that support JUnit such as Android Studio. + */ + +internal class LiblinphoneFlutterPluginTest { + @Test + fun onMethodCall_getPlatformVersion_returnsExpectedValue() { + val plugin = LiblinphoneFlutterPlugin() + + val call = MethodCall("getPlatformVersion", null) + val mockResult: MethodChannel.Result = Mockito.mock(MethodChannel.Result::class.java) + plugin.onMethodCall(call, mockResult) + + Mockito.verify(mockResult).success("Android " + android.os.Build.VERSION.RELEASE) + } +} diff --git a/example/.gitignore b/example/.gitignore new file mode 100644 index 0000000..3820a95 --- /dev/null +++ b/example/.gitignore @@ -0,0 +1,45 @@ +# Miscellaneous +*.class +*.log +*.pyc +*.swp +.DS_Store +.atom/ +.build/ +.buildlog/ +.history +.svn/ +.swiftpm/ +migrate_working_dir/ + +# IntelliJ related +*.iml +*.ipr +*.iws +.idea/ + +# The .vscode folder contains launch configuration and tasks you configure in +# VS Code which you may wish to be included in version control, so this line +# is commented out by default. +#.vscode/ + +# Flutter/Dart/Pub related +**/doc/api/ +**/ios/Flutter/.last_build_id +.dart_tool/ +.flutter-plugins-dependencies +.pub-cache/ +.pub/ +/build/ +/coverage/ + +# Symbolication related +app.*.symbols + +# Obfuscation related +app.*.map.json + +# Android Studio will place build artifacts here +/android/app/debug +/android/app/profile +/android/app/release diff --git a/example/README.md b/example/README.md new file mode 100644 index 0000000..cf4d2ba --- /dev/null +++ b/example/README.md @@ -0,0 +1,16 @@ +# liblinphone_flutter_example + +Demonstrates how to use the liblinphone_flutter plugin. + +## Getting Started + +This project is a starting point for a Flutter application. + +A few resources to get you started if this is your first Flutter project: + +- [Lab: Write your first Flutter app](https://docs.flutter.dev/get-started/codelab) +- [Cookbook: Useful Flutter samples](https://docs.flutter.dev/cookbook) + +For help getting started with Flutter development, view the +[online documentation](https://docs.flutter.dev/), which offers tutorials, +samples, guidance on mobile development, and a full API reference. diff --git a/example/analysis_options.yaml b/example/analysis_options.yaml new file mode 100644 index 0000000..0d29021 --- /dev/null +++ b/example/analysis_options.yaml @@ -0,0 +1,28 @@ +# This file configures the analyzer, which statically analyzes Dart code to +# check for errors, warnings, and lints. +# +# The issues identified by the analyzer are surfaced in the UI of Dart-enabled +# IDEs (https://dart.dev/tools#ides-and-editors). The analyzer can also be +# invoked from the command line by running `flutter analyze`. + +# The following line activates a set of recommended lints for Flutter apps, +# packages, and plugins designed to encourage good coding practices. +include: package:flutter_lints/flutter.yaml + +linter: + # The lint rules applied to this project can be customized in the + # section below to disable rules from the `package:flutter_lints/flutter.yaml` + # included above or to enable additional rules. A list of all available lints + # and their documentation is published at https://dart.dev/lints. + # + # Instead of disabling a lint rule for the entire project in the + # section below, it can also be suppressed for a single line of code + # or a specific dart file by using the `// ignore: name_of_lint` and + # `// ignore_for_file: name_of_lint` syntax on the line or in the file + # producing the lint. + rules: + # avoid_print: false # Uncomment to disable the `avoid_print` rule + # prefer_single_quotes: true # Uncomment to enable the `prefer_single_quotes` rule + +# Additional information about this file can be found at +# https://dart.dev/guides/language/analysis-options diff --git a/example/android/.gitignore b/example/android/.gitignore new file mode 100644 index 0000000..be3943c --- /dev/null +++ b/example/android/.gitignore @@ -0,0 +1,14 @@ +gradle-wrapper.jar +/.gradle +/captures/ +/gradlew +/gradlew.bat +/local.properties +GeneratedPluginRegistrant.java +.cxx/ + +# Remember to never publicly share your keystore. +# See https://flutter.dev/to/reference-keystore +key.properties +**/*.keystore +**/*.jks diff --git a/example/android/app/build.gradle.kts b/example/android/app/build.gradle.kts new file mode 100644 index 0000000..edab54c --- /dev/null +++ b/example/android/app/build.gradle.kts @@ -0,0 +1,44 @@ +plugins { + id("com.android.application") + id("kotlin-android") + // The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins. + id("dev.flutter.flutter-gradle-plugin") +} + +android { + namespace = "com.example.liblinphone_flutter_example" + compileSdk = flutter.compileSdkVersion + ndkVersion = flutter.ndkVersion + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } + + kotlinOptions { + jvmTarget = JavaVersion.VERSION_11.toString() + } + + defaultConfig { + // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). + applicationId = "com.example.liblinphone_flutter_example" + // You can update the following values to match your application needs. + // For more information, see: https://flutter.dev/to/review-gradle-config. + minSdk = flutter.minSdkVersion + targetSdk = flutter.targetSdkVersion + versionCode = flutter.versionCode + versionName = flutter.versionName + } + + buildTypes { + release { + // TODO: Add your own signing config for the release build. + // Signing with the debug keys for now, so `flutter run --release` works. + signingConfig = signingConfigs.getByName("debug") + } + } +} + +flutter { + source = "../.." +} diff --git a/example/android/app/src/debug/AndroidManifest.xml b/example/android/app/src/debug/AndroidManifest.xml new file mode 100644 index 0000000..399f698 --- /dev/null +++ b/example/android/app/src/debug/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/example/android/app/src/main/AndroidManifest.xml b/example/android/app/src/main/AndroidManifest.xml new file mode 100644 index 0000000..8e679bd --- /dev/null +++ b/example/android/app/src/main/AndroidManifest.xml @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/example/android/app/src/main/kotlin/com/example/liblinphone_flutter_example/MainActivity.kt b/example/android/app/src/main/kotlin/com/example/liblinphone_flutter_example/MainActivity.kt new file mode 100644 index 0000000..f20c5f2 --- /dev/null +++ b/example/android/app/src/main/kotlin/com/example/liblinphone_flutter_example/MainActivity.kt @@ -0,0 +1,5 @@ +package com.example.liblinphone_flutter_example + +import io.flutter.embedding.android.FlutterActivity + +class MainActivity : FlutterActivity() diff --git a/example/android/app/src/main/res/drawable-v21/launch_background.xml b/example/android/app/src/main/res/drawable-v21/launch_background.xml new file mode 100644 index 0000000..f74085f --- /dev/null +++ b/example/android/app/src/main/res/drawable-v21/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/example/android/app/src/main/res/drawable/launch_background.xml b/example/android/app/src/main/res/drawable/launch_background.xml new file mode 100644 index 0000000..304732f --- /dev/null +++ b/example/android/app/src/main/res/drawable/launch_background.xml @@ -0,0 +1,12 @@ + + + + + + + + diff --git a/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png b/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png new file mode 100644 index 0000000..db77bb4 Binary files /dev/null and b/example/android/app/src/main/res/mipmap-hdpi/ic_launcher.png differ diff --git a/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png b/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png new file mode 100644 index 0000000..17987b7 Binary files /dev/null and b/example/android/app/src/main/res/mipmap-mdpi/ic_launcher.png differ diff --git a/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png b/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png new file mode 100644 index 0000000..09d4391 Binary files /dev/null and b/example/android/app/src/main/res/mipmap-xhdpi/ic_launcher.png differ diff --git a/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png b/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png new file mode 100644 index 0000000..d5f1c8d Binary files /dev/null and b/example/android/app/src/main/res/mipmap-xxhdpi/ic_launcher.png differ diff --git a/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png b/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png new file mode 100644 index 0000000..4d6372e Binary files /dev/null and b/example/android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png differ diff --git a/example/android/app/src/main/res/values-night/styles.xml b/example/android/app/src/main/res/values-night/styles.xml new file mode 100644 index 0000000..06952be --- /dev/null +++ b/example/android/app/src/main/res/values-night/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/example/android/app/src/main/res/values/styles.xml b/example/android/app/src/main/res/values/styles.xml new file mode 100644 index 0000000..cb1ef88 --- /dev/null +++ b/example/android/app/src/main/res/values/styles.xml @@ -0,0 +1,18 @@ + + + + + + + diff --git a/example/android/app/src/profile/AndroidManifest.xml b/example/android/app/src/profile/AndroidManifest.xml new file mode 100644 index 0000000..399f698 --- /dev/null +++ b/example/android/app/src/profile/AndroidManifest.xml @@ -0,0 +1,7 @@ + + + + diff --git a/example/android/build.gradle.kts b/example/android/build.gradle.kts new file mode 100644 index 0000000..dbee657 --- /dev/null +++ b/example/android/build.gradle.kts @@ -0,0 +1,24 @@ +allprojects { + repositories { + google() + mavenCentral() + } +} + +val newBuildDir: Directory = + rootProject.layout.buildDirectory + .dir("../../build") + .get() +rootProject.layout.buildDirectory.value(newBuildDir) + +subprojects { + val newSubprojectBuildDir: Directory = newBuildDir.dir(project.name) + project.layout.buildDirectory.value(newSubprojectBuildDir) +} +subprojects { + project.evaluationDependsOn(":app") +} + +tasks.register("clean") { + delete(rootProject.layout.buildDirectory) +} diff --git a/example/android/gradle.properties b/example/android/gradle.properties new file mode 100644 index 0000000..f018a61 --- /dev/null +++ b/example/android/gradle.properties @@ -0,0 +1,3 @@ +org.gradle.jvmargs=-Xmx8G -XX:MaxMetaspaceSize=4G -XX:ReservedCodeCacheSize=512m -XX:+HeapDumpOnOutOfMemoryError +android.useAndroidX=true +android.enableJetifier=true diff --git a/example/android/gradle/wrapper/gradle-wrapper.properties b/example/android/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..ac3b479 --- /dev/null +++ b/example/android/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-all.zip diff --git a/example/android/settings.gradle.kts b/example/android/settings.gradle.kts new file mode 100644 index 0000000..fb605bc --- /dev/null +++ b/example/android/settings.gradle.kts @@ -0,0 +1,26 @@ +pluginManagement { + val flutterSdkPath = + run { + val properties = java.util.Properties() + file("local.properties").inputStream().use { properties.load(it) } + val flutterSdkPath = properties.getProperty("flutter.sdk") + require(flutterSdkPath != null) { "flutter.sdk not set in local.properties" } + flutterSdkPath + } + + includeBuild("$flutterSdkPath/packages/flutter_tools/gradle") + + repositories { + google() + mavenCentral() + gradlePluginPortal() + } +} + +plugins { + id("dev.flutter.flutter-plugin-loader") version "1.0.0" + id("com.android.application") version "8.9.1" apply false + id("org.jetbrains.kotlin.android") version "2.1.0" apply false +} + +include(":app") diff --git a/example/integration_test/plugin_integration_test.dart b/example/integration_test/plugin_integration_test.dart new file mode 100644 index 0000000..b95bffd --- /dev/null +++ b/example/integration_test/plugin_integration_test.dart @@ -0,0 +1,25 @@ +// This is a basic Flutter integration test. +// +// Since integration tests run in a full Flutter application, they can interact +// with the host side of a plugin implementation, unlike Dart unit tests. +// +// For more information about Flutter integration tests, please see +// https://flutter.dev/to/integration-testing + + +import 'package:flutter_test/flutter_test.dart'; +import 'package:integration_test/integration_test.dart'; + +import 'package:liblinphone_flutter/liblinphone_flutter.dart'; + +void main() { + IntegrationTestWidgetsFlutterBinding.ensureInitialized(); + + testWidgets('getPlatformVersion test', (WidgetTester tester) async { + final LiblinphoneFlutter plugin = LiblinphoneFlutter(); + final String? version = await plugin.getPlatformVersion(); + // The version string depends on the host platform running the test, so + // just assert that some non-empty string is returned. + expect(version?.isNotEmpty, true); + }); +} diff --git a/example/lib/main.dart b/example/lib/main.dart new file mode 100644 index 0000000..ac2d700 --- /dev/null +++ b/example/lib/main.dart @@ -0,0 +1,63 @@ +import 'package:flutter/material.dart'; +import 'dart:async'; + +import 'package:flutter/services.dart'; +import 'package:liblinphone_flutter/liblinphone_flutter.dart'; + +void main() { + runApp(const MyApp()); +} + +class MyApp extends StatefulWidget { + const MyApp({super.key}); + + @override + State createState() => _MyAppState(); +} + +class _MyAppState extends State { + String _platformVersion = 'Unknown'; + final _liblinphoneFlutterPlugin = LiblinphoneFlutter(); + + @override + void initState() { + super.initState(); + initPlatformState(); + } + + // Platform messages are asynchronous, so we initialize in an async method. + Future initPlatformState() async { + String platformVersion; + // Platform messages may fail, so we use a try/catch PlatformException. + // We also handle the message potentially returning null. + try { + platformVersion = + await _liblinphoneFlutterPlugin.getPlatformVersion() ?? 'Unknown platform version'; + } on PlatformException { + platformVersion = 'Failed to get platform version.'; + } + + // If the widget was removed from the tree while the asynchronous platform + // message was in flight, we want to discard the reply rather than calling + // setState to update our non-existent appearance. + if (!mounted) return; + + setState(() { + _platformVersion = platformVersion; + }); + } + + @override + Widget build(BuildContext context) { + return MaterialApp( + home: Scaffold( + appBar: AppBar( + title: const Text('Plugin example app'), + ), + body: Center( + child: Text('Running on: $_platformVersion\n'), + ), + ), + ); + } +} diff --git a/example/pubspec.lock b/example/pubspec.lock new file mode 100644 index 0000000..525849f --- /dev/null +++ b/example/pubspec.lock @@ -0,0 +1,283 @@ +# Generated by pub +# See https://dart.dev/tools/pub/glossary#lockfile +packages: + async: + dependency: transitive + description: + name: async + sha256: "758e6d74e971c3e5aceb4110bfd6698efc7f501675bcfe0c775459a8140750eb" + url: "https://pub.dev" + source: hosted + version: "2.13.0" + boolean_selector: + dependency: transitive + description: + name: boolean_selector + sha256: "8aab1771e1243a5063b8b0ff68042d67334e3feab9e95b9490f9a6ebf73b42ea" + url: "https://pub.dev" + source: hosted + version: "2.1.2" + characters: + dependency: transitive + description: + name: characters + sha256: f71061c654a3380576a52b451dd5532377954cf9dbd272a78fc8479606670803 + url: "https://pub.dev" + source: hosted + version: "1.4.0" + clock: + dependency: transitive + description: + name: clock + sha256: fddb70d9b5277016c77a80201021d40a2247104d9f4aa7bab7157b7e3f05b84b + url: "https://pub.dev" + source: hosted + version: "1.1.2" + collection: + dependency: transitive + description: + name: collection + sha256: "2f5709ae4d3d59dd8f7cd309b4e023046b57d8a6c82130785d2b0e5868084e76" + url: "https://pub.dev" + source: hosted + version: "1.19.1" + cupertino_icons: + dependency: "direct main" + description: + name: cupertino_icons + sha256: ba631d1c7f7bef6b729a622b7b752645a2d076dba9976925b8f25725a30e1ee6 + url: "https://pub.dev" + source: hosted + version: "1.0.8" + fake_async: + dependency: transitive + description: + name: fake_async + sha256: "5368f224a74523e8d2e7399ea1638b37aecfca824a3cc4dfdf77bf1fa905ac44" + url: "https://pub.dev" + source: hosted + version: "1.3.3" + file: + dependency: transitive + description: + name: file + sha256: a3b4f84adafef897088c160faf7dfffb7696046cb13ae90b508c2cbc95d3b8d4 + url: "https://pub.dev" + source: hosted + version: "7.0.1" + flutter: + dependency: "direct main" + description: flutter + source: sdk + version: "0.0.0" + flutter_driver: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + flutter_lints: + dependency: "direct dev" + description: + name: flutter_lints + sha256: "5398f14efa795ffb7a33e9b6a08798b26a180edac4ad7db3f231e40f82ce11e1" + url: "https://pub.dev" + source: hosted + version: "5.0.0" + flutter_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + fuchsia_remote_debug_protocol: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + integration_test: + dependency: "direct dev" + description: flutter + source: sdk + version: "0.0.0" + leak_tracker: + dependency: transitive + description: + name: leak_tracker + sha256: "8dcda04c3fc16c14f48a7bb586d4be1f0d1572731b6d81d51772ef47c02081e0" + url: "https://pub.dev" + source: hosted + version: "11.0.1" + leak_tracker_flutter_testing: + dependency: transitive + description: + name: leak_tracker_flutter_testing + sha256: "1dbc140bb5a23c75ea9c4811222756104fbcd1a27173f0c34ca01e16bea473c1" + url: "https://pub.dev" + source: hosted + version: "3.0.10" + leak_tracker_testing: + dependency: transitive + description: + name: leak_tracker_testing + sha256: "8d5a2d49f4a66b49744b23b018848400d23e54caf9463f4eb20df3eb8acb2eb1" + url: "https://pub.dev" + source: hosted + version: "3.0.2" + liblinphone_flutter: + dependency: "direct main" + description: + path: ".." + relative: true + source: path + version: "0.0.1" + lints: + dependency: transitive + description: + name: lints + sha256: c35bb79562d980e9a453fc715854e1ed39e24e7d0297a880ef54e17f9874a9d7 + url: "https://pub.dev" + source: hosted + version: "5.1.1" + matcher: + dependency: transitive + description: + name: matcher + sha256: dc58c723c3c24bf8d3e2d3ad3f2f9d7bd9cf43ec6feaa64181775e60190153f2 + url: "https://pub.dev" + source: hosted + version: "0.12.17" + material_color_utilities: + dependency: transitive + description: + name: material_color_utilities + sha256: f7142bb1154231d7ea5f96bc7bde4bda2a0945d2806bb11670e30b850d56bdec + url: "https://pub.dev" + source: hosted + version: "0.11.1" + meta: + dependency: transitive + description: + name: meta + sha256: e3641ec5d63ebf0d9b41bd43201a66e3fc79a65db5f61fc181f04cd27aab950c + url: "https://pub.dev" + source: hosted + version: "1.16.0" + path: + dependency: transitive + description: + name: path + sha256: "75cca69d1490965be98c73ceaea117e8a04dd21217b37b292c9ddbec0d955bc5" + url: "https://pub.dev" + source: hosted + version: "1.9.1" + platform: + dependency: transitive + description: + name: platform + sha256: "5d6b1b0036a5f331ebc77c850ebc8506cbc1e9416c27e59b439f917a902a4984" + url: "https://pub.dev" + source: hosted + version: "3.1.6" + plugin_platform_interface: + dependency: transitive + description: + name: plugin_platform_interface + sha256: "4820fbfdb9478b1ebae27888254d445073732dae3d6ea81f0b7e06d5dedc3f02" + url: "https://pub.dev" + source: hosted + version: "2.1.8" + process: + dependency: transitive + description: + name: process + sha256: c6248e4526673988586e8c00bb22a49210c258dc91df5227d5da9748ecf79744 + url: "https://pub.dev" + source: hosted + version: "5.0.5" + sky_engine: + dependency: transitive + description: flutter + source: sdk + version: "0.0.0" + source_span: + dependency: transitive + description: + name: source_span + sha256: "254ee5351d6cb365c859e20ee823c3bb479bf4a293c22d17a9f1bf144ce86f7c" + url: "https://pub.dev" + source: hosted + version: "1.10.1" + stack_trace: + dependency: transitive + description: + name: stack_trace + sha256: "8b27215b45d22309b5cddda1aa2b19bdfec9df0e765f2de506401c071d38d1b1" + url: "https://pub.dev" + source: hosted + version: "1.12.1" + stream_channel: + dependency: transitive + description: + name: stream_channel + sha256: "969e04c80b8bcdf826f8f16579c7b14d780458bd97f56d107d3950fdbeef059d" + url: "https://pub.dev" + source: hosted + version: "2.1.4" + string_scanner: + dependency: transitive + description: + name: string_scanner + sha256: "921cd31725b72fe181906c6a94d987c78e3b98c2e205b397ea399d4054872b43" + url: "https://pub.dev" + source: hosted + version: "1.4.1" + sync_http: + dependency: transitive + description: + name: sync_http + sha256: "7f0cd72eca000d2e026bcd6f990b81d0ca06022ef4e32fb257b30d3d1014a961" + url: "https://pub.dev" + source: hosted + version: "0.3.1" + term_glyph: + dependency: transitive + description: + name: term_glyph + sha256: "7f554798625ea768a7518313e58f83891c7f5024f88e46e7182a4558850a4b8e" + url: "https://pub.dev" + source: hosted + version: "1.2.2" + test_api: + dependency: transitive + description: + name: test_api + sha256: "522f00f556e73044315fa4585ec3270f1808a4b186c936e612cab0b565ff1e00" + url: "https://pub.dev" + source: hosted + version: "0.7.6" + vector_math: + dependency: transitive + description: + name: vector_math + sha256: d530bd74fea330e6e364cda7a85019c434070188383e1cd8d9777ee586914c5b + url: "https://pub.dev" + source: hosted + version: "2.2.0" + vm_service: + dependency: transitive + description: + name: vm_service + sha256: "45caa6c5917fa127b5dbcfbd1fa60b14e583afdc08bfc96dda38886ca252eb60" + url: "https://pub.dev" + source: hosted + version: "15.0.2" + webdriver: + dependency: transitive + description: + name: webdriver + sha256: "2f3a14ca026957870cfd9c635b83507e0e51d8091568e90129fbf805aba7cade" + url: "https://pub.dev" + source: hosted + version: "3.1.0" +sdks: + dart: ">=3.9.0-333.2.beta <4.0.0" + flutter: ">=3.18.0-18.0.pre.54" diff --git a/example/pubspec.yaml b/example/pubspec.yaml new file mode 100644 index 0000000..e560fe1 --- /dev/null +++ b/example/pubspec.yaml @@ -0,0 +1,85 @@ +name: liblinphone_flutter_example +description: "Demonstrates how to use the liblinphone_flutter plugin." +# The following line prevents the package from being accidentally published to +# pub.dev using `flutter pub publish`. This is preferred for private packages. +publish_to: 'none' # Remove this line if you wish to publish to pub.dev + +environment: + sdk: ^3.9.0-333.2.beta + +# Dependencies specify other packages that your package needs in order to work. +# To automatically upgrade your package dependencies to the latest versions +# consider running `flutter pub upgrade --major-versions`. Alternatively, +# dependencies can be manually updated by changing the version numbers below to +# the latest version available on pub.dev. To see which dependencies have newer +# versions available, run `flutter pub outdated`. +dependencies: + flutter: + sdk: flutter + + liblinphone_flutter: + # When depending on this package from a real application you should use: + # liblinphone_flutter: ^x.y.z + # See https://dart.dev/tools/pub/dependencies#version-constraints + # The example app is bundled with the plugin so we use a path dependency on + # the parent directory to use the current plugin's version. + path: ../ + + # The following adds the Cupertino Icons font to your application. + # Use with the CupertinoIcons class for iOS style icons. + cupertino_icons: ^1.0.8 + +dev_dependencies: + integration_test: + sdk: flutter + flutter_test: + sdk: flutter + + # The "flutter_lints" package below contains a set of recommended lints to + # encourage good coding practices. The lint set provided by the package is + # activated in the `analysis_options.yaml` file located at the root of your + # package. See that file for information about deactivating specific lint + # rules and activating additional ones. + flutter_lints: ^5.0.0 + +# For information on the generic Dart part of this file, see the +# following page: https://dart.dev/tools/pub/pubspec + +# The following section is specific to Flutter packages. +flutter: + + # The following line ensures that the Material Icons font is + # included with your application, so that you can use the icons in + # the material Icons class. + uses-material-design: true + + # To add assets to your application, add an assets section, like this: + # assets: + # - images/a_dot_burr.jpeg + # - images/a_dot_ham.jpeg + + # An image asset can refer to one or more resolution-specific "variants", see + # https://flutter.dev/to/resolution-aware-images + + # For details regarding adding assets from package dependencies, see + # https://flutter.dev/to/asset-from-package + + # To add custom fonts to your application, add a fonts section here, + # in this "flutter" section. Each entry in this list should have a + # "family" key with the font family name, and a "fonts" key with a + # list giving the asset and other descriptors for the font. For + # example: + # fonts: + # - family: Schyler + # fonts: + # - asset: fonts/Schyler-Regular.ttf + # - asset: fonts/Schyler-Italic.ttf + # style: italic + # - family: Trajan Pro + # fonts: + # - asset: fonts/TrajanPro.ttf + # - asset: fonts/TrajanPro_Bold.ttf + # weight: 700 + # + # For details regarding fonts from package dependencies, + # see https://flutter.dev/to/font-from-package diff --git a/example/test/widget_test.dart b/example/test/widget_test.dart new file mode 100644 index 0000000..70e7342 --- /dev/null +++ b/example/test/widget_test.dart @@ -0,0 +1,27 @@ +// This is a basic Flutter widget test. +// +// To perform an interaction with a widget in your test, use the WidgetTester +// utility in the flutter_test package. For example, you can send tap and scroll +// gestures. You can also use WidgetTester to find child widgets in the widget +// tree, read text, and verify that the values of widget properties are correct. + +import 'package:flutter/material.dart'; +import 'package:flutter_test/flutter_test.dart'; + +import 'package:liblinphone_flutter_example/main.dart'; + +void main() { + testWidgets('Verify Platform version', (WidgetTester tester) async { + // Build our app and trigger a frame. + await tester.pumpWidget(const MyApp()); + + // Verify that platform version is retrieved. + expect( + find.byWidgetPredicate( + (Widget widget) => widget is Text && + widget.data!.startsWith('Running on:'), + ), + findsOneWidget, + ); + }); +} diff --git a/lib/liblinphone_flutter.dart b/lib/liblinphone_flutter.dart new file mode 100644 index 0000000..43ff883 --- /dev/null +++ b/lib/liblinphone_flutter.dart @@ -0,0 +1,70 @@ +import 'package:flutter/services.dart' show EventChannel; + +import 'liblinphone_flutter_platform_interface.dart'; +import 'models/call_type.dart'; +import 'models/registration_state.dart'; +import 'models/call_state.dart'; + +class LiblinphoneFlutter { + final _registrationEventsStream = EventChannel( + 'liblinphone_flutter.nuark.xyz/registration_events', + ); + + final _callEventsStream = EventChannel( + 'liblinphone_flutter.nuark.xyz/call_events', + ); + + Stream get registrationEvents => + _registrationEventsStream.receiveBroadcastStream().map((dynamic event) { + return RegistrationState.fromOrdinal(event); + }); + + Stream get callEvents => + _callEventsStream.receiveBroadcastStream().map((dynamic event) { + print("call event: $event"); + return CallState.fromOrdinal(event); + }); + + Future checkPermissions() async => + LiblinphoneFlutterPlatform.instance.checkPermissions(); + + Future initialize() async => + LiblinphoneFlutterPlatform.instance.initialize(); + + Future register( + String username, + String password, + String serverIp, + int serverPort, + ) async => LiblinphoneFlutterPlatform.instance.register( + username, + password, + serverIp, + serverPort, + ); + + Future unregister() async => + LiblinphoneFlutterPlatform.instance.unregister(); + + Future makeCall(String callTo, bool isVideoEnabled) async => + LiblinphoneFlutterPlatform.instance.makeCall(callTo, isVideoEnabled); + + Future answerCall() async => + LiblinphoneFlutterPlatform.instance.answerCall(); + + Future hangupCall() async => + LiblinphoneFlutterPlatform.instance.hangupCall(); + + Future inCall() async => LiblinphoneFlutterPlatform.instance.inCall(); + + Future callType() async => + LiblinphoneFlutterPlatform.instance.callType(); + + Future toggleVideo() async => + LiblinphoneFlutterPlatform.instance.toggleVideo(); + + Future toggleMicrophone() async => + LiblinphoneFlutterPlatform.instance.toggleMicrophone(); + + Future stop() async => LiblinphoneFlutterPlatform.instance.stop(); +} diff --git a/lib/liblinphone_flutter_method_channel.dart b/lib/liblinphone_flutter_method_channel.dart new file mode 100644 index 0000000..42ab345 --- /dev/null +++ b/lib/liblinphone_flutter_method_channel.dart @@ -0,0 +1,87 @@ +import 'package:flutter/foundation.dart'; +import 'package:flutter/services.dart'; + +import 'liblinphone_flutter_platform_interface.dart'; +import 'models/call_type.dart'; + +/// An implementation of [LiblinphoneFlutterPlatform] that uses method channels. +class MethodChannelLiblinphoneFlutter extends LiblinphoneFlutterPlatform { + /// The method channel used to interact with the native platform. + @visibleForTesting + final methodChannel = const MethodChannel('liblinphone_flutter'); + + @override + Future checkPermissions() async { + return (await methodChannel.invokeMethod('checkPermissions'))!; + } + + @override + Future initialize() async { + return (await methodChannel.invokeMethod('initialize'))!; + } + + @override + Future register( + String username, + String password, + String serverIp, + int serverPort, + ) async { + return (await methodChannel + .invokeMethod('register', { + 'username': username, + 'password': password, + 'serverIp': serverIp, + 'serverPort': serverPort, + }))!; + } + + @override + Future unregister() async { + return (await methodChannel.invokeMethod('unregister'))!; + } + + @override + Future makeCall(String callTo, bool isVideoEnabled) async { + return (await methodChannel.invokeMethod( + 'makeCall', + {'callTo': callTo, 'isVideoEnabled': isVideoEnabled}, + ))!; + } + + @override + Future answerCall() async { + return (await methodChannel.invokeMethod('answerCall'))!; + } + + @override + Future hangupCall() async { + return (await methodChannel.invokeMethod('hangupCall'))!; + } + + @override + Future inCall() async { + return (await methodChannel.invokeMethod('inCall'))!; + } + + @override + Future callType() async { + final callTypeOrdinal = await methodChannel.invokeMethod('callType'); + return CallType.fromOrdinal(callTypeOrdinal!); + } + + @override + Future toggleVideo() async { + return (await methodChannel.invokeMethod('toggleVideo'))!; + } + + @override + Future toggleMicrophone() async { + return (await methodChannel.invokeMethod('toggleMicrophone'))!; + } + + @override + Future stop() async { + return (await methodChannel.invokeMethod('stop'))!; + } +} diff --git a/lib/liblinphone_flutter_platform_interface.dart b/lib/liblinphone_flutter_platform_interface.dart new file mode 100644 index 0000000..5475359 --- /dev/null +++ b/lib/liblinphone_flutter_platform_interface.dart @@ -0,0 +1,80 @@ +import 'package:liblinphone_flutter/models/call_type.dart'; +import 'package:plugin_platform_interface/plugin_platform_interface.dart'; + +import 'liblinphone_flutter_method_channel.dart'; + +abstract class LiblinphoneFlutterPlatform extends PlatformInterface { + /// Constructs a LiblinphoneFlutterPlatform. + LiblinphoneFlutterPlatform() : super(token: _token); + + static final Object _token = Object(); + + static LiblinphoneFlutterPlatform _instance = + MethodChannelLiblinphoneFlutter(); + + /// The default instance of [LiblinphoneFlutterPlatform] to use. + /// + /// Defaults to [MethodChannelLiblinphoneFlutter]. + static LiblinphoneFlutterPlatform get instance => _instance; + + /// Platform-specific implementations should set this with their own + /// platform-specific class that extends [LiblinphoneFlutterPlatform] when + /// they register themselves. + static set instance(LiblinphoneFlutterPlatform instance) { + PlatformInterface.verifyToken(instance, _token); + _instance = instance; + } + + Future checkPermissions() { + throw UnimplementedError('checkPermissions() has not been implemented.'); + } + + Future initialize() { + throw UnimplementedError('initialize() has not been implemented.'); + } + + Future register( + String username, + String password, + String serverIp, + int serverPort, + ) { + throw UnimplementedError('register() has not been implemented.'); + } + + Future unregister() { + throw UnimplementedError('unregister() has not been implemented.'); + } + + Future makeCall(String callTo, bool isVideoEnabled) { + throw UnimplementedError('makeCall() has not been implemented.'); + } + + Future answerCall() { + throw UnimplementedError('answerCall() has not been implemented.'); + } + + Future hangupCall() { + throw UnimplementedError('hangupCall() has not been implemented.'); + } + + Future inCall() { + throw UnimplementedError('inCall() has not been implemented.'); + } + + Future callType() { + throw UnimplementedError('callType() has not been implemented.'); + } + + Future toggleVideo() { + throw UnimplementedError('toggleVideo() has not been implemented.'); + } + + Future toggleMicrophone() { + throw UnimplementedError('toggleMicrophone() has not been implemented.'); + } + + Future stop() { + throw UnimplementedError('stop() has not been implemented.'); + } +} diff --git a/lib/models/call_state.dart b/lib/models/call_state.dart new file mode 100644 index 0000000..758b639 --- /dev/null +++ b/lib/models/call_state.dart @@ -0,0 +1,28 @@ +enum CallState { + Idle, + IncomingReceived, + PushIncomingReceived, + OutgoingInit, + OutgoingProgress, + OutgoingRinging, + OutgoingEarlyMedia, + Connected, + StreamsRunning, + Pausing, + Paused, + Resuming, + Referred, + Error, + End, + PausedByRemote, + UpdatedByRemote, + IncomingEarlyMedia, + Updating, + Released, + EarlyUpdatedByRemote, + EarlyUpdating; + + static CallState fromOrdinal(int ordinal) { + return values[ordinal]; + } +} diff --git a/lib/models/call_type.dart b/lib/models/call_type.dart new file mode 100644 index 0000000..3acdea0 --- /dev/null +++ b/lib/models/call_type.dart @@ -0,0 +1,9 @@ +enum CallType { + Audio, + Video, + Unknown; + + static CallType fromOrdinal(int ordinal) { + return values[ordinal]; + } +} diff --git a/lib/models/registration_state.dart b/lib/models/registration_state.dart new file mode 100644 index 0000000..ff077d0 --- /dev/null +++ b/lib/models/registration_state.dart @@ -0,0 +1,11 @@ +enum RegistrationState { + None, + Progress, + Ok, + Cleared, + Failed; + + static RegistrationState fromOrdinal(int ordinal) { + return values[ordinal]; + } +} diff --git a/lib/widgets/local_view.dart b/lib/widgets/local_view.dart new file mode 100644 index 0000000..1268059 --- /dev/null +++ b/lib/widgets/local_view.dart @@ -0,0 +1,20 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +class LocalView extends StatelessWidget { + const LocalView({super.key}); + + @override + Widget build(BuildContext context) { + final Map creationParams = {}; + + return AndroidView( + viewType: "liblinphone_flutter.nuark.xyz/local_view", + creationParams: creationParams, + creationParamsCodec: const StandardMessageCodec(), + onPlatformViewCreated: (int id) { + print("onPlatformViewCreated: created LocalView with id $id"); + }, + ); + } +} diff --git a/lib/widgets/remote_view.dart b/lib/widgets/remote_view.dart new file mode 100644 index 0000000..b72a018 --- /dev/null +++ b/lib/widgets/remote_view.dart @@ -0,0 +1,20 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; + +class RemoteView extends StatelessWidget { + const RemoteView({super.key}); + + @override + Widget build(BuildContext context) { + final Map creationParams = {}; + + return AndroidView( + viewType: "liblinphone_flutter.nuark.xyz/remote_view", + creationParams: creationParams, + creationParamsCodec: const StandardMessageCodec(), + onPlatformViewCreated: (int id) { + print("onPlatformViewCreated: created RemoteView with id $id"); + }, + ); + } +} diff --git a/pubspec.yaml b/pubspec.yaml new file mode 100644 index 0000000..471d767 --- /dev/null +++ b/pubspec.yaml @@ -0,0 +1,25 @@ +name: liblinphone_flutter +description: "A new Flutter plugin project." +version: 0.0.1 +# homepage: + +environment: + sdk: ^3.9.0-333.2.beta + flutter: ">=3.3.0" + +dependencies: + flutter: + sdk: flutter + plugin_platform_interface: ^2.0.2 + +dev_dependencies: + flutter_test: + sdk: flutter + flutter_lints: ^5.0.0 + +flutter: + plugin: + platforms: + android: + package: xyz.nuark.liblinphone_flutter + pluginClass: LiblinphoneFlutterPlugin diff --git a/test/liblinphone_flutter_method_channel_test.dart b/test/liblinphone_flutter_method_channel_test.dart new file mode 100644 index 0000000..352823a --- /dev/null +++ b/test/liblinphone_flutter_method_channel_test.dart @@ -0,0 +1,27 @@ +import 'package:flutter/services.dart'; +import 'package:flutter_test/flutter_test.dart'; +import 'package:liblinphone_flutter/liblinphone_flutter_method_channel.dart'; + +void main() { + TestWidgetsFlutterBinding.ensureInitialized(); + + MethodChannelLiblinphoneFlutter platform = MethodChannelLiblinphoneFlutter(); + const MethodChannel channel = MethodChannel('liblinphone_flutter'); + + setUp(() { + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler( + channel, + (MethodCall methodCall) async { + return '42'; + }, + ); + }); + + tearDown(() { + TestDefaultBinaryMessengerBinding.instance.defaultBinaryMessenger.setMockMethodCallHandler(channel, null); + }); + + test('getPlatformVersion', () async { + expect(await platform.getPlatformVersion(), '42'); + }); +} diff --git a/test/liblinphone_flutter_test.dart b/test/liblinphone_flutter_test.dart new file mode 100644 index 0000000..f8d21e4 --- /dev/null +++ b/test/liblinphone_flutter_test.dart @@ -0,0 +1,29 @@ +import 'package:flutter_test/flutter_test.dart'; +import 'package:liblinphone_flutter/liblinphone_flutter.dart'; +import 'package:liblinphone_flutter/liblinphone_flutter_platform_interface.dart'; +import 'package:liblinphone_flutter/liblinphone_flutter_method_channel.dart'; +import 'package:plugin_platform_interface/plugin_platform_interface.dart'; + +class MockLiblinphoneFlutterPlatform + with MockPlatformInterfaceMixin + implements LiblinphoneFlutterPlatform { + + @override + Future getPlatformVersion() => Future.value('42'); +} + +void main() { + final LiblinphoneFlutterPlatform initialPlatform = LiblinphoneFlutterPlatform.instance; + + test('$MethodChannelLiblinphoneFlutter is the default instance', () { + expect(initialPlatform, isInstanceOf()); + }); + + test('getPlatformVersion', () async { + LiblinphoneFlutter liblinphoneFlutterPlugin = LiblinphoneFlutter(); + MockLiblinphoneFlutterPlatform fakePlatform = MockLiblinphoneFlutterPlatform(); + LiblinphoneFlutterPlatform.instance = fakePlatform; + + expect(await liblinphoneFlutterPlugin.getPlatformVersion(), '42'); + }); +}