I am trying to create Voice Over IP (VOIP) mobile application using flutter.I haven't seen an implementation for a flutter plugin for twilio voice api so i intergrated my application with the native android voice api using MethodChannel.The twilio SDK doesnt seem like it intergrated correctly i cant access the twilio classes and methods in scripts. These are the errors i get.
Running Gradle task 'assembleDebug'...
/home/kudziesimz/voip20/android/app/src/main/java/com/workerbees /voip20/MainActivity.java:23: error: package android.support.annotation does not exist
import android.support.annotation.NonNull;
^
/home/kudziesimz/voip20/android/app/src/main/java/com/workerbees /voip20/MainActivity.java:295: error: cannot find symbol
public void onRequestPermissionsResult(int requestCode, #NonNull String[] permissions, #NonNull int[] grantResults) {
^
symbol: class NonNull
location: class MainActivity
/home/kudziesimz/voip20/android/app/src/main/java/com/workerbees /voip20/MainActivity.java:295: error: cannot find symbol
public void onRequestPermissionsResult(int requestCode, #NonNull String[] permissions, #NonNull int[] grantResults) {
^
symbol: class NonNull
location: class MainActivity
/home/kudziesimz/voip20/android/app/src/main/java/com/workerbees /voip20/MainActivity.java:117: error: cannot find symbol
soundPoolManager = SoundPoolManager.getInstance(this.MainActivity);
^
symbol: variable MainActivity
/home/kudziesimz/voip20/android/app/src/main/java/com/workerbees /voip20/MainActivity.java:186: error: cannot find symbol
public void onReconnecting(#NonNull Call call, #NonNull CallException callException) {
^
symbol: class NonNull
/home/kudziesimz/voip20/android/app/src/main/java/com/workerbees /voip20/MainActivity.java:186: error: cannot find symbol
public void onReconnecting(#NonNull Call call, #NonNull CallException callException) {
^
symbol: class NonNull
/home/kudziesimz/voip20/android/app/src/main/java /com/workerbees/voip20/MainActivity.java:191: error: cannot find symbol
public void onReconnected(#NonNull Call call) {
^
symbol: class NonNull
/home/kudziesimz/voip20/android/app/src/main/java/com/workerbees /voip20/MainActivity.java:279: error: cannot find symbol
int resultMic = ContextCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO);
^
symbol: variable ContextCompat
location: class MainActivity
/home/kudziesimz/voip20/android/app/src/main/java/com/workerbees /voip20/MainActivity.java:284: error: method shouldShowRequestPermissionRationale in class Activity cannot be applied to given types;
if (MainActivity.shouldShowRequestPermissionRationale(this, Manifest.permission.RECORD_AUDIO)) {
^
required: String
found: MainActivity,String
reason: actual and formal argument lists differ in length
/home/kudziesimz/voip20/android/app/src/main/java/com/workerbees /voip20/MainActivity.java:287: error: method requestPermissions in class Activity cannot be applied to given types;
MainActivity.requestPermissions(
^
required: String[],int
found: MainActivity,String[],int
reason: actual and formal argument lists differ in length
Note: /home/kudziesimz/voip20/android/app/src/main/java /com/workerbees/voip20/MainActivity.java uses or overrides a deprecated API.
Note: Recompile with -Xlint:deprecation for details.
10 errors
I followed the voice-quickstart-android guide shown here https://github.com/twilio/voice-quickstart-android
here is my code:main.dart
import 'package:flutter/material.dart';
import 'dart:async';
import 'package:flutter/services.dart';
//This is a test application which allows clients to make Voice Over The Internet Cal
void main() => runApp(MaterialApp(
home: MyApp(),
));
class MyApp extends StatefulWidget {
#override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
static const platform = const MethodChannel("com.voip.call_management/calls");
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text("Call Management"),
),
bottomNavigationBar: Center(
child: IconButton(
icon: Icon(Icons.phone),
onPressed: () {
_makeCall;
}),
),
);
}
Future<void> _makeCall() async {
return showDialog<void>(
context: context,
barrierDismissible: false, // user must tap button!
builder: (BuildContext context) {
return AlertDialog(
title: Row(
children: <Widget>[
Text('Call'),
Icon(
Icons.phone,
color: Colors.blue,
)
],
),
content: SingleChildScrollView(
child: ListBody(
children: <Widget>[
TextField(
decoration: InputDecoration(
hintText: "client identity or phone number"),
),
SizedBox(
height: 20,
),
Text(
'Dial a client name or number.Leaving the field empty will result in an automated response.'),
],
),
),
actions: <Widget>[
FlatButton(
child: Text('Cancel'),
onPressed: () {
Navigator.of(context).pop();
},
),
IconButton(icon: Icon(Icons.phone), onPressed:()async {
try {
final result = await platform.invokeMethod("makecall");
} on PlatformException catch (e) {
print(e.message);
}
})
],
);
},
);
}
}
MainActivity.java
package com.workerbees.voip20;
import android.os.Bundle;
import io.flutter.app.FlutterActivity;
import io.flutter.plugins.GeneratedPluginRegistrant;
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;
//javacode imports
import android.Manifest;
import android.content.Context;
import android.content.pm.PackageManager;
import android.media.AudioAttributes;
import android.media.AudioFocusRequest;
import android.media.AudioManager;
import android.os.Build;
import android.support.annotation.NonNull;
import android.util.Log;
import com.google.firebase.iid.FirebaseInstanceId;
import com.koushikdutta.async.future.FutureCallback;
import com.koushikdutta.ion.Ion;
import com.twilio.voice.Call;
import com.twilio.voice.CallException;
import com.twilio.voice.CallInvite;
import com.twilio.voice.ConnectOptions;
import com.twilio.voice.RegistrationException;
import com.twilio.voice.RegistrationListener;
import com.twilio.voice.Voice;
import java.util.HashMap;
//sound pool imports
import android.media.SoundPool;
import static android.content.Context.AUDIO_SERVICE;
public class MainActivity extends FlutterActivity {
private static final String CHANNEL = "com.workerbees.voip/calls"; // MethodChannel Declaration
private static final String TAG = "VoiceActivity";
private static String identity = "alice";
private static String contact;
/*
* You must provide the URL to the publicly accessible Twilio access token server route
*
* For example: https://myurl.io/accessToken
*
* If your token server is written in PHP, TWILIO_ACCESS_TOKEN_SERVER_URL needs .php extension at the end.
*
* For example : https://myurl.io/accessToken.php
*/
private static final String TWILIO_ACCESS_TOKEN_SERVER_URL = "https://bd107744.ngrok.io/accessToken";
private static final int MIC_PERMISSION_REQUEST_CODE = 1;
private String accessToken;
private AudioManager audioManager;
private int savedAudioMode = AudioManager.MODE_INVALID;
// Empty HashMap, never populated for the Quickstart
HashMap<String, String> params = new HashMap<>();
private SoundPoolManager soundPoolManager;
private Call activeCall;
Call.Listener callListener = callListener();
#Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
GeneratedPluginRegistrant.registerWith(this);
new MethodChannel(getFlutterView(), CHANNEL).setMethodCallHandler(
new MethodCallHandler() {
#Override
public void onMethodCall(MethodCall call, Result result) {
// Note: this method is invoked on the main thread.
// TODO
if(call.method.equals("makecall")){
params.put("to", contact);
ConnectOptions connectOptions = new ConnectOptions.Builder(accessToken)
.params(params)
.build();
activeCall = Voice.connect(MainActivity.this, connectOptions, callListener);
}
else if(call.method.equals("hangup")){
disconnect();
}
else if(call.method.equals("mute")){
mute();
}
else if (call.method.equals("hold")){
hold();
}
else{
Log.d(TAG,"invalid API call");
}
}
});
soundPoolManager = SoundPoolManager.getInstance(this.MainActivity);
/*
* Needed for setting/abandoning audio focus during a call
*/
audioManager = (AudioManager) getSystemService(Context.AUDIO_SERVICE);
audioManager.setSpeakerphoneOn(true);
/*
* Enable changing the volume using the up/down keys during a conversation
*/
setVolumeControlStream(AudioManager.STREAM_VOICE_CALL);
/*
* Displays a call dialog if the intent contains a call invite
*/
//handleIncomingCallIntent(getIntent());
/*
* Ensure the microphone permission is enabled
*/
if (!checkPermissionForMicrophone()) {
requestPermissionForMicrophone();
} else {
retrieveAccessToken();
}
}
private Call.Listener callListener() {
return new Call.Listener() {
/*
* This callback is emitted once before the Call.Listener.onConnected() callback when
* the callee is being alerted of a Call. The behavior of this callback is determined by
* the answerOnBridge flag provided in the Dial verb of your TwiML application
* associated with this client. If the answerOnBridge flag is false, which is the
* default, the Call.Listener.onConnected() callback will be emitted immediately after
* Call.Listener.onRinging(). If the answerOnBridge flag is true, this will cause the
* call to emit the onConnected callback only after the call is answered.
* See answeronbridge for more details on how to use it with the Dial TwiML verb. If the
* twiML response contains a Say verb, then the call will emit the
* Call.Listener.onConnected callback immediately after Call.Listener.onRinging() is
* raised, irrespective of the value of answerOnBridge being set to true or false
*/
#Override
public void onRinging(Call call) {
Log.d(TAG, "Ringing");
}
#Override
public void onConnectFailure(Call call, CallException error) {
setAudioFocus(false);
Log.d(TAG, "Connect failure");
String message = String.format("Call Error: %d, %s", error.getErrorCode(), error.getMessage());
Log.e(TAG, message);
}
#Override
public void onConnected(Call call) {
setAudioFocus(true);
Log.d(TAG, "Connected");
activeCall = call;
}
#Override
public void onReconnecting(#NonNull Call call, #NonNull CallException callException) {
Log.d(TAG, "onReconnecting");
}
#Override
public void onReconnected(#NonNull Call call) {
Log.d(TAG, "onReconnected");
}
#Override
public void onDisconnected(Call call, CallException error) {
setAudioFocus(false);
Log.d(TAG, "Disconnected");
if (error != null) {
String message = String.format("Call Error: %d, %s", error.getErrorCode(), error.getMessage());
Log.e(TAG, message);
}
}
};
}
private void disconnect() {
if (activeCall != null) {
activeCall.disconnect();
activeCall = null;
}
}
private void hold() {
if (activeCall != null) {
boolean hold = !activeCall.isOnHold();
activeCall.hold(hold);
}
}
private void mute() {
if (activeCall != null) {
boolean mute = !activeCall.isMuted();
activeCall.mute(mute);
}
}
private void setAudioFocus(boolean setFocus) {
if (audioManager != null) {
if (setFocus) {
savedAudioMode = audioManager.getMode();
// Request audio focus before making any device switch.
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
AudioAttributes playbackAttributes = new AudioAttributes.Builder()
.setUsage(AudioAttributes.USAGE_VOICE_COMMUNICATION)
.setContentType(AudioAttributes.CONTENT_TYPE_SPEECH)
.build();
AudioFocusRequest focusRequest = new AudioFocusRequest.Builder(AudioManager.AUDIOFOCUS_GAIN_TRANSIENT)
.setAudioAttributes(playbackAttributes)
.setAcceptsDelayedFocusGain(true)
.setOnAudioFocusChangeListener(new AudioManager.OnAudioFocusChangeListener() {
#Override
public void onAudioFocusChange(int i) {
}
})
.build();
audioManager.requestAudioFocus(focusRequest);
} else {
if (android.os.Build.VERSION.SDK_INT >= android.os.Build.VERSION_CODES.FROYO) {
int focusRequestResult = audioManager.requestAudioFocus(
new AudioManager.OnAudioFocusChangeListener() {
#Override
public void onAudioFocusChange(int focusChange)
{
}
}, AudioManager.STREAM_VOICE_CALL,
AudioManager.AUDIOFOCUS_GAIN_TRANSIENT);
}
}
/*
* Start by setting MODE_IN_COMMUNICATION as default audio mode. It is
* required to be in this mode when playout and/or recording starts for
* best possible VoIP performance. Some devices have difficulties with speaker mode
* if this is not set.
*/
audioManager.setMode(AudioManager.MODE_IN_COMMUNICATION);
} else {
audioManager.setMode(savedAudioMode);
audioManager.abandonAudioFocus(null);
}
}
}
private boolean checkPermissionForMicrophone() {
int resultMic = ContextCompat.checkSelfPermission(this, Manifest.permission.RECORD_AUDIO);
return resultMic == PackageManager.PERMISSION_GRANTED;
}
private void requestPermissionForMicrophone() {
if (MainActivity.shouldShowRequestPermissionRationale(this, Manifest.permission.RECORD_AUDIO)) {
} else {
MainActivity.requestPermissions(
this,
new String[]{Manifest.permission.RECORD_AUDIO},
MIC_PERMISSION_REQUEST_CODE);
}
}
#Override
public void onRequestPermissionsResult(int requestCode, #NonNull String[] permissions, #NonNull int[] grantResults) {
/*
* Check if microphone permissions is granted
*/
if (requestCode == MIC_PERMISSION_REQUEST_CODE && permissions.length > 0) {
if (grantResults[0] != PackageManager.PERMISSION_GRANTED) {
Log.d(TAG, "Microphone permissions needed. Please allow in your application settings.");
} else {
retrieveAccessToken();
}
}
}
/*
* Get an access token from your Twilio access token server
*/
private void retrieveAccessToken() {
Ion.with(this).load(TWILIO_ACCESS_TOKEN_SERVER_URL + "?identity=" + identity).asString().setCallback(new FutureCallback<String>() {
#Override
public void onCompleted(Exception e, String accessToken) {
if (e == null) {
Log.d(TAG, "Access token: " + accessToken);
MainActivity.this.accessToken = accessToken;
} else {
Log.d(TAG, "Registration failed");
}
}
});
}
}
class SoundPoolManager {
private boolean playing = false;
private boolean loaded = false;
private boolean playingCalled = false;
private float actualVolume;
private float maxVolume;
private float volume;
private AudioManager audioManager;
private SoundPool soundPool;
private int ringingSoundId;
private int ringingStreamId;
private int disconnectSoundId;
private static SoundPoolManager instance;
private SoundPoolManager(Context context) {
// AudioManager audio settings for adjusting the volume
audioManager = (AudioManager) context.getSystemService(AUDIO_SERVICE);
actualVolume = (float) audioManager.getStreamVolume(AudioManager.STREAM_MUSIC);
maxVolume = (float) audioManager.getStreamMaxVolume(AudioManager.STREAM_MUSIC);
volume = actualVolume / maxVolume;
// Load the sounds
int maxStreams = 1;
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
soundPool = new SoundPool.Builder()
.setMaxStreams(maxStreams)
.build();
} else {
soundPool = new SoundPool(maxStreams, AudioManager.STREAM_MUSIC, 0);
}
soundPool.setOnLoadCompleteListener(new SoundPool.OnLoadCompleteListener() {
#Override
public void onLoadComplete(SoundPool soundPool, int sampleId, int status) {
loaded = true;
if (playingCalled) {
playRinging();
playingCalled = false;
}
}
});
ringingSoundId = soundPool.load(context, R.raw.incoming, 1);
disconnectSoundId = soundPool.load(context, R.raw.disconnect, 1);
}
public static SoundPoolManager getInstance(Context context) {
if (instance == null) {
instance = new SoundPoolManager(context);
}
return instance;
}
public void playRinging() {
if (loaded && !playing) {
ringingStreamId = soundPool.play(ringingSoundId, volume, volume, 1, -1, 1f);
playing = true;
} else {
playingCalled = true;
}
}
public void stopRinging() {
if (playing) {
soundPool.stop(ringingStreamId);
playing = false;
}
}
public void playDisconnect() {
if (loaded && !playing) {
soundPool.play(disconnectSoundId, volume, volume, 1, 0, 1f);
playing = false;
}
}
public void release() {
if (soundPool != null) {
soundPool.unload(ringingSoundId);
soundPool.unload(disconnectSoundId);
soundPool.release();
soundPool = null;
}
instance = null;
}
}
This my build.gradle
def localProperties = new Properties()
def localPropertiesFile = rootProject.file('local.properties')
if (localPropertiesFile.exists()) {
localPropertiesFile.withReader('UTF-8') { reader ->
localProperties.load(reader)
}
}
def flutterRoot = localProperties.getProperty('flutter.sdk')
if (flutterRoot == null) {
throw new GradleException("Flutter SDK not found. Define location with flutter.sdk in the local.properties file.")
}
def flutterVersionCode = localProperties.getProperty('flutter.versionCode')
if (flutterVersionCode == null) {
flutterVersionCode = '1'
}
def flutterVersionName = localProperties.getProperty('flutter.versionName')
if (flutterVersionName == null) {
flutterVersionName = '1.0'
}
apply plugin: 'com.android.application'
apply from: "$flutterRoot/packages/flutter_tools/gradle /flutter.gradle"
android {
compileSdkVersion 28
lintOptions {
disable 'InvalidPackage'
}
compileOptions {
sourceCompatibility 1.8
targetCompatibility 1.8
}
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId "com.workerbees.voip20"
minSdkVersion 16
targetSdkVersion 28
versionCode flutterVersionCode.toInteger()
versionName flutterVersionName
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
buildTypes {
release {
// TODO: Add your own signing config for the release build.
// Signing with the debug keys for now, so `flutter run --release` works.
minifyEnabled true
proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
signingConfig signingConfigs.debug
}
// Specify that we want to split up the APK based on ABI
splits {
abi {
// Enable ABI split
enable true
// Clear list of ABIs
reset()
// Specify each architecture currently supported by the Video SDK
include "armeabi-v7a", "arm64-v8a", "x86", "x86_64"
// Specify that we do not want an additional universal SDK
universalApk false
}
}
}
}
flutter {
source '../..'
}
dependencies {
testImplementation 'junit:junit:4.12'
androidTestImplementation 'androidx.test:runner:1.1.1'
androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1'
implementation 'com.twilio:voice-android:4.5.0'
implementation 'com.android.support:design:28.0.0'
implementation 'com.android.support:support-media-compat:28.0.0'
implementation 'com.android.support:animated-vector-drawable:28.0.0'
implementation 'com.android.support:support-v4:28.0.0'
implementation 'com.squareup.retrofit:retrofit:1.9.0'
implementation 'com.koushikdutta.ion:ion:2.1.8'
implementation 'com.google.firebase:firebase-messaging:17.6.0'
implementation 'com.android.support:support-annotations:28.0.0'
}
this is my build.gradle from my gradle folder
buildscript {
repositories {
jcenter()
maven {
url 'https://maven.google.com/'
name 'Google'
}
google()
}
dependencies {
classpath 'com.android.tools.build:gradle:3.2.1'
}
}
allprojects {
repositories {
google()
jcenter()
mavenCentral()
maven {
url 'https://maven.google.com/'
name 'Google'
}
}
}
rootProject.buildDir = '../build'
subprojects {
project.buildDir = "${rootProject.buildDir}/${project.name}"
}
subprojects {
project.evaluationDependsOn(':app')
}
task clean(type: Delete) {
delete rootProject.buildDir
}
Are you still having this issue? Following Flutter platform channels guide, I was able to use Twilio Android SDK without issues. I integrated the bare minimum components needed for Twilio in this demo based from Twilio's Android quickstart.
main.dart
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
void main() {
runApp(MyApp());
}
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
static const platform = const MethodChannel('samples.flutter.dev/twilio');
Future<void> callTwilio() async{
try {
final String result = await platform.invokeMethod('callTwilio');
debugPrint('Result: $result');
} on PlatformException catch (e) {
debugPrint('Failed: ${e.message}.');
}
}
#override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: Text(widget.title),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: <Widget>[
Text(
'Hello',
),
],
),
),
floatingActionButton: FloatingActionButton(
onPressed: () => callTwilio(),
tooltip: 'Call',
child: Icon(Icons.phone),
),
);
}
}
android/app/src/main/kotlin/{PACKAGE_NAME}/MainActivity.kt
class MainActivity : FlutterActivity() {
private val CHANNEL = "samples.flutter.dev/twilio"
private val TAG = "MainActivity"
override fun configureFlutterEngine(#NonNull flutterEngine: FlutterEngine) {
super.configureFlutterEngine(flutterEngine)
MethodChannel(flutterEngine.dartExecutor.binaryMessenger, CHANNEL).setMethodCallHandler { call, result ->
if (call.method == "callTwilio") {
executeTwilioVoiceCall()
result.success("Hello from Android")
} else {
result.notImplemented()
}
}
}
private val accessToken = ""
var params = HashMap<String, String>()
var callListener: Call.Listener = callListener()
fun executeTwilioVoiceCall(){
val connectOptions = ConnectOptions.Builder(accessToken)
.params(params)
.build()
Voice.connect(this, connectOptions, callListener)
}
private fun callListener(): Call.Listener {
return object : Call.Listener {
override fun onRinging(call: Call) {
Log.d(TAG, "Ringing")
}
override fun onConnectFailure(call: Call, error: CallException) {
Log.d(TAG, "Connect failure")
}
override fun onConnected(call: Call) {
Log.d(TAG, "Connected")
}
override fun onReconnecting(call: Call, callException: CallException) {
Log.d(TAG, "onReconnecting")
}
override fun onReconnected(call: Call) {
Log.d(TAG, "onReconnected")
}
override fun onDisconnected(call: Call, error: CallException?) {
Log.d(TAG, "Disconnected")
}
override fun onCallQualityWarningsChanged(call: Call,
currentWarnings: MutableSet<CallQualityWarning>,
previousWarnings: MutableSet<CallQualityWarning>) {
if (previousWarnings.size > 1) {
val intersection: MutableSet<CallQualityWarning> = HashSet(currentWarnings)
currentWarnings.removeAll(previousWarnings)
intersection.retainAll(previousWarnings)
previousWarnings.removeAll(intersection)
}
val message = String.format(
Locale.US,
"Newly raised warnings: $currentWarnings Clear warnings $previousWarnings")
Log.e(TAG, message)
}
}
}
}
As for the dependencies in Android, I've added these on the build.gradle configs
android/build.gradle
ext.versions = [
'voiceAndroid' : '5.6.2',
'audioSwitch' : '1.1.0',
]
android/app/build.grade
dependencies {
...
implementation "com.twilio:audioswitch:${versions.audioSwitch}"
implementation "com.twilio:voice-android:${versions.voiceAndroid}"
}
Here's my flutter doctor verbose logs for reference
[✓] Flutter (Channel master, 1.26.0-2.0.pre.281, on macOS 11.1 20C69 darwin-x64)
• Flutter version 1.26.0-2.0.pre.281
• Framework revision 4d5db88998 (3 weeks ago), 2021-01-11 10:29:26 -0800
• Engine revision d5cacaa3a6
• Dart version 2.12.0 (build 2.12.0-211.0.dev)
[✓] Android toolchain - develop for Android devices (Android SDK version 29.0.2)
• Platform android-30, build-tools 29.0.2
• Java binary at: /Applications/Android Studio.app/Contents/jre/jdk/Contents/Home/bin/java
• Java version OpenJDK Runtime Environment (build 1.8.0_242-release-1644-b3-6915495)
• All Android licenses accepted.
[✓] Xcode - develop for iOS and macOS (Xcode 12.0.1)
• Xcode at /Applications/Xcode.app/Contents/Developer
• Xcode 12.0.1, Build version 12A7300
• CocoaPods version 1.10.0
[✓] Chrome - develop for the web
• Chrome at /Applications/Google Chrome.app/Contents/MacOS/Google Chrome
[✓] Android Studio (version 4.1)
• Android Studio at /Applications/Android Studio.app/Contents
• Flutter plugin can be installed from:
🔨 https://plugins.jetbrains.com/plugin/9212-flutter
• Dart plugin can be installed from:
🔨 https://plugins.jetbrains.com/plugin/6351-dart
• Java version OpenJDK Runtime Environment (build 1.8.0_242-release-1644-b3-6915495)
[✓] VS Code (version 1.52.1)
• VS Code at /Applications/Visual Studio Code.app/Contents
• Flutter extension version 3.18.1
[✓] Connected device (2 available)
• AOSP on IA Emulator (mobile) • emulator-5554 • android-x86 • Android 9 (API 28) (emulator)
• Chrome (web) • chrome • web-javascript • Google Chrome 88.0.4324.96
• No issues found!
Here's how the sample app looks when run. Logs throw "Connect failure" and "Forbidden:403" errors since the API keys set are invalid, but this proves that Twilio Android SDK is functional through Flutter platform channels.
You can also check pub.dev for Twilio Flutter plugins made by the community that may fit your use case.
Related
I use package ffi in flutter but when building with cmake: error exception ,Whatever I searched for, I did not find how to fix this error
Execution failed for task ':libresample_flutter:configureCMakeDebug[arm64-v8a]'.
[CXX1405] error when building with cmake using C:\project\libresample_flutter\android\CMakeLists.txt: Build command failed
cmake_minimum_required(VERSION 3.4.1)
add_library( native_libresample
SHARED
../ios/Classes/filterkit.c ../ios/Classes/lrsexport.c ../ios/Classes/resample.c ../ios/Classes/resamplesubs.c )```
and gradle:
```group 'io.swhh.libresample_flutter'
version '1.0'
buildscript {
repositories {
google()
jcenter()
}
dependencies {
classpath 'com.android.tools.build:gradle:3.5.0'
}
}
rootProject.allprojects {
repositories {
google()
jcenter()
}
}
apply plugin: 'com.android.library'
android {
compileSdkVersion 31
defaultConfig {
minSdkVersion 16
testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
}
lintOptions {
disable 'InvalidPackage'
}
externalNativeBuild {
// Encapsulates your CMake build configurations.
cmake {
// Provides a relative path to your CMake build script.
path "CMakeLists.txt"
}
}
}
and flutter.dart
import 'dart:ffi';
import 'dart:io';
import 'dart:math';
import 'dart:typed_data';
/*
'C' Header definition
void* lrs_open(WORD highQuality,
double minFactor,
double maxFactor) {
*/
typedef lrs_open_func = Pointer<Void> Function(Int32, Double, Double);
typedef LrsOpen = Pointer<Void> Function(int, double, double);
/*
'C' Header definition
void lrs_close(void *handle) {
*/
typedef lrs_close_func = Void Function(Pointer<Void>);
typedef LrsClose = void Function(Pointer<Void>);
/*
'C' Header definition
WORD lrs_process(void *handle,
double factor,
float *inBuffer,
WORD inBufferLen,
WORD lastFlag,
WORD *inBufferUsed,
float *outBuffer,
WORD outBufferLen) {
*/
typedef lrs_process_func = Int32 Function(
Pointer<Void>,
Double,
Pointer<Float>,
Int32,
Int32,
Pointer<Int32>,
Pointer<Float>,
Int32,
);
typedef LrsProcess = int Function(
Pointer<Void>,
double,
Pointer<Float>,
int,
int,
Pointer<Int32>,
Pointer<Float>,
int,
);
class LibresampleFlutter {
static LibresampleFlutter? _instance;
LrsOpen? _lrsOpen;
LrsClose? _lrsClose;
LrsProcess? _lrsProcess;
factory LibresampleFlutter() {
if (_instance == null) {
_instance = LibresampleFlutter._();
}
return _instance!;
}
LibresampleFlutter._() {
final DynamicLibrary nativeLrsLib = Platform.isAndroid
? DynamicLibrary.open("libnative_libresample.so")
: DynamicLibrary.process();
_lrsOpen = nativeLrsLib
.lookup<NativeFunction<lrs_open_func>>('lrs_open')
.asFunction();
_lrsClose = nativeLrsLib
.lookup<NativeFunction<lrs_close_func>>('lrs_close')
.asFunction();
_lrsProcess = nativeLrsLib
.lookup<NativeFunction<lrs_process_func>>('lrs_process')
.asFunction();
}
}
class Resampler {
double? _maxFactor;
Pointer<Void>? _nativeInstance;
var _closed = false;
Pointer<Int32>? _inUsed = allocate();
Pointer<Float>? _inBuf;
var _inLen = 0;
Pointer<Float>? _outBuf;
var _outLen = 0;
[process]. If
/// a fixed refactoring factor is required they may be the same.
///
/// The [highQuality] setting makes the resample create more filters, leading
/// to better quality output.
Resampler(bool highQuality, double minFactor, double maxFactor) {
_maxFactor = maxFactor;
_nativeInstance = LibresampleFlutter()._lrsOpen!(
highQuality ? 1 : 0,
minFactor,
maxFactor,
);
}
void _ensureBuffers(int inSize) {
// output buffer needs to be factor * input buffer
var outSize = (inSize * _maxFactor!).ceil();
// plus some for luck
outSize += min(100, (outSize * 0.1).ceil());
// if (_inLen == 0) {
// _inLen = inSize;
// _inBuf = allocate<Float>(count: inSize);
// } else if (inSize > _inLen) {
// free(_inBuf);
// _inLen = inSize;
// _inBuf = allocate<Float>(count: inSize);
// }
//
// if (_outLen == 0) {
// _outLen = outSize;
// _outBuf = allocate<Float>(count: outSize);
// } else if (outSize > _outLen) {
// free(_outBuf);
// _outLen = outSize;
// _outBuf = allocate<Float>(count: outSize);
// }
}
Float32List? process(double factor, Float32List input, bool last) {
if (_closed) {
throw Exception('already closed');
}
// _ensureBuffers(input.length);
_inBuf!.asTypedList(_inLen).setRange(0, input.length, input);
var processed = LibresampleFlutter()._lrsProcess!(
_nativeInstance!,
factor,
_inBuf!,
input.length,
last ? 1 : 0,
_inUsed!,
_outBuf!,
_outLen,
);
if (processed <= 0) {
print('hmmm $processed');
return null;
}
var output = Float32List(processed);
output.setRange(0, processed, _outBuf!.asTypedList(_outLen));
return output;
}
/// Closes the resampler and frees its native resources.
void close() {
if (!_closed) {
LibresampleFlutter()._lrsClose!(_nativeInstance!);
_closed = true;
}
}
static Pointer<Int32>? allocate() {
return null;
}
}
[inputStream.moveNext()]
/// until that returns [false] at the end of the audio.
class ResamplerStream {
double _factor;
Resampler? _resampler;
Iterator<Float32List>? inputStream;
/// Creates a wrapper around a resampler with an [Iterator] as input and an
/// [Iterable] as output.
///
/// See [Resampler] for an explanation of [_factor] and [highQuality].
ResamplerStream(this._factor, bool highQuality) {
_resampler = Resampler(highQuality, _factor, _factor);
}
/// Sets the input of this audio block to the [Iterator]
void setInputStream(Iterator<Float32List> stream) {
inputStream = stream;
}
/// Retrieves the [Iterable] that can be used by the next block in the chain.
Iterable<Float32List?> getOutputStream() sync* {
while (true) {
if (!inputStream!.moveNext()) break;
yield _resampler!.process(_factor, inputStream!.current, false);
}
}
}
and main.dart
import 'dart:typed_data';
import 'package:flutter/material.dart';
import 'dart:async';
import 'package:flutter/services.dart';
import 'package:libresample_flutter/libresample_flutter.dart';
void main() => runApp(MyApp());
class MyApp extends StatefulWidget {
#override
_MyAppState createState() => _MyAppState();
}
class _MyAppState extends State<MyApp> {
#override
void initState() {
super.initState();
var resampler = Resampler(true, 2, 2);
resampler.process(2, Float32List(160), false);
resampler.process(2, Float32List(160), false);
resampler.process(2, Float32List(160), false);
resampler.process(2, Float32List(160), false);
resampler.process(2, Float32List(160), true);
resampler.close();
}
#override
Widget build(BuildContext context) {
return MaterialApp(
home: Scaffold(
appBar: AppBar(
title: const Text('Plugin example app'),
),
body: Center(
child: Text('Testing'),
),
),
);
}
}
Thanks for helping me
When navigating from one page to another using GetX Flutter, the arguments passed from the first page are not received on the second page.
First Controller:
class LoginController extends GetxController {
var _loading = false.obs;
get loading => _loading.value;
set loading(value) {
_loading.value = value;
}
var _loginResponse = LoginResponse().obs;
late TextEditingController emailController, passwordController;
#override
void onInit() {
super.onInit();
emailController = TextEditingController();
passwordController = TextEditingController();
}
#override
void onReady() {
super.onReady();
}
#override
void onClose() {
super.onClose();
emailController.dispose();
passwordController.dispose();
}
Future<void> loginCall(
{required String email, required String password}) async {
if (!GetUtils.isEmail(email)) {
Get.snackbar('Invalid email', 'Please enter valid email',
snackPosition: SnackPosition.BOTTOM);
return;
}
if (password.length < 8) {
Get.snackbar('Invalid password', 'Minimum 8 chars',
snackPosition: SnackPosition.BOTTOM);
return;
}
_loading.value = true;
var result =
await NetworkRequest.loginCall(email: email, password: password);
result != null
? _loginResponse.value = result
: Get.snackbar(
'Something went wrong', 'Hold back and try again after sometime.',
snackPosition: SnackPosition.BOTTOM);
if(_loginResponse.value.responseCode == 1) {
**Get.offAll(GoogleAuthenticationScreen(), arguments: _loginResponse.value.responseData!.session.toString());**
}
_loading.value = false;
}
}
Second Controller:
class GoogleAuthenticationController extends GetxController {
var _loading = false.obs;
late TextEditingController pinController;
var _authResponse = GoogleTwoFactorResponse().obs;
#override
void onInit() {
super.onInit();
pinController = TextEditingController();
var session = Get.arguments.toString();
twoFactorCall(token: session);
}
#override
void onClose() {
super.onClose();
pinController.dispose();
}
Future<void> twoFactorCall({required String token}) async {
_loading.value = true;
var result = await NetworkRequest.googleAuthentication(token: token);
result != null
? _authResponse.value = result
: Get.snackbar(
'Something went wrong', 'Hold back and try again after sometime.',
snackPosition: SnackPosition.BOTTOM);
Get.snackbar(result!.responseMessage!, "",
snackPosition: SnackPosition.BOTTOM);
_loading.value = false;
}
}
Getx Version get: ^4.3.8
Doctor summary (to see all details, run flutter doctor -v):
[✓] Flutter (Channel stable, 2.2.2, on macOS 11.4 20F71 darwin-x64, locale en-IN)
[✓] Android toolchain - develop for Android devices (Android SDK version 30.0.2)
[✓] Xcode - develop for iOS and macOS
[✓] Chrome - develop for the web
[✓] Android Studio (version 4.1)
[✓] Connected device (2 available)
• No issues found!
you can try
final session = Get.arguments;
outside of onInIt method. hope it will work
As next week will have importat launch for Rust 2018 and Flutter 1.0, I thought to build an app using Rust for the business logic and Flutter for the user interface, that can run at both Android and iOS, I built one and tested it at Android and it is working fine.
I just wonder how to measure the performance and compare it with native Android/iOS app.
The app flow is:
Main is in Flutter, that is calling native function through platform_channel
The native function is calling rust library through JNI (JNI wrapper is required to be call the rust library)
The structure is as below:
The code used is:
main.dart:
import 'dart:async';
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
void main() => runApp(MyApp());
class MyApp extends StatelessWidget {
#override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
theme: ThemeData(
primarySwatch: Colors.blue,
),
home: MyHomePage(title: 'Flutter Demo Home Page'),
);
}
}
class MyHomePage extends StatefulWidget {
MyHomePage({Key key, this.title}) : super(key: key);
final String title;
#override
_MyHomePageState createState() => _MyHomePageState();
}
class _MyHomePageState extends State<MyHomePage> {
static const platform = const MethodChannel('samples.flutter.io/battery');
String _batteryLevel = 'Unknown battery level.';
Future<void> _getBatteryLevel() async {
String batteryLevel;
try {
final String hello = await platform.invokeMethod('getText');
final int result = await platform.invokeMethod('getBatteryLevel');
batteryLevel = '$hello Battery level at $result %.';
} on PlatformException catch (e) {
batteryLevel = "Failed to get battery level: '${e.message}'.";
}
setState(() {
_batteryLevel = batteryLevel;
});
}
#override
Widget build(BuildContext context) {
return Material(
child: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
RaisedButton(
child: Text('Get Battery Level'),
onPressed: _getBatteryLevel,
),
Text(_batteryLevel),
],
),
),
);
}
}
JNI wrapper - RustGreetings.kt
package com.mozilla.greetings
class RustGreetings {
companion object {
init {
System.loadLibrary("greetings")
}
}
private external fun greeting(pattern: String): String
fun sayHello(to: String): String = greeting(to)
}
And the Main Android activity is:
package com.example.batterylevel
import android.os.Bundle
import io.flutter.app.FlutterActivity
import io.flutter.plugins.GeneratedPluginRegistrant
import io.flutter.plugin.common.MethodChannel
import android.content.Context
import android.content.ContextWrapper
import android.content.Intent
import android.content.IntentFilter
import android.os.BatteryManager
import android.os.Build.VERSION
import android.os.Build.VERSION_CODES
import lib.Library
import com.mozilla.greetings.RustGreetings
class MainActivity: FlutterActivity() {
private val CHANNEL = "samples.flutter.io/battery"
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
GeneratedPluginRegistrant.registerWith(this)
MethodChannel(flutterView, CHANNEL).setMethodCallHandler { call, result ->
if (call.method == "getText") {
result.success(getText())
} else if (call.method == "getBatteryLevel") {
// result.success(getText())
val batteryLevel = getBatteryLevel()
if (batteryLevel != -1) {
result.success(batteryLevel)
} else {
result.error("UNAVAILABLE", "Battery level not available.", null)
}
}
else {
result.notImplemented()
}
}
}
private fun getBatteryLevel(): Int {
val batteryLevel: Int
if (VERSION.SDK_INT >= VERSION_CODES.LOLLIPOP) {
val batteryManager = getSystemService(Context.BATTERY_SERVICE) as BatteryManager
batteryLevel = batteryManager.getIntProperty(BatteryManager.BATTERY_PROPERTY_CAPACITY)
} else {
val intent = ContextWrapper(applicationContext).registerReceiver(null, IntentFilter(Intent.ACTION_BATTERY_CHANGED))
batteryLevel = intent!!.getIntExtra(BatteryManager.EXTRA_LEVEL, -1) * 100 / intent.getIntExtra(BatteryManager.EXTRA_SCALE, -1)
}
return batteryLevel
}
private fun getText(): String {
val x = Library().someLibraryMethod()
val g = RustGreetings()
val r = g.sayHello("My $x Rust")
return r
}
}
In the Android gradle.build I just added the below, as I'm interested to check also the impact of adding kotlin JVM library and getting it interacted with the Rust library within the mobile application:
dependencies {
implementation(files("src/main/libs/lib.jar"))
}
My question is:
How can check the performance and impact of each process when it is executed or called by another process
With the introduction of ffi in Dart, things became more smoother now, with a better performance as the interction now is Dart/Rust directly, without a need for Dart/Kotlin/Rust or Dart/Swift/Rust cycle, below a simple example:
First src/lib.rs
#[no_mangle]
pub extern fn rust_fn(x: i32) -> i32 {
println!("Hello from rust\nI'll return: {}", x.pow(2));
x.pow(2)
}
and Cargo.toml
[package]
name = "Double_in_Rost"
version = "0.1.0"
authors = ["Hasan Yousef"]
edition = "2018"
[lib]
name = "rust_lib"
crate-type = ["dylib"] # could be `staticlib` as well
[dependencies]
Running cargo build --release will generate target\release\rust_lib.dll copy/paste it into Dart application root directory
Write Dart code as below:
import 'dart:ffi';
import 'dart:io' show Platform;
// FFI signature of the hello_world C function
typedef ffi_func = Int32 Function(Int32 x); //pub extern fn rust_fn(x: i32) -> i32
// Dart type definition for calling the C foreign function
typedef dart_func = int Function(int x);
void main() {
// Open the dynamic library
var path = './rust_lib.so';
if (Platform.isMacOS) path = './rust_lib.dylib';
if (Platform.isWindows) path = 'rust_lib.dll';
final dylib = DynamicLibrary.open(path);
// Look up the Rust/C function
final my_func =
dylib.lookup<NativeFunction<ffi_func>>('rust_fn').asFunction<dart_func>();
print('Double of 3 is ${my_func(3)}');
}
Using Flutter, a kotlin/swift function can be called by something like:
file.dart:
static const platform = const MethodChannel('my.test.flutterapp/battery');
final int result = await platform.invokeMethod('getBatteryLevel');
file.kt:
private val CHANNEL = "my.test.flutterapp/battery"
MethodChannel(flutterView, CHANNEL).setMethodCallHandler { call, result ->
if (call.method == "getBatteryLevel") {
...
} else {
result.notImplemented()
}
}
Is there something similar to call Kotlin function from standard Dart app, like Dart console app!
https://flutter.io/developing-packages/
Plugin packages: A specialized Dart package which contain an API written in Dart code combined with a platform-specific implementation for Android (using Java or Kotlin), and/or for iOS (using ObjC or Swift). A concrete example is the battery plugin package.
...
flutter create --template=plugin -i swift -a kotlin hello
For the VM the mechanisms available are basic OS operations and native extensions.
By OS operations I mean, you could launch a separate process and interact with it, using files or the network stack. This is probably not as fine grained as you're looking for.
Native extensions allow you to call out to C or C++ code. I don't know enough about kotlin to know if you can easily expose functionality to C/C++. If it's possible, this will give you the tightest integration.
https://www.dartlang.org/articles/dart-vm/native-extensions
You can see this project: Full Sample
In Andriod:
class MainActivity: FlutterActivity() {
private val DATA_CHANNEL = "app.channel.shared.data"
override fun configureFlutterEngine(#NonNull flutterEngine: FlutterEngine) {
GeneratedPluginRegistrant.registerWith(flutterEngine);
MethodChannel(flutterEngine.getDartExecutor().getBinaryMessenger(), DATA_CHANNEL).setMethodCallHandler { call, result ->
if (call.method!!.contentEquals("getSharedText")) {
result.success("Shared Text")
}
}
}
}
In Dart:
import 'package:flutter/material.dart';
import 'package:flutter/services.dart';
void main() {
WidgetsFlutterBinding.ensureInitialized();
runApp(MyApp());
}
class MyApp extends StatefulWidget {
#override
State<StatefulWidget> createState() {
return _MyAppState();
}
}
class _MyAppState extends State<MyApp> {
static const platform = const MethodChannel('app.channel.shared.data');
String dataShared = "No Data";
#override
void initState() {
super.initState();
getSharedText();
}
getSharedText() async {
var sharedData = await platform.invokeMethod("getSharedText");
if (sharedData != null) {
setState(() {
dataShared = sharedData;
});
}
}
#override
Widget build(BuildContext context) {
return MaterialApp(
home:Scaffold(body: Center(child: Text(dataShared)))
);
}
}
I’m using local notifications native plugin on my ionic 3 project (latest version), but when I click on notification and my app is closed the click event is not triggered.
It works when app is in background or foreground.
I use local notifications inside a provider and my on click code is inside its constructor but when app is closed it's not working.
I’ve tried to write code inside platform ready in app/app.component.ts but this approach does not work.
This is my code:
app/app.component.ts
export class MyApp {
#ViewChild(Nav) nav: Nav;
rootPage: any;
constructor(
public platform: Platform,
public menu: MenuController,
public statusBar: StatusBar,
public splashScreen: SplashScreen,
public BeaconServiceProvider: BeaconServiceProvider, /* my provider with local notifications*/
public RemoteServiceProvider: RemoteServiceProvider,
public translate: TranslateService,
public globalization: Globalization,
private oneSignal: OneSignal,
public ga: GoogleAnalytics
) {
......
}
}
My provider
import { HttpClient } from '#angular/common/http';
import { Injectable } from '#angular/core';
import { Storage } from '#ionic/storage';
import { IBeacon } from '#ionic-native/ibeacon';
import { LocalNotifications } from '#ionic-native/local-notifications';
import { RemoteServiceProvider } from '../remote-service/remote-service';
import { StorageProvider } from '../storage/storage';
import { TranslateService } from '#ngx-translate/core';
import { AlertController } from 'ionic-angular';
import { NavController, App} from "ionic-angular/index";
import { GoogleAnalytics } from '#ionic-native/google-analytics';
#Injectable()
export class BeaconServiceProvider {
remoteBeacons: Array<{major: string, minor: string}> = [];
localBeacons: Array<{major: string, minor: string, date: any}> = [];
bluetoothInit: boolean = false;
private navCtrl: NavController;
constructor(
public http: HttpClient,
public alertCtrl: AlertController,
public storage: Storage,
public ibeacon: IBeacon,
public translate: TranslateService,
public localNotifications: LocalNotifications,
public RemoteServiceProvider: RemoteServiceProvider,
public StorageProvider: StorageProvider,
public ga: GoogleAnalytics,
private app: App
) {
console.log('Hello BeaconProvider Provider');
this.localBeacons = this.StorageProvider.localBeacons;
this.navCtrl = this.app.getActiveNav();
let thiz_app = this.app;
let remote = this.RemoteServiceProvider;
this.localNotifications.on("click", function(notification, state){
this.ga.trackEvent(
"Notifiche Beacon (" + remote.version_code + ")",
"Apertura notifica",
"Click sulla notifica beacon " + " {" + notification.message + "}",
1
);
let nav = thiz_app.getActiveNav();
let data = JSON.parse(notification.data);
let page_id = data.page_id;
page_id = page_id.toString();
switch(page_id)
{
case "" :
case "0" :
break;
case "-1" : //Contact Page
if (nav == null)
nav.push('ContactPage');
else if (nav.getActive().component.name != 'ContactPage')
nav.push('ContactPage');
break;
default :
remote.isPageListNew(page_id).subscribe(result => {
let page = result.has_subpages ? 'List2Page' : 'ItemDetailsPage';
if (nav == null)
nav.push(page, { item: result });
else
{
if( nav.getActive().component.name != page)
nav.push(page, { item: result });
else
{
let item_id = nav.getActive().data.item.id;
if (item_id != result.id)
nav.push(page, { item: result });
}
}
});
}
});
}
Any ideas why my code does not work?
Thanks