将原生 Twilio Android SDK 与 Flutter 集成

2024-05-12

我正在尝试使用 flutter 创建 IP 语音 (VOIP) 移动应用程序。我还没有看到 twilio 语音 api 的 flutter 插件的实现,所以我使用 MethodChannel 将我的应用程序与本机 android 语音 api 集成。twilio SDK 似乎没有就像它正确集成一样,我无法访问脚本中的 twilio 类和方法。这些是我得到的错误。

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

我按照此处显示的语音快速入门 Android 指南进行操作https://github.com/twilio/voice-quickstart-android https://github.com/twilio/voice-quickstart-android

这是我的代码: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;
 }
    }

这是我的 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'
        }

这是我的 gradle 文件夹中的 build.gradle

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
    }

您还有这个问题吗?下列的Flutter平台渠道指南 https://flutter.dev/docs/development/platform-integration/platform-channels,我能够毫无问题地使用 Twilio Android SDK。我在这个基于以下内容的演示中集成了 Twilio 所需的最少组件Twilio 的 Android 快速入门 https://github.com/twilio/voice-quickstart-android.

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)
            }
        }
    }
}

至于Android中的依赖项,我已将它们添加到build.gradle配置中

android/build.gradle

ext.versions = [
    'voiceAndroid'       : '5.6.2',
    'audioSwitch'        : '1.1.0',
]

android/app/build.gradle

dependencies {
    ...
    implementation "com.twilio:audioswitch:${versions.audioSwitch}"
    implementation "com.twilio:voice-android:${versions.voiceAndroid}"
}

这是我的flutter doctor详细日志供参考

[✓] 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!

以下是示例应用程序运行时的外观。由于 API 密钥集无效,日志会抛出“Connect failure”和“Forbidden:403”错误,但这证明 Twilio Android SDK 可以通过 Flutter 平台通道正常运行。

您还可以检查pub.dev https://pub.dev/packages?q=twilio社区制作的可能适合您的用例的 Twilio Flutter 插件。

本文内容由网友自发贡献,版权归原作者所有,本站不承担相应法律责任。如您发现有涉嫌抄袭侵权的内容,请联系:hwhale#tublm.com(使用前将#替换为@)

