Pārlūkot izejas kodu

Merge commit '58c40e6ee244e6b78cc8754e8cc621e8400507d7'

drone 2 gadi atpakaļ
vecāks
revīzija
272f2e839c

+ 1 - 1
app/build.gradle

@@ -39,7 +39,7 @@ android {
     buildToolsVersion '33.0.0'
     defaultConfig {
         minSdkVersion 21
-        targetSdkVersion 30
+        targetSdkVersion 31
         testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner"
 
         // mayor.minor.hotfix.increment (for increment: 01-50=Alpha / 51-89=RC / 90-99=stable)

+ 5 - 2
app/src/gplay/AndroidManifest.xml

@@ -2,6 +2,8 @@
   ~ Nextcloud Talk application
   ~
   ~ @author Mario Danic
+  ~ @author Tim Krüger
+  ~ Copyright (C) 2022 Tim Krüger <t@timkrueger.me>
   ~ Copyright (C) 2017-2019 Mario Danic <mario@lovelyhq.com>
   ~
   ~ This program is free software: you can redistribute it and/or modify
@@ -38,8 +40,9 @@
         <meta-data android:name="google_analytics_adid_collection_enabled" android:value="false" />
 
         <service
-            android:name=".services.firebase.MagicFirebaseMessagingService"
-            android:exported="false">
+            android:name=".services.firebase.ChatAndCallMessagingService"
+            android:exported="false"
+            android:foregroundServiceType="phoneCall">
             <intent-filter>
                 <action android:name="com.google.firebase.MESSAGING_EVENT"/>
             </intent-filter>

+ 28 - 27
app/src/gplay/java/com/nextcloud/talk/services/firebase/MagicFirebaseMessagingService.kt → app/src/gplay/java/com/nextcloud/talk/services/firebase/ChatAndCallMessagingService.kt

@@ -2,6 +2,8 @@
  * Nextcloud Talk application
  *
  * @author Mario Danic
+ * @author Tim Krüger
+ * Copyright (C) 2022 Tim Krüger <t@timkrueger.me>
  * Copyright (C) 2017-2019 Mario Danic <mario@lovelyhq.com>
  *
  * This program is free software: you can redistribute it and/or modify
@@ -24,6 +26,7 @@ import android.app.Notification
 import android.app.PendingIntent
 import android.content.Intent
 import android.net.Uri
+import android.os.Build
 import android.os.Bundle
 import android.os.Handler
 import android.util.Base64
@@ -80,32 +83,29 @@ import javax.inject.Inject
 
 @SuppressLint("LongLogTag")
 @AutoInjector(NextcloudTalkApplication::class)
-class MagicFirebaseMessagingService : FirebaseMessagingService() {
-    @JvmField
+class ChatAndCallMessagingService : FirebaseMessagingService() {
+
     @Inject
-    var appPreferences: AppPreferences? = null
+    lateinit var appPreferences: AppPreferences
 
-    var isServiceInForeground: Boolean = false
+    private var isServiceInForeground: Boolean = false
     private var decryptedPushMessage: DecryptedPushMessage? = null
     private var signatureVerification: SignatureVerification? = null
     private var handler: Handler = Handler()
 
-    @JvmField
     @Inject
-    var retrofit: Retrofit? = null
+    lateinit var retrofit: Retrofit
 
-    @JvmField
     @Inject
-    var okHttpClient: OkHttpClient? = null
+    lateinit var okHttpClient: OkHttpClient
 
-    @JvmField
     @Inject
-    var eventBus: EventBus? = null
+    lateinit var eventBus: EventBus
 
     override fun onCreate() {
         super.onCreate()
         sharedApplication!!.componentApplication.inject(this)
-        eventBus?.register(this)
+        eventBus.register(this)
     }
 
     @Subscribe(threadMode = ThreadMode.BACKGROUND)
@@ -118,7 +118,7 @@ class MagicFirebaseMessagingService : FirebaseMessagingService() {
     override fun onDestroy() {
         Log.d(TAG, "onDestroy")
         isServiceInForeground = false
-        eventBus?.unregister(this)
+        eventBus.unregister(this)
         stopForeground(true)
         handler.removeCallbacksAndMessages(null)
         super.onDestroy()
@@ -127,7 +127,7 @@ class MagicFirebaseMessagingService : FirebaseMessagingService() {
     override fun onNewToken(token: String) {
         super.onNewToken(token)
         sharedApplication!!.componentApplication.inject(this)
-        appPreferences!!.pushToken = token
+        appPreferences.pushToken = token
         Log.d(TAG, "onNewToken. token = $token")
 
         val data: Data = Data.Builder().putString(PushRegistrationWorker.ORIGIN, "onNewToken").build()
@@ -168,7 +168,7 @@ class MagicFirebaseMessagingService : FirebaseMessagingService() {
                 Log.d(NotificationWorker.TAG, "Invalid private key " + e1.localizedMessage)
             }
         } catch (exception: Exception) {
-            Log.d(NotificationWorker.TAG, "Something went very wrong " + exception.localizedMessage)
+            Log.d(NotificationWorker.TAG, "Something went very wrong " + exception.localizedMessage, exception)
         }
     }
 
@@ -214,19 +214,23 @@ class MagicFirebaseMessagingService : FirebaseMessagingService() {
 
                 fullScreenIntent.flags = Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_NEW_TASK
                 val fullScreenPendingIntent = PendingIntent.getActivity(
-                    this@MagicFirebaseMessagingService,
+                    this@ChatAndCallMessagingService,
                     0,
                     fullScreenIntent,
-                    PendingIntent.FLAG_UPDATE_CURRENT
+                    if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
+                        PendingIntent.FLAG_MUTABLE or PendingIntent.FLAG_UPDATE_CURRENT
+                    } else {
+                        PendingIntent.FLAG_UPDATE_CURRENT
+                    }
                 )
 
-                val soundUri = getCallRingtoneUri(applicationContext!!, appPreferences!!)
+                val soundUri = getCallRingtoneUri(applicationContext!!, appPreferences)
                 val notificationChannelId = NotificationUtils.NOTIFICATION_CHANNEL_CALLS_V4
                 val uri = Uri.parse(signatureVerification!!.userEntity!!.baseUrl)
                 val baseUrl = uri.host
 
                 val notification =
-                    NotificationCompat.Builder(this@MagicFirebaseMessagingService, notificationChannelId)
+                    NotificationCompat.Builder(this@ChatAndCallMessagingService, notificationChannelId)
                         .setPriority(NotificationCompat.PRIORITY_HIGH)
                         .setCategory(NotificationCompat.CATEGORY_CALL)
                         .setSmallIcon(R.drawable.ic_call_black_24dp)
@@ -263,8 +267,8 @@ class MagicFirebaseMessagingService : FirebaseMessagingService() {
         decryptedPushMessage: DecryptedPushMessage
     ) {
         Log.d(TAG, "checkIfCallIsActive")
-        val ncApi = retrofit!!.newBuilder()
-            .client(okHttpClient!!.newBuilder().cookieJar(JavaNetCookieJar(CookieManager())).build()).build()
+        val ncApi = retrofit.newBuilder()
+            .client(okHttpClient.newBuilder().cookieJar(JavaNetCookieJar(CookieManager())).build()).build()
             .create(NcApi::class.java)
         var hasParticipantsInCall = true
         var inCallOnDifferentDevice = false
@@ -292,9 +296,7 @@ class MagicFirebaseMessagingService : FirebaseMessagingService() {
             }
             .subscribeOn(Schedulers.io())
             .subscribe(object : Observer<ParticipantsOverall> {
-                override fun onSubscribe(d: Disposable) {
-                    // unused atm
-                }
+                override fun onSubscribe(d: Disposable) = Unit
 
                 override fun onNext(participantsOverall: ParticipantsOverall) {
                     val participantList: List<Participant> = participantsOverall.ocs!!.data!!
@@ -316,9 +318,8 @@ class MagicFirebaseMessagingService : FirebaseMessagingService() {
                     }
                 }
 
-                override fun onError(e: Throwable) {
-                    // unused atm
-                }
+                override fun onError(e: Throwable) = Unit
+
                 override fun onComplete() {
                     stopForeground(true)
                     handler.removeCallbacksAndMessages(null)
@@ -327,7 +328,7 @@ class MagicFirebaseMessagingService : FirebaseMessagingService() {
     }
 
     companion object {
-        const val TAG = "MagicFirebaseMessagingService"
+        private val TAG = ChatAndCallMessagingService::class.simpleName
         private const val OBSERVABLE_COUNT = 12
         private const val OBSERVABLE_DELAY: Long = 5
     }

+ 11 - 3
app/src/main/AndroidManifest.xml

@@ -37,8 +37,11 @@
     <uses-permission
         android:name="android.permission.AUTHENTICATE_ACCOUNTS"
         android:maxSdkVersion="22" />
-    <uses-permission android:name="android.permission.BLUETOOTH" />
+    <uses-permission
+        android:name="android.permission.BLUETOOTH"
+        android:maxSdkVersion="30" />
     <uses-permission android:name="android.permission.BLUETOOTH_CONNECT" />
+
     <uses-permission android:name="android.permission.CAMERA" />
     <uses-permission
         android:name="android.permission.GET_ACCOUNTS"
@@ -105,6 +108,7 @@
         <activity
             android:name=".activities.MainActivity"
             android:label="@string/nc_app_name"
+            android:exported="true"
             android:windowSoftInputMode="adjustResize">
             <intent-filter>
                 <action android:name="android.intent.action.MAIN" />
@@ -127,6 +131,8 @@
                 <action android:name="android.intent.action.VIEW" />
                 <category android:name="android.intent.category.DEFAULT" />
                 <data android:mimeType="vnd.android.cursor.item/vnd.com.nextcloud.talk2.chat" />
+                <data android:scheme="content" />
+                <data android:scheme="file" />
             </intent-filter>
         </activity>
 
@@ -185,7 +191,8 @@
             android:name=".messagesearch.MessageSearchActivity"
             android:theme="@style/AppTheme" />
 
-        <receiver android:name=".receivers.PackageReplacedReceiver">
+        <receiver android:name=".receivers.PackageReplacedReceiver"
+            android:exported="false">
             <intent-filter>
                 <action android:name="android.intent.action.MY_PACKAGE_REPLACED" />
             </intent-filter>
@@ -208,7 +215,8 @@
                 android:resource="@xml/contacts" />
         </service>
 
-        <service android:name=".utils.AuthenticatorService">
+        <service android:name=".utils.AuthenticatorService"
+            android:exported="false">
             <intent-filter>
                 <action android:name="android.accounts.AccountAuthenticator" />
             </intent-filter>

+ 54 - 15
app/src/main/java/com/nextcloud/talk/activities/CallActivity.java

@@ -30,6 +30,7 @@ import android.content.BroadcastReceiver;
 import android.content.Context;
 import android.content.Intent;
 import android.content.IntentFilter;
+import android.content.pm.PackageManager;
 import android.content.res.Configuration;
 import android.graphics.Color;
 import android.graphics.drawable.Icon;
@@ -94,7 +95,7 @@ import com.nextcloud.talk.utils.database.user.UserUtils;
 import com.nextcloud.talk.utils.power.PowerManagerUtils;
 import com.nextcloud.talk.utils.preferences.AppPreferences;
 import com.nextcloud.talk.utils.singletons.ApplicationWideCurrentRoomHolder;
-import com.nextcloud.talk.webrtc.MagicAudioManager;
+import com.nextcloud.talk.webrtc.WebRtcAudioManger;
 import com.nextcloud.talk.webrtc.MagicWebRTCUtils;
 import com.nextcloud.talk.webrtc.MagicWebSocketInstance;
 import com.nextcloud.talk.webrtc.PeerConnectionWrapper;
@@ -139,12 +140,15 @@ import java.util.concurrent.TimeUnit;
 
 import javax.inject.Inject;
 
+import androidx.activity.result.ActivityResultLauncher;
+import androidx.activity.result.contract.ActivityResultContracts;
 import androidx.annotation.DrawableRes;
 import androidx.annotation.NonNull;
 import androidx.annotation.Nullable;
 import androidx.annotation.RequiresApi;
 import androidx.appcompat.app.AppCompatActivity;
 import androidx.appcompat.content.res.AppCompatResources;
+import androidx.core.content.ContextCompat;
 import androidx.core.graphics.drawable.DrawableCompat;
 import autodagger.AutoInjector;
 import io.reactivex.Observable;
@@ -158,6 +162,8 @@ import me.zhanghai.android.effortlesspermissions.OpenAppDetailsDialogFragment;
 import okhttp3.Cache;
 import pub.devrel.easypermissions.AfterPermissionGranted;
 
+import static android.app.PendingIntent.FLAG_MUTABLE;
+import static android.app.PendingIntent.FLAG_MUTABLE;
 import static com.nextcloud.talk.webrtc.Globals.JOB_ID;
 import static com.nextcloud.talk.webrtc.Globals.PARTICIPANTS_UPDATE;
 import static com.nextcloud.talk.webrtc.Globals.ROOM_TOKEN;
@@ -183,11 +189,11 @@ public class CallActivity extends CallBaseActivity {
 
     public static final String TAG = "CallActivity";
 
-    public MagicAudioManager audioManager;
+    public WebRtcAudioManger audioManager;
 
     private static final String[] PERMISSIONS_CALL = {
-        android.Manifest.permission.CAMERA,
-        android.Manifest.permission.RECORD_AUDIO,
+        Manifest.permission.CAMERA,
+        Manifest.permission.RECORD_AUDIO
     };
 
     private static final String[] PERMISSIONS_CAMERA = {
@@ -269,6 +275,13 @@ public class CallActivity extends CallBaseActivity {
 
     private AudioOutputDialog audioOutputDialog;
 
+    private final ActivityResultLauncher<String> requestBluetoothPermissionLauncher =
+        registerForActivityResult(new ActivityResultContracts.RequestPermission(), isGranted -> {
+            if (isGranted) {
+                enableBluetoothManager();
+            }
+        });
+
     @SuppressLint("ClickableViewAccessibility")
     @Override
     public void onCreate(Bundle savedInstanceState) {
@@ -318,6 +331,9 @@ public class CallActivity extends CallBaseActivity {
             .setRepeatCount(PulseAnimation.INFINITE)
             .setRepeatMode(PulseAnimation.REVERSE);
 
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
+            requestBluetoothPermission();
+        }
         basicInitialization();
         participantDisplayItems = new HashMap<>();
         initViews();
@@ -327,6 +343,22 @@ public class CallActivity extends CallBaseActivity {
         updateSelfVideoViewPosition();
     }
 
+    @SuppressLint("InlinedApi")
+    @RequiresApi(api = Build.VERSION_CODES.S)
+    private void requestBluetoothPermission() {
+        if (ContextCompat.checkSelfPermission(
+            getContext(), Manifest.permission.BLUETOOTH_CONNECT) ==
+            PackageManager.PERMISSION_DENIED) {
+            requestBluetoothPermissionLauncher.launch(Manifest.permission.BLUETOOTH_CONNECT);
+        }
+    }
+
+    private void enableBluetoothManager() {
+        if (audioManager != null) {
+            audioManager.startBluetoothManager();
+        }
+    }
+
     @Override
     public void onStart() {
         super.onStart();
@@ -420,16 +452,16 @@ public class CallActivity extends CallBaseActivity {
 
         // Create and audio manager that will take care of audio routing,
         // audio modes, audio device enumeration etc.
-        audioManager = MagicAudioManager.create(getApplicationContext(), isVoiceOnlyCall);
+        audioManager = WebRtcAudioManger.create(getApplicationContext(), isVoiceOnlyCall);
         // Store existing audio settings and change audio mode to
         // MODE_IN_COMMUNICATION for best possible VoIP performance.
         Log.d(TAG, "Starting the audio manager...");
         audioManager.start(this::onAudioManagerDevicesChanged);
 
         if (isVoiceOnlyCall) {
-            setAudioOutputChannel(MagicAudioManager.AudioDevice.EARPIECE);
+            setAudioOutputChannel(WebRtcAudioManger.AudioDevice.EARPIECE);
         } else {
-            setAudioOutputChannel(MagicAudioManager.AudioDevice.SPEAKER_PHONE);
+            setAudioOutputChannel(WebRtcAudioManger.AudioDevice.SPEAKER_PHONE);
         }
 
         iceServers = new ArrayList<>();
@@ -463,14 +495,14 @@ public class CallActivity extends CallBaseActivity {
         microphoneInitialization();
     }
 
-    public void setAudioOutputChannel(MagicAudioManager.AudioDevice selectedAudioDevice) {
+    public void setAudioOutputChannel(WebRtcAudioManger.AudioDevice selectedAudioDevice) {
         if (audioManager != null) {
             audioManager.selectAudioDevice(selectedAudioDevice);
             updateAudioOutputButton(audioManager.getCurrentAudioDevice());
         }
     }
 
-    private void updateAudioOutputButton(MagicAudioManager.AudioDevice activeAudioDevice) {
+    private void updateAudioOutputButton(WebRtcAudioManger.AudioDevice activeAudioDevice) {
         switch (activeAudioDevice) {
             case BLUETOOTH:
                 binding.audioOutputButton.getHierarchy().setPlaceholderImage(
@@ -764,14 +796,14 @@ public class CallActivity extends CallBaseActivity {
     }
 
     private void onAudioManagerDevicesChanged(
-        final MagicAudioManager.AudioDevice currentDevice,
-        final Set<MagicAudioManager.AudioDevice> availableDevices) {
+        final WebRtcAudioManger.AudioDevice currentDevice,
+        final Set<WebRtcAudioManger.AudioDevice> availableDevices) {
         Log.d(TAG, "onAudioManagerDevicesChanged: " + availableDevices + ", "
             + "currentDevice: " + currentDevice);
 
-        final boolean shouldDisableProximityLock = (currentDevice.equals(MagicAudioManager.AudioDevice.WIRED_HEADSET)
-            || currentDevice.equals(MagicAudioManager.AudioDevice.SPEAKER_PHONE)
-            || currentDevice.equals(MagicAudioManager.AudioDevice.BLUETOOTH));
+        final boolean shouldDisableProximityLock = (currentDevice.equals(WebRtcAudioManger.AudioDevice.WIRED_HEADSET)
+            || currentDevice.equals(WebRtcAudioManger.AudioDevice.SPEAKER_PHONE)
+            || currentDevice.equals(WebRtcAudioManger.AudioDevice.BLUETOOTH));
 
         if (shouldDisableProximityLock) {
             powerManagerUtils.updatePhoneState(PowerManagerUtils.PhoneState.WITHOUT_PROXIMITY_SENSOR_LOCK);
@@ -2579,12 +2611,19 @@ public class CallActivity extends CallBaseActivity {
             final ArrayList<RemoteAction> actions = new ArrayList<>();
 
             final Icon icon = Icon.createWithResource(this, iconId);
+
+            int intentFlag;
+            if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
+                intentFlag = FLAG_MUTABLE;
+            } else {
+                intentFlag = 0;
+            }
             final PendingIntent intent =
                 PendingIntent.getBroadcast(
                     this,
                     requestCode,
                     new Intent(MICROPHONE_PIP_INTENT_NAME).putExtra(MICROPHONE_PIP_INTENT_EXTRA_ACTION, requestCode),
-                    0);
+                    intentFlag);
 
             actions.add(new RemoteAction(icon, title, title, intent));
 

+ 15 - 2
app/src/main/java/com/nextcloud/talk/jobs/NotificationWorker.java

@@ -295,7 +295,13 @@ public class NotificationWorker extends Worker {
         // Use unique request code to make sure that a new PendingIntent gets created for each notification
         // See https://github.com/nextcloud/talk-android/issues/2111
         int requestCode = (int) System.currentTimeMillis();
-        PendingIntent pendingIntent = PendingIntent.getActivity(context, requestCode, intent, 0);
+        int intentFlag;
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
+            intentFlag = PendingIntent.FLAG_MUTABLE;
+        } else {
+            intentFlag = 0;
+        }
+        PendingIntent pendingIntent = PendingIntent.getActivity(context, requestCode, intent, intentFlag);
 
         Uri uri = Uri.parse(signatureVerification.getUserEntity().getBaseUrl());
         String baseUrl = uri.getHost();
@@ -422,8 +428,15 @@ public class NotificationWorker extends Worker {
         // It is NOT the same as the notification ID used in communication with the server.
         actualIntent.putExtra(BundleKeys.INSTANCE.getKEY_SYSTEM_NOTIFICATION_ID(), systemNotificationId);
         actualIntent.putExtra(BundleKeys.INSTANCE.getKEY_ROOM_TOKEN(), decryptedPushMessage.getId());
+
+        int intentFlag;
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
+            intentFlag = PendingIntent.FLAG_MUTABLE|PendingIntent.FLAG_UPDATE_CURRENT;
+        } else {
+            intentFlag = PendingIntent.FLAG_UPDATE_CURRENT;
+        }
         PendingIntent replyPendingIntent =
-            PendingIntent.getBroadcast(context, systemNotificationId, actualIntent, PendingIntent.FLAG_UPDATE_CURRENT);
+            PendingIntent.getBroadcast(context, systemNotificationId, actualIntent, intentFlag);
 
         NotificationCompat.Action replyAction =
             new NotificationCompat.Action.Builder(R.drawable.ic_reply, replyLabel, replyPendingIntent)

+ 12 - 12
app/src/main/java/com/nextcloud/talk/ui/dialog/AudioOutputDialog.kt

@@ -30,7 +30,7 @@ import com.google.android.material.bottomsheet.BottomSheetDialog
 import com.nextcloud.talk.R
 import com.nextcloud.talk.activities.CallActivity
 import com.nextcloud.talk.databinding.DialogAudioOutputBinding
-import com.nextcloud.talk.webrtc.MagicAudioManager
+import com.nextcloud.talk.webrtc.WebRtcAudioManger
 
 class AudioOutputDialog(val callActivity: CallActivity) : BottomSheetDialog(callActivity) {
 
@@ -47,26 +47,26 @@ class AudioOutputDialog(val callActivity: CallActivity) : BottomSheetDialog(call
     }
 
     fun updateOutputDeviceList() {
-        if (callActivity.audioManager?.audioDevices?.contains(MagicAudioManager.AudioDevice.BLUETOOTH) == false) {
+        if (callActivity.audioManager?.audioDevices?.contains(WebRtcAudioManger.AudioDevice.BLUETOOTH) == false) {
             dialogAudioOutputBinding.audioOutputBluetooth.visibility = View.GONE
         } else {
             dialogAudioOutputBinding.audioOutputBluetooth.visibility = View.VISIBLE
         }
 
-        if (callActivity.audioManager?.audioDevices?.contains(MagicAudioManager.AudioDevice.EARPIECE) == false) {
+        if (callActivity.audioManager?.audioDevices?.contains(WebRtcAudioManger.AudioDevice.EARPIECE) == false) {
             dialogAudioOutputBinding.audioOutputEarspeaker.visibility = View.GONE
         } else {
             dialogAudioOutputBinding.audioOutputEarspeaker.visibility = View.VISIBLE
         }
 
-        if (callActivity.audioManager?.audioDevices?.contains(MagicAudioManager.AudioDevice.SPEAKER_PHONE) == false) {
+        if (callActivity.audioManager?.audioDevices?.contains(WebRtcAudioManger.AudioDevice.SPEAKER_PHONE) == false) {
             dialogAudioOutputBinding.audioOutputSpeaker.visibility = View.GONE
         } else {
             dialogAudioOutputBinding.audioOutputSpeaker.visibility = View.VISIBLE
         }
 
         if (callActivity.audioManager?.currentAudioDevice?.equals(
-                MagicAudioManager.AudioDevice.WIRED_HEADSET
+                WebRtcAudioManger.AudioDevice.WIRED_HEADSET
             ) == true
         ) {
             dialogAudioOutputBinding.audioOutputEarspeaker.visibility = View.GONE
@@ -81,7 +81,7 @@ class AudioOutputDialog(val callActivity: CallActivity) : BottomSheetDialog(call
 
     private fun highlightActiveOutputChannel() {
         when (callActivity.audioManager?.currentAudioDevice) {
-            MagicAudioManager.AudioDevice.BLUETOOTH -> {
+            WebRtcAudioManger.AudioDevice.BLUETOOTH -> {
                 dialogAudioOutputBinding.audioOutputBluetoothIcon.setColorFilter(
                     ContextCompat.getColor(
                         context,
@@ -94,7 +94,7 @@ class AudioOutputDialog(val callActivity: CallActivity) : BottomSheetDialog(call
                 )
             }
 
-            MagicAudioManager.AudioDevice.SPEAKER_PHONE -> {
+            WebRtcAudioManger.AudioDevice.SPEAKER_PHONE -> {
                 dialogAudioOutputBinding.audioOutputSpeakerIcon.setColorFilter(
                     ContextCompat.getColor(
                         context,
@@ -107,7 +107,7 @@ class AudioOutputDialog(val callActivity: CallActivity) : BottomSheetDialog(call
                 )
             }
 
-            MagicAudioManager.AudioDevice.EARPIECE -> {
+            WebRtcAudioManger.AudioDevice.EARPIECE -> {
                 dialogAudioOutputBinding.audioOutputEarspeakerIcon.setColorFilter(
                     ContextCompat.getColor(
                         context,
@@ -120,7 +120,7 @@ class AudioOutputDialog(val callActivity: CallActivity) : BottomSheetDialog(call
                 )
             }
 
-            MagicAudioManager.AudioDevice.WIRED_HEADSET -> {
+            WebRtcAudioManger.AudioDevice.WIRED_HEADSET -> {
                 dialogAudioOutputBinding.audioOutputWiredHeadsetIcon.setColorFilter(
                     ContextCompat.getColor(
                         context,
@@ -139,17 +139,17 @@ class AudioOutputDialog(val callActivity: CallActivity) : BottomSheetDialog(call
 
     private fun initClickListeners() {
         dialogAudioOutputBinding.audioOutputBluetooth.setOnClickListener {
-            callActivity.setAudioOutputChannel(MagicAudioManager.AudioDevice.BLUETOOTH)
+            callActivity.setAudioOutputChannel(WebRtcAudioManger.AudioDevice.BLUETOOTH)
             dismiss()
         }
 
         dialogAudioOutputBinding.audioOutputSpeaker.setOnClickListener {
-            callActivity.setAudioOutputChannel(MagicAudioManager.AudioDevice.SPEAKER_PHONE)
+            callActivity.setAudioOutputChannel(WebRtcAudioManger.AudioDevice.SPEAKER_PHONE)
             dismiss()
         }
 
         dialogAudioOutputBinding.audioOutputEarspeaker.setOnClickListener {
-            callActivity.setAudioOutputChannel(MagicAudioManager.AudioDevice.EARPIECE)
+            callActivity.setAudioOutputChannel(WebRtcAudioManger.AudioDevice.EARPIECE)
             dismiss()
         }
     }

+ 38 - 33
app/src/main/java/com/nextcloud/talk/webrtc/MagicAudioManager.java → app/src/main/java/com/nextcloud/talk/webrtc/WebRtcAudioManger.java

@@ -2,6 +2,8 @@
  * Nextcloud Talk application
  *
  * @author Mario Danic
+ * @author Tim Krüger
+ * Copyright (C) 2022 Tim Krüger <t@timkrueger.me>
  * Copyright (C) 2017 Mario Danic <mario@lovelyhq.com>
  *
  * This program is free software: you can redistribute it and/or modify
@@ -52,15 +54,12 @@ import java.util.Collections;
 import java.util.HashSet;
 import java.util.Set;
 
-/**
- * MagicAudioManager manages all audio related parts of the AppRTC demo.
- */
-public class MagicAudioManager {
-    private static final String TAG = "MagicAudioManager";
+public class WebRtcAudioManger {
+    private static final String TAG = WebRtcAudioManger.class.getCanonicalName();
     private final Context magicContext;
-    private final MagicBluetoothManager bluetoothManager;
-    private boolean useProximitySensor;
-    private AudioManager audioManager;
+    private final WebRtcBluetoothManager bluetoothManager;
+    private final boolean useProximitySensor;
+    private final AudioManager audioManager;
     private AudioManagerListener audioManagerListener;
     private AudioManagerState amState;
     private int savedAudioMode = AudioManager.MODE_INVALID;
@@ -75,17 +74,17 @@ public class MagicAudioManager {
 
     private Set<AudioDevice> audioDevices = new HashSet<>();
 
-    private BroadcastReceiver wiredHeadsetReceiver;
+    private final BroadcastReceiver wiredHeadsetReceiver;
     private AudioManager.OnAudioFocusChangeListener audioFocusChangeListener;
 
-    private PowerManagerUtils powerManagerUtils;
+    private final PowerManagerUtils powerManagerUtils;
 
-    private MagicAudioManager(Context context, boolean useProximitySensor) {
+    private WebRtcAudioManger(Context context, boolean useProximitySensor) {
         Log.d(TAG, "ctor");
         ThreadUtils.checkIsOnMainThread();
         magicContext = context;
         audioManager = ((AudioManager) context.getSystemService(Context.AUDIO_SERVICE));
-        bluetoothManager = MagicBluetoothManager.create(context, this);
+        bluetoothManager = WebRtcBluetoothManager.create(context, this);
         wiredHeadsetReceiver = new WiredHeadsetReceiver();
         amState = AudioManagerState.UNINITIALIZED;
 
@@ -111,8 +110,14 @@ public class MagicAudioManager {
     /**
      * Construction.
      */
-    public static MagicAudioManager create(Context context, boolean useProximitySensor) {
-        return new MagicAudioManager(context, useProximitySensor);
+    public static WebRtcAudioManger create(Context context, boolean useProximitySensor) {
+       return new WebRtcAudioManger(context, useProximitySensor);
+    }
+
+    public void startBluetoothManager() {
+        // Initialize and start Bluetooth if a BT device is available or initiate
+        // detection of new (enabled) BT devices.
+        bluetoothManager.start();
     }
 
     /**
@@ -136,7 +141,7 @@ public class MagicAudioManager {
                                                                        .SENSOR_NEAR, null, null, null, null));
 
             } else {
-                setAudioDeviceInternal(MagicAudioManager.AudioDevice.SPEAKER_PHONE);
+                setAudioDeviceInternal(WebRtcAudioManger.AudioDevice.SPEAKER_PHONE);
                 Log.d(TAG, "switched to SPEAKER_PHONE because userSelectedAudioDevice was SPEAKER_PHONE and proximity=far");
 
                 EventBus.getDefault().post(new PeerConnectionEvent(PeerConnectionEvent.PeerConnectionEventType
@@ -228,9 +233,7 @@ public class MagicAudioManager {
         currentAudioDevice = AudioDevice.NONE;
         audioDevices.clear();
 
-        // Initialize and start Bluetooth if a BT device is available or initiate
-        // detection of new (enabled) BT devices.
-        bluetoothManager.start();
+        startBluetoothManager();
 
         // Do initial selection of audio device. This setting can later be changed
         // either by adding/removing a BT or wired headset or by covering/uncovering
@@ -256,7 +259,9 @@ public class MagicAudioManager {
 
         unregisterReceiver(wiredHeadsetReceiver);
 
-        bluetoothManager.stop();
+        if(bluetoothManager.started()) {
+            bluetoothManager.stop();
+        }
 
         // Restore previously stored audio states.
         setSpeakerphoneOn(savedIsSpeakerPhoneOn);
@@ -411,17 +416,17 @@ public class MagicAudioManager {
             + "current=" + currentAudioDevice + ", "
             + "user selected=" + userSelectedAudioDevice);
 
-        if (bluetoothManager.getState() == MagicBluetoothManager.State.HEADSET_AVAILABLE
-            || bluetoothManager.getState() == MagicBluetoothManager.State.HEADSET_UNAVAILABLE
-            || bluetoothManager.getState() == MagicBluetoothManager.State.SCO_DISCONNECTING) {
+        if (bluetoothManager.getState() == WebRtcBluetoothManager.State.HEADSET_AVAILABLE
+            || bluetoothManager.getState() == WebRtcBluetoothManager.State.HEADSET_UNAVAILABLE
+            || bluetoothManager.getState() == WebRtcBluetoothManager.State.SCO_DISCONNECTING) {
             bluetoothManager.updateDevice();
         }
 
         Set<AudioDevice> newAudioDevices = new HashSet<>();
 
-        if (bluetoothManager.getState() == MagicBluetoothManager.State.SCO_CONNECTED
-            || bluetoothManager.getState() == MagicBluetoothManager.State.SCO_CONNECTING
-            || bluetoothManager.getState() == MagicBluetoothManager.State.HEADSET_AVAILABLE) {
+        if (bluetoothManager.getState() == WebRtcBluetoothManager.State.SCO_CONNECTED
+            || bluetoothManager.getState() == WebRtcBluetoothManager.State.SCO_CONNECTING
+            || bluetoothManager.getState() == WebRtcBluetoothManager.State.HEADSET_AVAILABLE) {
             newAudioDevices.add(AudioDevice.BLUETOOTH);
         }
 
@@ -441,7 +446,7 @@ public class MagicAudioManager {
 
         // Correct user selected audio devices if needed.
         if (userSelectedAudioDevice == AudioDevice.BLUETOOTH
-            && bluetoothManager.getState() == MagicBluetoothManager.State.HEADSET_UNAVAILABLE) {
+            && bluetoothManager.getState() == WebRtcBluetoothManager.State.HEADSET_UNAVAILABLE) {
             userSelectedAudioDevice = AudioDevice.SPEAKER_PHONE;
         }
         if (userSelectedAudioDevice == AudioDevice.SPEAKER_PHONE && hasWiredHeadset) {
@@ -455,21 +460,21 @@ public class MagicAudioManager {
         // Need to start Bluetooth if it is available and user either selected it explicitly or
         // user did not select any output device.
         boolean needBluetoothAudioStart =
-            bluetoothManager.getState() == MagicBluetoothManager.State.HEADSET_AVAILABLE
+            bluetoothManager.getState() == WebRtcBluetoothManager.State.HEADSET_AVAILABLE
                 && (userSelectedAudioDevice == AudioDevice.NONE
                 || userSelectedAudioDevice == AudioDevice.BLUETOOTH);
 
         // Need to stop Bluetooth audio if user selected different device and
         // Bluetooth SCO connection is established or in the process.
         boolean needBluetoothAudioStop =
-            (bluetoothManager.getState() == MagicBluetoothManager.State.SCO_CONNECTED
-                || bluetoothManager.getState() == MagicBluetoothManager.State.SCO_CONNECTING)
+            (bluetoothManager.getState() == WebRtcBluetoothManager.State.SCO_CONNECTED
+                || bluetoothManager.getState() == WebRtcBluetoothManager.State.SCO_CONNECTING)
                 && (userSelectedAudioDevice != AudioDevice.NONE
                 && userSelectedAudioDevice != AudioDevice.BLUETOOTH);
 
-        if (bluetoothManager.getState() == MagicBluetoothManager.State.HEADSET_AVAILABLE
-            || bluetoothManager.getState() == MagicBluetoothManager.State.SCO_CONNECTING
-            || bluetoothManager.getState() == MagicBluetoothManager.State.SCO_CONNECTED) {
+        if (bluetoothManager.getState() == WebRtcBluetoothManager.State.HEADSET_AVAILABLE
+            || bluetoothManager.getState() == WebRtcBluetoothManager.State.SCO_CONNECTING
+            || bluetoothManager.getState() == WebRtcBluetoothManager.State.SCO_CONNECTED) {
             Log.d(TAG, "Need BT audio: start=" + needBluetoothAudioStart + ", "
                 + "stop=" + needBluetoothAudioStop + ", "
                 + "BT state=" + bluetoothManager.getState());
@@ -494,7 +499,7 @@ public class MagicAudioManager {
         // Update selected audio device.
         AudioDevice newCurrentAudioDevice;
 
-        if (bluetoothManager.getState() == MagicBluetoothManager.State.SCO_CONNECTED) {
+        if (bluetoothManager.getState() == WebRtcBluetoothManager.State.SCO_CONNECTED) {
             // If a Bluetooth is connected, then it should be used as output audio
             // device. Note that it is not sufficient that a headset is available;
             // an active SCO channel must also be up and running.

+ 50 - 23
app/src/main/java/com/nextcloud/talk/webrtc/MagicBluetoothManager.java → app/src/main/java/com/nextcloud/talk/webrtc/WebRtcBluetoothManager.java

@@ -2,6 +2,8 @@
  * Nextcloud Talk application
  *
  * @author Mario Danic
+ * @author Tim Krüger
+ * Copyright (C) 2022 Tim Krüger <t@timkrueger.me>
  * Copyright (C) 2017 Mario Danic <mario@lovelyhq.com>
  *
  * This program is free software: you can redistribute it and/or modify
@@ -31,6 +33,7 @@
 
 package com.nextcloud.talk.webrtc;
 
+import android.Manifest;
 import android.annotation.SuppressLint;
 import android.bluetooth.BluetoothAdapter;
 import android.bluetooth.BluetoothDevice;
@@ -47,20 +50,23 @@ import android.os.Handler;
 import android.os.Looper;
 import android.os.Process;
 import android.util.Log;
+
 import org.webrtc.ThreadUtils;
 
 import java.util.List;
 import java.util.Set;
 
-public class MagicBluetoothManager {
-    private static final String TAG = "MagicBluetoothManager";
+import androidx.core.app.ActivityCompat;
+
+public class WebRtcBluetoothManager {
+    private static final String TAG = WebRtcBluetoothManager.class.getCanonicalName();
 
     // Timeout interval for starting or stopping audio to a Bluetooth SCO device.
     private static final int BLUETOOTH_SCO_TIMEOUT_MS = 4000;
     // Maximum number of SCO connection attempts.
     private static final int MAX_SCO_CONNECTION_ATTEMPTS = 2;
     private final Context apprtcContext;
-    private final MagicAudioManager apprtcAudioManager;
+    private final WebRtcAudioManger webRtcAudioManager;
     private final AudioManager audioManager;
     private final Handler handler;
     private final BluetoothProfile.ServiceListener bluetoothServiceListener;
@@ -73,18 +79,14 @@ public class MagicBluetoothManager {
     // Runs when the Bluetooth timeout expires. We use that timeout after calling
     // startScoAudio() or stopScoAudio() because we're not guaranteed to get a
     // callback after those calls.
-    private final Runnable bluetoothTimeoutRunnable = new Runnable() {
-        @Override
-        public void run() {
-            bluetoothTimeout();
-        }
-    };
+    private final Runnable bluetoothTimeoutRunnable = this::bluetoothTimeout;
+    private boolean started = false;
 
-    protected MagicBluetoothManager(Context context, MagicAudioManager audioManager) {
+    protected WebRtcBluetoothManager(Context context, WebRtcAudioManger audioManager) {
         Log.d(TAG, "ctor");
         ThreadUtils.checkIsOnMainThread();
         apprtcContext = context;
-        apprtcAudioManager = audioManager;
+        webRtcAudioManager = audioManager;
         this.audioManager = getAudioManager(context);
         bluetoothState = State.UNINITIALIZED;
         bluetoothServiceListener = new BluetoothServiceListener();
@@ -95,8 +97,8 @@ public class MagicBluetoothManager {
     /**
      * Construction.
      */
-    static MagicBluetoothManager create(Context context, MagicAudioManager audioManager) {
-        return new MagicBluetoothManager(context, audioManager);
+    static WebRtcBluetoothManager create(Context context, WebRtcAudioManger audioManager) {
+        return new WebRtcBluetoothManager(context, audioManager);
     }
 
     /**
@@ -122,11 +124,11 @@ public class MagicBluetoothManager {
      * Note that the MagicAudioManager is also involved in driving this state
      * change.
      */
+    @SuppressLint("MissingPermission")
     public void start() {
         ThreadUtils.checkIsOnMainThread();
         Log.d(TAG, "start");
-        if (!hasPermission(apprtcContext, android.Manifest.permission.BLUETOOTH)) {
-            Log.w(TAG, "Process (pid=" + Process.myPid() + ") lacks BLUETOOTH permission");
+        if(hasNoBluetoothPermission()){
             return;
         }
         if (bluetoothState != State.UNINITIALIZED) {
@@ -166,6 +168,7 @@ public class MagicBluetoothManager {
                 + stateToString(bluetoothAdapter.getProfileConnectionState(BluetoothProfile.HEADSET)));
         Log.d(TAG, "Bluetooth proxy for headset profile has started");
         bluetoothState = State.HEADSET_UNAVAILABLE;
+        started = true;
         Log.d(TAG, "start done: BT state=" + bluetoothState);
     }
 
@@ -262,8 +265,12 @@ public class MagicBluetoothManager {
      * HEADSET_AVAILABLE and |bluetoothDevice| will be mapped to the connected
      * device if available.
      */
+    @SuppressLint("MissingPermission")
     public void updateDevice() {
-        if (bluetoothState == State.UNINITIALIZED || bluetoothHeadset == null) {
+        boolean hasNoBluetoothPermissions = hasNoBluetoothPermission();
+        if (hasNoBluetoothPermissions ||
+            bluetoothState == State.UNINITIALIZED ||
+            bluetoothHeadset == null) {
             return;
         }
         Log.d(TAG, "updateDevice");
@@ -307,16 +314,28 @@ public class MagicBluetoothManager {
         return bluetoothAdapter.getProfileProxy(context, listener, profile);
     }
 
-    protected boolean hasPermission(Context context, String permission) {
-        return apprtcContext.checkPermission(permission, Process.myPid(), Process.myUid())
-                == PackageManager.PERMISSION_GRANTED;
+    private boolean hasNoBluetoothPermission() {
+        String permission;
+
+        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) {
+            permission = Manifest.permission.BLUETOOTH_CONNECT;
+        } else {
+            permission = Manifest.permission.BLUETOOTH;
+        }
+
+        boolean hasPermission =
+            ActivityCompat.checkSelfPermission(apprtcContext, permission) == PackageManager.PERMISSION_GRANTED;
+        if(!hasPermission) {
+            Log.w(TAG, "Process (pid=" + Process.myPid() + ") lacks \"" + permission + "\" permission");
+        }
+        return !hasPermission;
     }
 
     /**
      * Logs the state of the local Bluetooth adapter.
      */
-    @SuppressLint("HardwareIds")
-    protected void logBluetoothAdapterInfo(BluetoothAdapter localAdapter) {
+    @SuppressLint({"HardwareIds", "MissingPermission"})
+    private void logBluetoothAdapterInfo(BluetoothAdapter localAdapter) {
         Log.d(TAG, "BluetoothAdapter: "
             + "enabled=" + localAdapter.isEnabled() + ", "
             + "state=" + stateToString(localAdapter.getState()) + ", "
@@ -337,7 +356,7 @@ public class MagicBluetoothManager {
     private void updateAudioDeviceState() {
         ThreadUtils.checkIsOnMainThread();
         Log.d(TAG, "updateAudioDeviceState");
-        apprtcAudioManager.updateAudioDeviceState();
+        webRtcAudioManager.updateAudioDeviceState();
     }
 
     /**
@@ -362,9 +381,13 @@ public class MagicBluetoothManager {
      * Called when start of the BT SCO channel takes too long time. Usually
      * happens when the BT device has been turned on during an ongoing call.
      */
+    @SuppressLint("MissingPermission")
     private void bluetoothTimeout() {
         ThreadUtils.checkIsOnMainThread();
-        if (bluetoothState == State.UNINITIALIZED || bluetoothHeadset == null) {
+        boolean hasNoBluetoothPermissions = hasNoBluetoothPermission();
+        if (hasNoBluetoothPermissions ||
+            bluetoothState == State.UNINITIALIZED ||
+            bluetoothHeadset == null) {
             return;
         }
         Log.d(TAG, "bluetoothTimeout: BT state=" + bluetoothState + ", "
@@ -435,6 +458,10 @@ public class MagicBluetoothManager {
         }
     }
 
+    public boolean started() {
+        return started;
+    }
+
     // Bluetooth connection state.
     public enum State {
         // Bluetooth is not available; no adapter or Bluetooth is off.

+ 1 - 1
scripts/analysis/lint-results.txt

@@ -1,2 +1,2 @@
 DO NOT TOUCH; GENERATED BY DRONE
-      <span class="mdl-layout-title">Lint Report: 2 errors and 123 warnings</span>
+      <span class="mdl-layout-title">Lint Report: 1 error and 117 warnings</span>