用腾讯即时通讯IM和实时音视频实现完整语音通话功能

说来奇怪,即时通讯领域的霸主QQ,微信,旗下产品出的腾讯即时通讯IM就像个残疾人一样,这里不对那里不对,要达到生产级别,就不得不去改它很多源码才行。今天先不吐槽其他的,我们看看如何在腾讯Im里面完成语音通话功能。
大致分为以下几步:

  • 原材料准备
  • 初步实现语音通话
  • 完善通话逻辑
  • 铃声震动实现、悬浮窗实现
  • 细节优化
原材料准备
  • 腾讯最新版实时音视频SDK(我这里下载的是精简版TRTC)
  • Android Studio 3.5+(需要升级Android Studio的可以参考一下我写的
    升级Android Studio踩坑)的文章,Android 4.1及以上系统(腾讯要求)
初步实现语音通话(根据腾讯的文档集成SDK)

1、集成SDK

  • 在模块的build.gradle中的 dependencies中添加
    dependencies {
    implementation 'com.tencent.liteav:LiteAVSDK_TRTC:latest.release'
    }
  • 在defaultCOnfig中,指定CPU架构
    defaultConfig {
       ndk {
          abiFilters "armeabi", "armeabi-v7a", "arm64-v8a"
        }
    }
  • 配置权限
最后,如果这篇对你有一丁点帮助,请点个赞再走吧,谢谢了喂。
    <uses-permission android:name="android.permission.INTERNET" />    
    <uses-permission android:name="android.permission.ACCESS_NETWORK_STATE" />
    <uses- permission android:name="android.permission.ACCESS_WIFI_STATE" />
    <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" />
    <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE" />
    <uses-permission android:name="android.permission.RECORD_AUDIO" />
    <uses-permission android:name="android.permission.MODIFY_AUDIO_SETTINGS" />
    <uses-permission android:name="android.permission.BLUETOOTH" />
    <uses-permission android:name="android.permission.CAMERA" />
    <uses-permission android:name="android.permission.READ_PHONE_STATE" />
    <uses-feature android:name="android.hardware.camera" />
    <uses-feature android:name="android.hardware.camera.autofocus" />
  • 设置混淆
    -keep class com.tencent.** { *; }
  • 设置打包参数
   packagingOptions {
       pickFirst '**/libc++_shared.so'
       doNotStrip "*/armeabi/libYTCommon.so"
       doNotStrip "*/armeabi-v7a/libYTCommon.so"
       doNotStrip "*/x86/libYTCommon.so"
       doNotStrip "*/arm64-v8a/libYTCommon.so"
   }

2、实现通话

  • 复制源码文件夹trtcaudiocalldemo 中的ui和model到项目中。这里看自己的需求进行选择,实现语音通话,我们只需要TRTCAudioCallActivity.java文件
  • 复制CallService 到项目中,这个Service主要负责处理接听电话的事务(接听电话需要进房需要查询用户信息,生成一个beingCallUserModel传入)
  • 调用 TRTCAudioCallActivity.startCallSomeone(getContext(), mContactList);发起语音通话,这里的mContactList 如果是单聊或者群聊只邀请一个人,只会有一个model,查询设置这个model的avatar、phone、userid、username、groupId即可。到此初步集成完毕,可以进行语音通话了。
完善通话逻辑

