使用Flutter集成Twilio Android原生SDK

9

我正在尝试使用Flutter创建语音传输(VOIP)的手机应用程序。由于我没有看到flutter插件实现twilio语音API,因此我使用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

我按照这里展示的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
    }

你成功了吗? - Richard
这是由于未更新到AndroidX,参见https://flutter.dev/docs/development/androidx-migration或类似的问题,如https://dev59.com/MFQJ5IYBdhLWcg3wFx1_或https://github.com/flutter/flutter/issues/23995。 - TWL
1个回答

1

您是否仍然遇到此问题?按照Flutter平台通道指南的说明,我能够无问题地使用Twilio Android SDK。在这个演示中,我基于Twilio的Android快速入门集成了Twilio所需的最少组件。

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.grade

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.281Framework revision 4d5db88998 (3 weeks ago), 2021-01-11 10:29:26 -0800Engine 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.2Java 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/DeveloperXcode 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/ContentsFlutter plugin can be installed from:
       https://plugins.jetbrains.com/plugin/9212-flutterDart plugin can be installed from:
       https://plugins.jetbrains.com/plugin/6351-dartJava 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/ContentsFlutter 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.96No issues found!

这是示例应用程序运行时的外观。由于设置的API密钥无效,日志会抛出“连接失败”和“禁止:403”错误,但这证明Twilio Android SDK通过Flutter平台通道是功能正常的。

Demo

您还可以查看由社区制作的 Twilio Flutter 插件pub.dev,这些插件可能适合您的用例。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接