将原生 Twilio Android SDK 与 Flutter 集成 的相关文章

  • Android 在打开应用程序时会广播吗?

    例如 如果我想知道Youtube何时打开 是否有与之相关的广播 我当然知道我可以轮询 logcat 消息来检查活动 但我可以通过广播来做到这一点吗 因为它会少得多的耗电 此链接似乎表明这是不可能的 如何跟踪 Android 中的应用程序使用
  • Android 上的 SVG 支持

    Android 支持 SVG 吗 有什么例子吗 最完整的答案是这样的 Android 2 x 默认浏览器本身不支持 SVG Android 3 默认浏览器支持 SVG 要将 SVG 支持添加到 2 x 版本的平台 您有两个基本选择 安装功能
  • 使用 ADB 命令获取 IMEI 号码 Android 12

    对于 11 之前的 Android 版本 我使用以下命令从我的设备获取 IMEI 号码 adb shell service call iphonesubinfo 4 cut c 52 66 tr d space or adb shell s
  • 如何在android上的python kivy中关闭应用程序后使服务继续工作

    我希望我的服务在关闭应用程序后继续工作 但我做不到 我听说我应该使用startForeground 但如何在Python中做到这一点呢 应用程序代码 from kivy app import App from kivy uix floatl
  • 调整浮动操作按钮的图标大小(fab)

    The new floating action button should be 56dp x 56dp and the icon inside it should be 24dp x 24dp So the space between i
  • android EditText 输入类型用于 StreetNumber 字段

    我试图在地址对话框的 streetNumber 字段中选择正确的 inputType 我想先显示数字键盘 然后让用户输入字母字符 对于一些非常特殊的情况 更接近这个的是 inputType datetime 但这不允许输入字母字符 那么如何
  • AudioTrack、SoundPool 或 MediaPlayer,我应该使用哪个?

    如果我需要能够 播放多个音频文件 具有不同的持续时间 例如 5 到 30 秒 独立设置右 左声道的音量 应用声音效果 如混响 失真 那么 我应该使用哪个 API 另外 我在 AudioTrack API 上找不到太多文档 有谁知道在哪里可以
  • 放置在 NavigationDrawer 顶部的片段

    我正在尝试添加一个PreferenceFragment在我的应用程序中 问题是 它自动放置在我的顶部NavigationDrawer public class SetPreferenceActivity extends Activity O
  • 如何检查用户在EditText中输入自己的电话号码?

    用户将在我的 Android 应用程序的注册页面上的编辑文本中输入手机号码 如何检查用户输入的是他 她的手机号码而不是其他人的 我试过这个 TelephonyManager tMgr TelephonyManager mAppContext
  • 如何在 Android 模块中使用 FirebaseAuth

    我正在开发一个聊天库 我想在其中显示登录用户的对话 制作该库的原因是我想将其集成到多个项目中 我现在面临的问题是FirebaseAuth表示用户尚未登录 FirebaseAuth getInstance mFirebaseApp getCu
  • 将 Firebase 云消息传递与 Windows 应用程序结合使用

    我在 Android 和 iOS 应用程序中使用 Firebase Cloud Messaging 但是我还有此应用程序的 Windows Mac OS 版本 我想保留相同的逻辑 我知道 Firebase Cloud Messaging 可
  • 在 React Native 中调试应用程序崩溃

    我是 React Native 新手 我正在尝试安装 React Native Facebook SDK 以便我可以使用我的应用程序进行 Facebook 登录 我按照此处列出的步骤操作 https tylermcginnis com in
  • 在游戏视图下添加 admob

    我一直试图将 admob 放在我的游戏视图下 这是我的代码 public class HoodStarGame extends AndroidApplication Override public void onCreate Bundle
  • HERE 地图:更改路线已行驶部分的颜色

    导航时可以改变路线的颜色吗 具体来说 我希望路线中已行驶的部分的颜色与即将行驶的部分的颜色不同 现在都是同一个颜色 将 MapRoute 对象的 TravelColor 变量设置为透明对我来说很有效 mapRoute color Resou
  • Android 后台服务示例,具有交互式调用方法

    我不是 Android 方面的专家 我正在寻找一个 Android 应用程序的示例 该应用程序使用一个服务 其中有真正的功能方法 或者换句话说 一个服务可以用来做什么 我们什么时候需要它 超越简单的东西服务举例 我确信您渴望获得一些工作代码
  • 如何修改 Skobbler 注释而不重新添加它

    我必须修改 SKAnnotation 的图像 注释生成器代码 private SKAnnotation getAnnotationFromView int id int minZoomLvl View view SKAnnotation a
  • java.lang.NumberFormatException: Invalid int: "3546504756",这个错误是什么意思?

    我正在创建一个 Android 应用程序 并且正在从文本文件中读取一些坐标 我在用着Integer parseInt xCoordinateStringFromFile 将 X 坐标转换为整数 Y 坐标的转换方法相同 当我运行该应用程序时
  • 模块中的类无法加载

    我正在开发一个 2D Unity android 游戏 其中我最近添加了 Firebase Beta SDK 但添加后FirebaseAnalytics unitypackage我面临的错误是 无法加载模块中的类 当我删除文件夹时Fireb
  • javafx android 中的文本字段和组合框问题

    我在简单的 javafx android 应用程序中遇到问题 问题是我使用 gradle javafxmobile plugin 在 netbeans ide 中构建了非常简单的应用程序 其中包含一些文本字段和组合框 我在 android
  • 单元测试时 Android Studio 2.0 中测试状态终止且没有任何失败消息

    Issue 我昨天在 Ubuntu 上从 1 5 升级到了 Android Studio 2 0 当我在 Android Studio 2 0 中进行单元测试时 即使所有测试都已通过 它也会显示 终止测试 状态 有时它只显示部分测试通过 我