1、Android端的通话逻辑并不完善,让我们来看看它的问题

  • 不会发送结束消息,任何情况下的挂断都是发送 取消命令

  • 群通话远端用户离开房间不会触发通话挂断
    问题所在:TRTCAuduiCallImpl中的hangup 在通话进行中或者发起人主动挂断的情况下只会发送取消通话命令


    image.png

    腾讯自己也知道自己有问题,留了一个todo。那么我们如何修改呢?
    根据正常的打电话逻辑,A打给B,会有以下几种情况

    • 未通话:A取消,B拒绝,
    • 通话中:A挂断 ,B挂断
      首先B拒绝,会在hangup方法中进入reject()方法中,发送一个拒绝的消息,这个我们不用处理;然后是A取消的情况,可以通过判断邀请列表的人,如果邀请列表的人大于0,这个时候挂断,那么一定是A取消;再是A挂断和B挂断,这里得区分一下在群聊通话,还是单聊通话,如果是单聊通话,那么A挂断 就是A判断房间中用户数未0,发送一个通话结束消息出去,同理B一样。如果是群聊中,那么就是最后一个退出房间的人判断,发送一个通话结束的消息出去。
      所以在群聊和单聊中没我们可以这样判断:
                    Log.d(TAG, "Hangup: " + mCurRoomUserSet + " " + mCurInvitedList + "  " + mIsInRoom);
                    if (mIsInRoom) {
                        if (isCollectionEmpty(mCurRoomUserSet)) {
                            if (mCurInvitedList.size() > 0) {
                            //取消
                                sendModel("", CallModel.VIDEO_CALL_ACTION_SPONSOR_CANCEL);
                            } else {
                            //通话结束
                                sendModel("", CallModel.VIDEO_CALL_ACTION_HANGUP);
                            }
                        }
                    }
                    stopCall();
                    exitRoom();
                }

并且如果是群聊 ,需要在远端用户退出群主,并且群主里面没有用户的时候发送通话结束的消息即 在preExitRoom方法里面调用groupHangup方法,并且退房相关操作需要注释掉,因为groupHangup方法里面会对房间参数进行判断,需要发消息,然后退房。
当然发送消息并退房并不是所有情况都适用,比如忙线,拒接、超时的时候,就只需要执行退房操作,所以在这些情况下不能调用groupHangup方法,只判断执行退房操作。
2、解析自定义消息
这个东西看需求,一般情况下,一次通话都会有两条消息,即一条发起通话消息,一条结束(拒绝、忙线、挂断、超时等情况),我这里贴一下我的解析方式和效果图:

  private void buildVoiceCallView(ICustomMessageViewGroup parent, MessageInfo info, TRTCAudioCallImpl.CallModel data) {
            if (data.action == TRTCAudioCallImpl.CallModel.VIDEO_CALL_ACTION_DIALING) {
                // 把自定义消息view添加到TUIKit内部的父容器里
                View view = LayoutInflater.from(AndroidApplication.getInstance()).inflate(R.layout.dial_senc_call_message, null, false);
                parent.addMessageItemView(view);
                TextView tv = view.findViewById(R.id.tv_content);
                if (info.isSelf()) {
                    tv.setText("您发起了语音通话");
                } else {
                    tv.setText("对方发起了语音通话");
                }
                return;
            }

            // 把自定义消息view添加到TUIKit内部的父容器里
            View view = LayoutInflater.from(AndroidApplication.getInstance()).inflate(R.layout.dial_custom_message, null, false);
            parent.addMessageContentView(view);

            // 自定义消息view的实现,这里仅仅展示文本信息,并且实现超链接跳转
            TextView textView = view.findViewById(R.id.tv_dial_status);

            ImageView ivLeft = view.findViewById(R.id.iv_left);
            ImageView ivRight = view.findViewById(R.id.iv_right);
            if (info.isSelf()) {
                ivRight.setVisibility(View.VISIBLE);
                ivLeft.setVisibility(View.GONE);
                textView.setTextColor(getResources().getColor(R.color.white));
            } else {
                ivRight.setVisibility(View.GONE);
                ivLeft.setVisibility(View.VISIBLE);
                textView.setTextColor(getResources().getColor(R.color.color_333333));
            }
            String text;
            switch (data.action) {
                case TRTCAudioCallImpl.CallModel.VIDEO_CALL_ACTION_SPONSOR_CANCEL:
                    text = "已取消";
                    break;
                case TRTCAudioCallImpl.CallModel.VIDEO_CALL_ACTION_REJECT:
                    text = "已拒绝";
                    break;
                case TRTCAudioCallImpl.CallModel.VIDEO_CALL_ACTION_SPONSOR_TIMEOUT:
                    text = "无人接听";
                    break;
                case TRTCAudioCallImpl.CallModel.VIDEO_CALL_ACTION_HANGUP:
                    if (data.duration == 0) {
                        text = "通话结束";
                    } else {
                        text = "通话结束 " + TimeUtils.millis2StringByCorrect(data.duration * 1000, data.duration >= 60 * 60 ? "HH:mm:ss" : "mm:ss");
                    }
                    break;
                case TRTCAudioCallImpl.CallModel.VIDEO_CALL_ACTION_LINE_BUSY:
                    text = "忙线中";
                    break;
                default:
                    text = "未知通话错误";
                    break;
            }
            textView.setText(text);
        }
        
