用騰訊即時通訊IM和實時音視頻實現(xiàn)完整語音通話功能

說來奇怪,即時通訊領(lǐng)域的霸主QQ,微信,旗下產(chǎn)品出的騰訊即時通訊IM就像個殘疾人一樣,這里不對那里不對,要達到生產(chǎn)級別,就不得不去改它很多源碼才行。今天先不吐槽其他的,我們看看如何在騰訊Im里面完成語音通話功能。
大致分為以下幾步:

  • 原材料準(zhǔn)備
  • 初步實現(xiàn)語音通話
  • 完善通話邏輯
  • 鈴聲震動實現(xiàn)、懸浮窗實現(xiàn)
  • 細節(jié)優(yōu)化
原材料準(zhǔn)備
  • 騰訊最新版實時音視頻SDK(我這里下載的是精簡版TRTC)
  • Android Studio 3.5+(需要升級Android Studio的可以參考一下我寫的
    升級Android Studio踩坑)的文章,Android 4.1及以上系統(tǒng)(騰訊要求)
初步實現(xiàn)語音通話(根據(jù)騰訊的文檔集成SDK)

1、集成SDK

  • 在模塊的build.gradle中的 dependencies中添加
    dependencies {
    implementation 'com.tencent.liteav:LiteAVSDK_TRTC:latest.release'
    }
  • 在defaultCOnfig中,指定CPU架構(gòu)
    defaultConfig {
       ndk {
          abiFilters "armeabi", "armeabi-v7a", "arm64-v8a"
        }
    }
  • 配置權(quán)限
最后,如果這篇對你有一丁點幫助,請點個贊再走吧,謝謝了喂。
    <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" />
  • 設(shè)置混淆
    -keep class com.tencent.** { *; }
  • 設(shè)置打包參數(shù)
   packagingOptions {
       pickFirst '**/libc++_shared.so'
       doNotStrip "*/armeabi/libYTCommon.so"
       doNotStrip "*/armeabi-v7a/libYTCommon.so"
       doNotStrip "*/x86/libYTCommon.so"
       doNotStrip "*/arm64-v8a/libYTCommon.so"
   }

2、實現(xiàn)通話

  • 復(fù)制源碼文件夾trtcaudiocalldemo 中的ui和model到項目中。這里看自己的需求進行選擇,實現(xiàn)語音通話,我們只需要TRTCAudioCallActivity.java文件
  • 復(fù)制CallService 到項目中,這個Service主要負責(zé)處理接聽電話的事務(wù)(接聽電話需要進房需要查詢用戶信息,生成一個beingCallUserModel傳入)
  • 調(diào)用 TRTCAudioCallActivity.startCallSomeone(getContext(), mContactList);發(fā)起語音通話,這里的mContactList 如果是單聊或者群聊只邀請一個人,只會有一個model,查詢設(shè)置這個model的avatar、phone、userid、username、groupId即可。到此初步集成完畢,可以進行語音通話了。
完善通話邏輯

1、Android端的通話邏輯并不完善,讓我們來看看它的問題

  • 不會發(fā)送結(jié)束消息,任何情況下的掛斷都是發(fā)送 取消命令

  • 群通話遠端用戶離開房間不會觸發(fā)通話掛斷
    問題所在:TRTCAuduiCallImpl中的hangup 在通話進行中或者發(fā)起人主動掛斷的情況下只會發(fā)送取消通話命令


    image.png

    騰訊自己也知道自己有問題,留了一個todo。那么我們?nèi)绾涡薷哪兀?br> 根據(jù)正常的打電話邏輯,A打給B,會有以下幾種情況

    • 未通話:A取消,B拒絕,
    • 通話中:A掛斷 ,B掛斷
      首先B拒絕,會在hangup方法中進入reject()方法中,發(fā)送一個拒絕的消息,這個我們不用處理;然后是A取消的情況,可以通過判斷邀請列表的人,如果邀請列表的人大于0,這個時候掛斷,那么一定是A取消;再是A掛斷和B掛斷,這里得區(qū)分一下在群聊通話,還是單聊通話,如果是單聊通話,那么A掛斷 就是A判斷房間中用戶數(shù)未0,發(fā)送一個通話結(jié)束消息出去,同理B一樣。如果是群聊中,那么就是最后一個退出房間的人判斷,發(fā)送一個通話結(jié)束的消息出去。
      所以在群聊和單聊中沒我們可以這樣判斷:
                    Log.d(TAG, "Hangup: " + mCurRoomUserSet + " " + mCurInvitedList + "  " + mIsInRoom);
                    if (mIsInRoom) {
                        if (isCollectionEmpty(mCurRoomUserSet)) {
                            if (mCurInvitedList.size() > 0) {
                            //取消
                                sendModel("", CallModel.VIDEO_CALL_ACTION_SPONSOR_CANCEL);
                            } else {
                            //通話結(jié)束
                                sendModel("", CallModel.VIDEO_CALL_ACTION_HANGUP);
                            }
                        }
                    }
                    stopCall();
                    exitRoom();
                }