随机推荐

  • Native TF 与 Keras TF 性能比较

    我使用本机和后端张量流创建了完全相同的网络 但在使用多个不同参数进行了多个小时的测试后 仍然无法弄清楚为什么 keras 优于本机张量流并产生更好 稍微但更好 的结果 Keras 是否实现了不同的权重初始化方法 或者执行除 tf train
  • 如何使用 Azure CLI 命令获取虚拟机的公共 IP 地址

    我想在 Azure bash 命令行中获取特定虚拟机的公共 IP 地址 到目前为止我已使用此命令 但它返回网络接口信息 az vm list ip addresses g dev rg n dev vm 返回值 virtualMachine
  • 使用“magic_shell”食谱更新 $PATH

    我创建了两本食谱 java kevin 和 maven kevin 食谱 java kevin recipes default rb node default user vagrant node default user home home
  • 当key未知时如何获取js对象中的属性值

    我有一个对象数组 a 81 25 p 81 25 81 26 p 81 26 我想循环遍历数组并获取值p在每个元素中 for var key in a console log a key outputs 81 25 Object How d
  • 如何使用 XIconifyWindow 更改任务栏图标?

    我在 Linux 中编程 并在桌面上使用 X11 我想更改任务栏中最小化窗口的图像 我正在使用 Fluxbox 当用户单击最小化窗口时 我当前正在使用 XIconifyWindow 当用户最小化任务栏中的小方形图标时 如何控制它 Thank
  • JavaFx 中装饰且不可移动的舞台

    我想在 JavaFx 中创建一个装饰舞台 它也将不可移动 我正在从另一个控制器类创建这个阶段 我能够创造和展示舞台 但它是自由移动的 我怎样才能创建这个 非常感谢帮助和建议 我把打开新关卡的方法贴出来 private void addRec
  • 熊猫加入具有不同索引级别/日期时间的数据帧?

    嗨 我有两个 DataFrame 如下所示 dineType menuName unique columns date y m d
  • 将 geojson 文件下载到 jupyter 中的 folium 中

    我想要一张欧洲地图 作为 json 文件 这样我就可以使用 geojson 将其作为图层放置在 Folium 地图上 这样我就可以将我的数据集嵌入到其中 以显示欧洲哪个国家的酒精含量最高 我在从 GitHub 获取 json 文件以在 jy
  • Symfony:为什么 isInitialized 总是 false?

    我用教义查询了一个用户 customer this gt getDoctrine gt getRepository DemoUserBundle Customer gt find 1 但我得到了结果 顾客 1441 已初始化 错误的 ID
  • (如何)我可以抑制未找到包配置文件的警告吗?

    我正在尝试创建一个CMakeLists txt尝试查找的文件Qt5 如果失败 则尝试回退到Qt4安装 该脚本到目前为止有效 但如果出现以下情况我总会收到警告Qt5未安装 注意FindQt5 cmake是由提供Qt5并且仅当以下情况时才可用Q
  • 使用 Python 将列名称与 CSV 文件中的数据对齐

    这是我用来将数据写入 csv 文件的代码 with open temp csv a as fp a csv writer fp delimiter t data faceXpos faceYpos faceHeight faceWidth
  • 垂直子图的单一颜色条

    我想让下面的 MATLAB 图有一个沿着两个子图延伸的颜色条 像这样的事情 使用图形编辑器手动完成 Note 这与提出的问题不同here https stackoverflow com questions 39950229 matlab t
  • 会话亲和性和粘性会话之间的区别?

    有什么区别会话关联性 and 粘性会话在负载平衡服务器的上下文中 我见过这些术语可以互换使用 但有不同的实现方式 在第一个响应中发送 cookie 然后在后续响应中查找它 cookie 表明要发送到哪个真实服务器 Bad如果您必须支持无 c
  • 找不到“C:\Microsoft.Cpp.Default.props”

    我在 Visual Studio 2013 中创建了一个项目 项目文件具有以下属性 工具版本 12 0 平台工具集 v120 我安装了 Visual Studio 2013 和 Microsoft Build Tools 2015 该项目使
  • 如何命名一段代码并在不同的方法中调用它?

    我使用 Grand Central Dispatch 方法在队列中执行我的应用程序 我在该队列的计算中决定按钮的框架 我希望我的应用程序重新绘制其屏幕并计算旋转后的新帧 这是我所做的一些伪代码解释 CGFloat a 123 b 24 di
  • 使用 Laravel Fluent 查询生成器从多个表中进行选择

    我正在重写一些 PHP MySQL 来与 Laravel 一起使用 我想做的一件事是使数据库查询更加简洁使用 Fluent 查询生成器 http laravel com docs database fluent但我有点迷失 SELECT p
  • 如何使用 Spring MVC 和 Thymeleaf 添加静态文件

    我的问题是如何添加 CSS 和图像文件等静态文件 以便我可以使用它们 我正在使用 Spring MVC 和 Thymeleaf 我查看了有关此主题的各种帖子 但它们对我没有帮助 所以我才来问 根据这些帖子 我将 CSS 和图像文件放在res
  • 如何在 .htaccess 文件中创建一个包罗万象的处理程序?

    我想在 htaccess 文件末尾创建一条规则 捕获在此之前未能匹配的所有内容 我怎样才能做到这一点 附 我已经尝试过一切 实际上 我没有 但看起来确实如此 Update 有些人的回复是RewriteRule 或同等内容 这不行 它将匹配一
  • MUI v5:系统属性样式与 sx prop 之间是否存在性能差异?

    The 从 v4 迁移到 v5 https mui com guides migration v4 box指南指出 The Box系统 props 在 v5 中有一个可选的替代 API 使用sx支柱 你可以阅读本节 https mui co
  • 将原生 Twilio Android SDK 与 Flutter 集成

    我正在尝试使用 flutter 创建 IP 语音 VOIP 移动应用程序 我还没有看到 twilio 语音 api 的 flutter 插件的实现 所以我使用 MethodChannel 将我的应用程序与本机 android 语音 api