效果图
铃声震动实现、悬浮窗实现

1、铃声震动(呼叫和待接听响铃,接听和挂断停止响铃)

  • 呼叫方 邀请页面响铃或震动,在showInvitingView()方法中添加
//开始呼叫响铃
if (mRingVibrateHelper != null) {    mRingVibrateHelper.initLocalCallRinging();}
  • 通话中停止响铃或震动,在showCallingView()方法中使用
//停止响铃if (mRingVibrateHelper != null) {    mRingVibrateHelper.stopRing();}
  • 接听方在,接听等待页面响铃或震动,在showWaitingResponseView()方法中使用
//响铃或者震动mRingVibrateHelper.initRemoteCallRinging();
  • 页面退出,停止响铃
 if (mRingVibrateHelper != null) {
             mRingVibrateHelper.stopRing();
             mRingVibrateHelper.releaseMediaPlayer();
         }

分享一下响铃震动帮助类TimRingVibrateHelper

/**
 * @author leary
 * 响铃震动帮助类
 */
public class TimRingVibrateHelper {
    private static final String TAG = TimRingVibrateHelper.class.getSimpleName();
    /**
     * =============响铃 震动相关
     */
    private MediaPlayer mMediaPlayer;
    private Vibrator mVibrator;

    private static TimRingVibrateHelper instance;

    public static TimRingVibrateHelper getInstance() {
        if (instance == null) {
            synchronized (TimRingVibrateHelper.class) {
                if (instance == null) {
                    instance = new TimRingVibrateHelper();
                }
            }
        }
        return instance;
    }

    private TimRingVibrateHelper() {
        //铃声相关
        mMediaPlayer = new MediaPlayer();
        mMediaPlayer.setOnPreparedListener(mp -> {
            if (mp != null) {
                mp.setLooping(true);
                mp.start();
            }
        });
    }