并且如果是群聊 ,需要在遠端用戶退出群主,并且群主里面沒有用戶的時候發(fā)送通話結(jié)束的消息即 在preExitRoom方法里面調(diào)用groupHangup方法,并且退房相關(guān)操作需要注釋掉,因為groupHangup方法里面會對房間參數(shù)進行判斷,需要發(fā)消息,然后退房。
當(dāng)然發(fā)送消息并退房并不是所有情況都適用,比如忙線,拒接、超時的時候,就只需要執(zhí)行退房操作,所以在這些情況下不能調(diào)用groupHangup方法,只判斷執(zhí)行退房操作。
2、解析自定義消息
這個東西看需求,一般情況下,一次通話都會有兩條消息,即一條發(fā)起通話消息,一條結(jié)束(拒絕、忙線、掛斷、超時等情況),我這里貼一下我的解析方式和效果圖:

  private void buildVoiceCallView(ICustomMessageViewGroup parent, MessageInfo info, TRTCAudioCallImpl.CallModel data) {
            if (data.action == TRTCAudioCallImpl.CallModel.VIDEO_CALL_ACTION_DIALING) {
                // 把自定義消息view添加到TUIKit內(nèi)部的父容器里
                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("您發(fā)起了語音通話");
                } else {
                    tv.setText("對方發(fā)起了語音通話");
                }
                return;
            }

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

            // 自定義消息view的實現(xiàn),這里僅僅展示文本信息,并且實現(xiàn)超鏈接跳轉(zhuǎn)
            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 = "通話結(jié)束";
                    } else {
                        text = "通話結(jié)束 " + 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);
        }
        
效果圖
鈴聲震動實現(xiàn)、懸浮窗實現(xiàn)

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();
    /**
     * =============響鈴 震動相關(guān)
     */
    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() {
        //鈴聲相關(guān)
        mMediaPlayer = new MediaPlayer();
        mMediaPlayer.setOnPreparedListener(mp -> {
            if (mp != null) {
                mp.setLooping(true);
                mp.start();
            }
        });
    }

    /**
     * ==============響鈴、震動相關(guān)方法========================
     */
    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();
            // 設(shè)置 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);
                // 設(shè)置此值可在撥打時控制響鈴音量
                am.setMode(AudioManager.MODE_IN_COMMUNICATION);
                // 設(shè)置撥打時響鈴音量默認值

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

    /**
     * 判斷系統(tǒng)響鈴正東相關(guān)設(shè)置
     * 1、系統(tǒng)靜音 不震動 就兩個都不設(shè)置
     * 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);
    }

    /**
     * 判斷系統(tǒng)是否設(shè)置了 響鈴時振動
     */
    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;
        }
        // 退出此頁面后應(yīng)設(shè)置成正常模式,否則按下音量鍵無法更改其他音頻類型的音量
        if (AndroidApplication.getInstance() != null) {
            AudioManager am = (AudioManager) AndroidApplication.getInstance().getApplicationContext().getSystemService(Context.AUDIO_SERVICE);
            if (am != null) {
                am.setMode(AudioManager.MODE_NORMAL);
            }
        }
    }
}

2、懸浮窗 實現(xiàn)

  • 申請權(quán)限
  • 將當(dāng)前通話Activity移動到后臺執(zhí)行
  • 開啟懸浮窗服務(wù)

1)申請權(quán)限

   @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獲得您的懸浮窗權(quán)限,將影響您接聽對方發(fā)起的語音通話。").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;
      }
  }

當(dāng)然申請懸浮窗全選會有跳轉(zhuǎn)到設(shè)置界面這個過程,所以還需要添加判斷是否具有懸浮窗權(quán)限的判斷過程,這里就留點發(fā)揮空間了。

