用騰訊即時通訊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和騰訊實時音視頻 的坑很多,不過都被我們一一淌過來了,如果你遇到不好解決的問題,歡迎留言交流,最後,如果這篇對你有一丁點幫助,請點個贊再走吧,謝謝。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章