    /**
     * ==============响铃、震动相关方法========================
     */
    public void initLocalCallRinging() {
        try {
            AssetFileDescriptor assetFileDescriptor = AndroidApplication.getInstance().getResources().openRawResourceFd(R.raw.voip_outgoing_ring);
            mMediaPlayer.reset();
            mMediaPlayer.setDataSource(assetFileDescriptor.getFileDescriptor(),
                    assetFileDescriptor.getStartOffset(), assetFileDescriptor.getLength());
            assetFileDescriptor.close();
            // 设置 MediaPlayer 播放的声音用途
            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
                AudioAttributes attributes = new AudioAttributes.Builder()
                        .setUsage(AudioAttributes.USAGE_VOICE_COMMUNICATION)
                        .build();
                mMediaPlayer.setAudioAttributes(attributes);
            } else {
                mMediaPlayer.setAudioStreamType(AudioManager.STREAM_VOICE_CALL);
            }
            mMediaPlayer.prepareAsync();
            final AudioManager am = (AudioManager) AndroidApplication.getInstance().getSystemService(Context.AUDIO_SERVICE);
            if (am != null) {
                am.setSpeakerphoneOn(false);
                // 设置此值可在拨打时控制响铃音量
                am.setMode(AudioManager.MODE_IN_COMMUNICATION);
                // 设置拨打时响铃音量默认值

                am.setStreamVolume(AudioManager.STREAM_VOICE_CALL, 8, AudioManager.STREAM_VOICE_CALL);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    /**
     * 判断系统响铃正东相关设置
     * 1、系统静音 不震动 就两个都不设置
     * 2、静音震动
     * 3、只响铃不震动
     * 4、响铃且震动
     */
    public void initRemoteCallRinging() {
        int ringerMode = getRingerMode(AndroidApplication.getInstance());
        if (ringerMode != AudioManager.RINGER_MODE_SILENT) {
            if (ringerMode == AudioManager.RINGER_MODE_VIBRATE) {
                startVibrator();
            } else {
                if (isVibrateWhenRinging()) {
                    startVibrator();
                }
                startRing();
            }
        }
    }

    private int getRingerMode(Context context) {
        AudioManager audio = (AudioManager) context.getSystemService(Context.AUDIO_SERVICE);
        return audio.getRingerMode();
    }

    /**
     * 开始响铃
     */
    private void startRing() {
        Uri uri = RingtoneManager.getDefaultUri(RingtoneManager.TYPE_RINGTONE);
        try {
            mMediaPlayer.setDataSource(AndroidApplication.getInstance(), uri);
            mMediaPlayer.prepareAsync();
        } catch (Exception e) {
            e.printStackTrace();
            Log.e(TAG, "Ringtone not found : " + uri);
            try {
                uri = RingtoneManager.getValidRingtoneUri(AndroidApplication.getInstance());
                mMediaPlayer.setDataSource(AndroidApplication.getInstance(), uri);
                mMediaPlayer.prepareAsync();
            } catch (Exception e1) {
                e1.printStackTrace();
                Log.e(TAG, "Ringtone not found: " + uri);
            }
        }
    }

    /**
     * 开始震动
     */
    private void startVibrator() {
        if (mVibrator == null) {
            mVibrator = (Vibrator) AndroidApplication.getInstance().getSystemService(Context.VIBRATOR_SERVICE);
        } else {
            mVibrator.cancel();
        }
        mVibrator.vibrate(new long[]{500, 1000}, 0);
    }

    /**
     * 判断系统是否设置了 响铃时振动
     */
    private boolean isVibrateWhenRinging() {
        ContentResolver resolver = AndroidApplication.getInstance().getApplicationContext().getContentResolver();
        if (Build.MANUFACTURER.equals("Xiaomi")) {
            return Settings.System.getInt(resolver, "vibrate_in_normal", 0) == 1;
        } else if (Build.MANUFACTURER.equals("smartisan")) {
            return Settings.Global.getInt(resolver, "telephony_vibration_enabled", 0) == 1;
        } else {
            return Settings.System.getInt(resolver, "vibrate_when_ringing", 0) == 1;
        }
    }

    /**
     * 停止震动和响铃
     */
    public void stopRing() {
        if (mMediaPlayer != null) {
            mMediaPlayer.reset();
        }
        if (mVibrator != null) {
            mVibrator.cancel();
        }
        if (AndroidApplication.getInstance() != null) {
            //通话时控制音量
            AudioManager audioManager = (AudioManager) AndroidApplication.getInstance().getApplicationContext().getSystemService(AUDIO_SERVICE);
            audioManager.setMode(AudioManager.MODE_NORMAL);
        }
    }

    /**
     * 释放资源
     */
    public void releaseMediaPlayer() {
        if (mMediaPlayer != null) {
            mMediaPlayer.release();
            mMediaPlayer = null;
        }
        if (instance != null) {
            instance = null;
        }
        // 退出此页面后应设置成正常模式,否则按下音量键无法更改其他音频类型的音量
        if (AndroidApplication.getInstance() != null) {
            AudioManager am = (AudioManager) AndroidApplication.getInstance().getApplicationContext().getSystemService(Context.AUDIO_SERVICE);
            if (am != null) {
                am.setMode(AudioManager.MODE_NORMAL);
            }
        }
    }
}

2、悬浮窗 实现

  • 申请权限
  • 将当前通话Activity移动到后台执行
  • 开启悬浮窗服务

1)申请权限