2)將當(dāng)前通話Activity移動到后臺執(zhí)行
這個很簡單,就是將Activity的lunchMode改為SingleInstance模式,然后直接調(diào)用moveTaskToBack(true);方法,這里傳true,表示任何情況下 都會將Acitivty移動到后臺。但是有得必有失,設(shè)置為SingleInstance模式會為我們帶來一些問題,這些我會在后面說明。
3)綁定懸浮窗服務(wù),開啟懸浮窗
創(chuàng)建一個懸浮窗Service,獲取WindowManager,在windowManager添加一個自定義的懸浮窗View即可,當(dāng)然要想懸浮窗可以移動,得重寫懸浮窗的,觸摸事件。在懸浮窗里面注冊一個本地廣播,方便改變通話狀態(tài),記錄通話時間等等。貼一下代碼,需要自取。

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;

  /**
   * 開始觸控的坐標(biāo),移動時的坐標(biāo)(相對于屏幕左上角的坐標(biāo))
   */
  private int mTouchStartX, mTouchStartY, mTouchCurrentX, mTouchCurrentY;
  /**
   * 開始時的坐標(biāo)和結(jié)束時的坐標(biāo)(相對于自身控件的坐標(biāo))
   */
  private int mStartX, mStartY, mStopX, mStopY;
  /**
   * 判斷懸浮窗口是否移動,這里做個標(biāo)記,防止移動后松手觸發(fā)了點擊事件
   */
  private boolean isMove;
  /**
   * 判斷是否綁定了服務(wù)
   */
  private boolean isServiceBind;
  /**
   * 通話狀態(tài)
   */
  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();
      //設(shè)置懸浮窗基本參數(shù)(位置、寬高等)
      initWindow();
      //注冊 BroadcastReceiver 監(jiān)聽情景模式的切換
      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);
  }


  /**
   * 設(shè)置懸浮框基本參數(shù)(位置、寬高等)
   */
  private void initWindow() {
      mWindowManager = (WindowManager) getApplicationContext().getSystemService(Context.WINDOW_SERVICE);
      //設(shè)置好懸浮窗的參數(shù)
      wmParams = getParams();
      // 懸浮窗默認顯示以右上角為起始坐標(biāo)
      wmParams.gravity = Gravity.RIGHT | Gravity.TOP;
      // 不設(shè)置這個彈出框的透明遮罩顯示為黑色
      wmParams.format = PixelFormat.TRANSLUCENT;
      //懸浮窗的開始位置,因為設(shè)置的是從右上角開始,所以屏幕左上角是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;
      }
      //設(shè)置可以顯示在狀態(tài)欄上
      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;

      //設(shè)置懸浮窗口長寬數(shù)據(jù)
      wmParams.width = WindowManager.LayoutParams.WRAP_CONTENT;
      wmParams.height = WindowManager.LayoutParams.WRAP_CONTENT;
      return wmParams;
  }


  //加載遠端視屏:在這對懸浮窗內(nèi)內(nèi)容做操作
  private void initFloating() {
      //將子View加載進懸浮窗View
      //懸浮窗父布局
      mMainView = mFloatingLayout.findViewById(R.id.layout_dial_float);
      //加載進懸浮窗的子View,這個VIew來自天轉(zhuǎn)過來的那個Activity里面的那個需要加載的View
      mAcceptStatus = mFloatingLayout.findViewById(R.id.tv_accept_status);
//        View mChildView = renderView.getChildView();
//        mMainView.addView(mChildView);//將需要懸浮顯示的Viewadd到mTXCloudVideoView中
      //懸浮框觸摸事件,設(shè)置懸浮框可拖動
      mMainView.setOnTouchListener(this);
      //懸浮框點擊事件
      mMainView.setOnClickListener(v -> {
          //綁定了服務(wù)才跳轉(zhuǎn),不綁定服務(wù)不跳轉(zhuǎn)
          if (!isServiceBind) {
              return;
          }
          //在這里實現(xiàn)點擊重新回到Activity
          //從該service跳轉(zhuǎn)至該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;
      }
      //如果是移動事件不觸發(fā)OnClick事件,防止移動的時候一放手形成點擊事件
      return isMove;
  }
}
細節(jié)優(yōu)化

1、SingleInstance的 Home鍵處理

當(dāng)luncherModel為SingleInstance的時候,點擊Home鍵會引發(fā)很多問題

  • 點擊圖標(biāo)回到app的時候進入到的是第一個棧,而不是打電話頁面
    我的解決辦法是在聊天頁面檢測通話頁面是否正在運行,如果在運行的話,生成一個正在進行語音通話的noticeLayout,然后給noticeLauout設(shè)置點擊回到語音通話頁面。
  • 點擊recent鍵,會回到最初的狀態(tài),即 就算通話已經(jīng)結(jié)束,從recent回去 會變成打電話的初始狀態(tài)。
    設(shè)置一個通話是否結(jié)束的標(biāo)記位,保存在SharePreference里面,在onCreate 中進行判斷,如果是已經(jīng)結(jié)束的通話,就加載另外一套通話結(jié)束的頁面。

2、當(dāng)應(yīng)用退到后臺的時候,部分手機無法喚起后臺彈出(小米手機)功能,而有些手機又會直接彈出,顯然這兩種都不友好。

我們在接電話的地方設(shè)置一個30s的計時器,在這30s中不停檢測應(yīng)用是否在前臺運行,并且判斷通話是否結(jié)束,如果檢測過程中兩個條件都滿足了,我們就打開通話頁面,然后取消計時。這樣做有兩個好處,一個是,無法喚起后臺彈出的手機,當(dāng)我們打開app的收,在有效期之內(nèi)還能接到電話。另外一個是,能后臺自動彈出的手機,不會突兀的響鈴和亂跳轉(zhuǎn)頁面。

3、離線打電話消息接收問題

騰訊的離線推送沒有統(tǒng)一的處理,這使得我們監(jiān)聽離線消息變得十分困難,并且有些手機的離線推送甚至不能被檢測到。這個時候我們換一種思路,我們直接在打開app的時候檢測消息列表的歷史消息,獲取最后一條消息,進行語音通話的消息處理,這樣們在接收離線通知的情況下,也能直接打開到通話頁面

最后

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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

友情鏈接更多精彩內(nèi)容