   @TargetApi(19)
  public static boolean canDrawOverlays(final Context context, boolean needOpenPermissionSetting) {
      boolean result = true;
      if (Build.VERSION.SDK_INT >= 23) {
          try {
              boolean booleanValue = (Boolean) Settings.class.getDeclaredMethod("canDrawOverlays", Context.class).invoke((Object) null, context);
              if (!booleanValue && needOpenPermissionSetting) {
                  ArrayList<String> permissionList = new ArrayList();
                  permissionList.add("android.settings.action.MANAGE_OVERLAY_PERMISSION");
                  showPermissionAlert(context, context.getString(R.string.tim_float_window_not_allowed), new DialogInterface.OnClickListener() {
                      @Override
                      public void onClick(DialogInterface dialog, int which) {
                          if (-1 == which) {
                              Intent intent = new Intent("android.settings.action.MANAGE_OVERLAY_PERMISSION", Uri.parse("package:" + context.getPackageName()));
                              context.startActivity(intent);
                          }
                          if (-2 == which) {
                              Toasty.warning(context, "抱歉,您已拒绝DBC获得您的悬浮窗权限,将影响您接听对方发起的语音通话。").show();
                          }

                      }
                  });
              }

              Log.i(TAG, "isFloatWindowOpAllowed allowed: " + booleanValue);
              return booleanValue;
          } catch (Exception var7) {
              Log.e(TAG, String.format("getDeclaredMethod:canDrawOverlays! Error:%s, etype:%s", var7.getMessage(), var7.getClass().getCanonicalName()));
              return true;
          }
      } else if (Build.VERSION.SDK_INT < 19) {
          return true;
      } else {
          Object systemService = context.getSystemService(Context.APP_OPS_SERVICE);
          Method method;
          try {
              method = Class.forName("android.app.AppOpsManager").getMethod("checkOp", Integer.TYPE, Integer.TYPE, String.class);
          } catch (NoSuchMethodException var9) {
              Log.e(TAG, String.format("NoSuchMethodException method:checkOp! Error:%s", var9.getMessage()));
              method = null;
          } catch (ClassNotFoundException var10) {
              var10.printStackTrace();
              method = null;
          }

          if (method != null) {
              try {
                  Integer tmp = (Integer) method.invoke(systemService, 24, context.getApplicationInfo().uid, context.getPackageName());
                  result = tmp == 0;
              } catch (Exception var8) {
                  Log.e(TAG, String.format("call checkOp failed: %s etype:%s", var8.getMessage(), var8.getClass().getCanonicalName()));
              }
          }

          Log.i(TAG, "isFloatWindowOpAllowed allowed: " + result);
          return result;
      }
  }

当然申请悬浮窗全选会有跳转到设置界面这个过程,所以还需要添加判断是否具有悬浮窗权限的判断过程,这里就留点发挥空间了。

2)将当前通话Activity移动到后台执行
这个很简单,就是将Activity的lunchMode改为SingleInstance模式,然后直接调用moveTaskToBack(true);方法,这里传true,表示任何情况下 都会将Acitivty移动到后台。但是有得必有失,设置为SingleInstance模式会为我们带来一些问题,这些我会在后面说明。
3)绑定悬浮窗服务,开启悬浮窗
创建一个悬浮窗Service,获取WindowManager,在windowManager添加一个自定义的悬浮窗View即可,当然要想悬浮窗可以移动,得重写悬浮窗的,触摸事件。在悬浮窗里面注册一个本地广播,方便改变通话状态,记录通话时间等等。贴一下代码,需要自取。

public class TimFloatWindowService extends Service implements View.OnTouchListener {

  private WindowManager mWindowManager;
  private WindowManager.LayoutParams wmParams;
  private LayoutInflater inflater;
  /**
   * 浮动布局view
   */
  private View mFloatingLayout;
  /**
   * 容器父布局
   */
  private View mMainView;

  /**
   * 开始触控的坐标,移动时的坐标(相对于屏幕左上角的坐标)
   */
  private int mTouchStartX, mTouchStartY, mTouchCurrentX, mTouchCurrentY;
  /**
   * 开始时的坐标和结束时的坐标(相对于自身控件的坐标)
   */
  private int mStartX, mStartY, mStopX, mStopY;
  /**
   * 判断悬浮窗口是否移动,这里做个标记,防止移动后松手触发了点击事件
   */
  private boolean isMove;
  /**
   * 判断是否绑定了服务
   */
  private boolean isServiceBind;
  /**
   * 通话状态
   */
  private TextView mAcceptStatus;

  public class TimBinder extends Binder {
      public TimFloatWindowService getService() {
          return TimFloatWindowService.this;
      }
  }

  private BroadcastReceiver mTimBroadCastReceiver = new BroadcastReceiver() {
      @Override
      public void onReceive(Context context, Intent intent) {
          if (isServiceBind && CommonI.TIM.BROADCAST_FLAG_FLOAT_STATUS.equals(intent.getAction())
                  && mAcceptStatus != null) {
              String status = intent.getStringExtra(CommonI.TIM.KEY_ACCEPT_STATUS);
              mAcceptStatus.setText(status);
          }
      }
  };

  @Override
  public IBinder onBind(Intent intent) {
      isServiceBind = true;
      initFloating();//悬浮框点击事件的处理
      return new TimBinder();
  }

  @Override
  public void onCreate() {
      super.onCreate();
      //设置悬浮窗基本参数(位置、宽高等)
      initWindow();
      //注册 BroadcastReceiver 监听情景模式的切换
      IntentFilter filter = new IntentFilter();
      filter.addAction(CommonI.TIM.BROADCAST_FLAG_FLOAT_STATUS);
      LocalBroadcastManager.getInstance(this).registerReceiver(mTimBroadCastReceiver, filter);
  }

  @Override
  public int onStartCommand(Intent intent, int flags, int startId) {
      return super.onStartCommand(intent, flags, startId);
  }

  @Override
  public void onDestroy() {
      super.onDestroy();
      isServiceBind = false;
      if (mFloatingLayout != null) {
          // 移除悬浮窗口
          mWindowManager.removeView(mFloatingLayout);
          mFloatingLayout = null;
      }
      LocalBroadcastManager.getInstance(this).unregisterReceiver(mTimBroadCastReceiver);
  }


  /**
   * 设置悬浮框基本参数(位置、宽高等)
   */
  private void initWindow() {
      mWindowManager = (WindowManager) getApplicationContext().getSystemService(Context.WINDOW_SERVICE);
      //设置好悬浮窗的参数
      wmParams = getParams();
      // 悬浮窗默认显示以右上角为起始坐标
      wmParams.gravity = Gravity.RIGHT | Gravity.TOP;
      // 不设置这个弹出框的透明遮罩显示为黑色
      wmParams.format = PixelFormat.TRANSLUCENT;
      //悬浮窗的开始位置,因为设置的是从右上角开始,所以屏幕左上角是x=0;y=0
      wmParams.x = 40;
      wmParams.y = 160;
      //得到容器,通过这个inflater来获得悬浮窗控件
      inflater = LayoutInflater.from(getApplicationContext());
      // 获取浮动窗口视图所在布局
      mFloatingLayout = inflater.inflate(R.layout.layout_tim_float_window, null);
      // 添加悬浮窗的视图
      mWindowManager.addView(mFloatingLayout, wmParams);
  }


  private WindowManager.LayoutParams getParams() {
      wmParams = new WindowManager.LayoutParams();
      if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT && Build.VERSION.SDK_INT < Build.VERSION_CODES.N) {
          wmParams.type = WindowManager.LayoutParams.TYPE_TOAST;
      } else if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
          wmParams.type = WindowManager.LayoutParams.TYPE_APPLICATION_OVERLAY;
      } else {
          wmParams.type = WindowManager.LayoutParams.TYPE_PHONE;
      }
      //设置可以显示在状态栏上
      wmParams.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE | WindowManager.LayoutParams.FLAG_NOT_TOUCH_MODAL |
              WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN | WindowManager.LayoutParams.FLAG_LAYOUT_INSET_DECOR |
              WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH | WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM |
              WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE;

      //设置悬浮窗口长宽数据
      wmParams.width = WindowManager.LayoutParams.WRAP_CONTENT;
      wmParams.height = WindowManager.LayoutParams.WRAP_CONTENT;
      return wmParams;
  }


  //加载远端视屏:在这对悬浮窗内内容做操作
  private void initFloating() {
      //将子View加载进悬浮窗View
      //悬浮窗父布局
      mMainView = mFloatingLayout.findViewById(R.id.layout_dial_float);
      //加载进悬浮窗的子View,这个VIew来自天转过来的那个Activity里面的那个需要加载的View
      mAcceptStatus = mFloatingLayout.findViewById(R.id.tv_accept_status);
//        View mChildView = renderView.getChildView();
//        mMainView.addView(mChildView);//将需要悬浮显示的Viewadd到mTXCloudVideoView中
      //悬浮框触摸事件,设置悬浮框可拖动
      mMainView.setOnTouchListener(this);
      //悬浮框点击事件
      mMainView.setOnClickListener(v -> {
          //绑定了服务才跳转,不绑定服务不跳转
          if (!isServiceBind) {
              return;
          }
          //在这里实现点击重新回到Activity
          //从该service跳转至该activity会将该activity从后台唤醒,所以activity会走onReStart()
          Intent intent = new Intent(TimFloatWindowService.this, TRTCAudioCallActivity.class);
          //需要Intent.FLAG_ACTIVITY_NEW_TASK,不然会崩溃
          intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK);
          startActivity(intent);
      });
  }

  @Override
  public boolean onTouch(View v, MotionEvent event) {
      int action = event.getAction();
      switch (action) {
          case MotionEvent.ACTION_DOWN:
              isMove = false;
              mTouchStartX = (int) event.getRawX();
              mTouchStartY = (int) event.getRawY();
              mStartX = (int) event.getX();
              mStartY = (int) event.getY();
              break;
          case MotionEvent.ACTION_MOVE:
              mTouchCurrentX = (int) event.getRawX();
              mTouchCurrentY = (int) event.getRawY();
              wmParams.x -= mTouchCurrentX - mTouchStartX;
              wmParams.y += mTouchCurrentY - mTouchStartY;
              Log.i("Tim_FloatingListener", " Cx: " + mTouchCurrentX + " Sx: " + mTouchStartX + " Cy: " + mTouchCurrentY + " Sy: " + mTouchStartY);
              if (mFloatingLayout != null) {
                  mWindowManager.updateViewLayout(mFloatingLayout, wmParams);
              }
              mTouchStartX = mTouchCurrentX;
              mTouchStartY = mTouchCurrentY;
              break;
          case MotionEvent.ACTION_UP:
              mStopX = (int) event.getX();
              mStopY = (int) event.getY();
              if (Math.abs(mStartX - mStopX) >= 1 || Math.abs(mStartY - mStopY) >= 1) {
                  isMove = true;
              }
              break;
          default:
              break;
      }
      //如果是移动事件不触发OnClick事件,防止移动的时候一放手形成点击事件
      return isMove;
  }
}
细节优化

1、SingleInstance的 Home键处理

当luncherModel为SingleInstance的时候,点击Home键会引发很多问题

  • 点击图标回到app的时候进入到的是第一个栈,而不是打电话页面
    我的解决办法是在聊天页面检测通话页面是否正在运行,如果在运行的话,生成一个正在进行语音通话的noticeLayout,然后给noticeLauout设置点击回到语音通话页面。
  • 点击recent键,会回到最初的状态,即 就算通话已经结束,从recent回去 会变成打电话的初始状态。
    设置一个通话是否结束的标记位,保存在SharePreference里面,在onCreate 中进行判断,如果是已经结束的通话,就加载另外一套通话结束的页面。

2、当应用退到后台的时候,部分手机无法唤起后台弹出(小米手机)功能,而有些手机又会直接弹出,显然这两种都不友好。

我们在接电话的地方设置一个30s的计时器,在这30s中不停检测应用是否在前台运行,并且判断通话是否结束,如果检测过程中两个条件都满足了,我们就打开通话页面,然后取消计时。这样做有两个好处,一个是,无法唤起后台弹出的手机,当我们打开app的收,在有效期之内还能接到电话。另外一个是,能后台自动弹出的手机,不会突兀的响铃和乱跳转页面。

3、离线打电话消息接收问题

腾讯的离线推送没有统一的处理,这使得我们监听离线消息变得十分困难,并且有些手机的离线推送甚至不能被检测到。这个时候我们换一种思路,我们直接在打开app的时候检测消息列表的历史消息,获取最后一条消息,进行语音通话的消息处理,这样们在接收离线通知的情况下,也能直接打开到通话页面

最后

使用腾讯IM和腾讯实时音视频 的坑很多,不过都被我们一一淌过来了,如果你遇到不好解决的问题,欢迎留言交流,最后,如果这篇对你有一丁点帮助,请点个赞再走吧,谢谢。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,590评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 86,808评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,151评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,779评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,773评论 5 367
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,656评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,022评论 3 398
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,678评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,038评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,659评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,756评论 1 330
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,411评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,005评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,973评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,203评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,053评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,495评论 2 343