diff --git a/app/build.gradle b/app/build.gradle index b7d7d278c..c8ec880b5 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -239,7 +239,6 @@ dependencies { implementation 'net.orange-box.storebox:storebox-lib:1.4.0' implementation 'eu.davidea:flexible-adapter:5.1.0' implementation 'eu.davidea:flexible-adapter-ui:1.0.0' - implementation 'me.zhanghai.android.effortlesspermissions:library:1.1.0' implementation 'org.apache.commons:commons-lang3:3.12.0' implementation 'com.github.wooplr:Spotlight:1.3' implementation 'com.google.code.findbugs:jsr305:3.0.2' diff --git a/app/src/main/java/com/nextcloud/talk/activities/CallActivity.java b/app/src/main/java/com/nextcloud/talk/activities/CallActivity.java index fc402088e..f1e902656 100644 --- a/app/src/main/java/com/nextcloud/talk/activities/CallActivity.java +++ b/app/src/main/java/com/nextcloud/talk/activities/CallActivity.java @@ -34,7 +34,6 @@ 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; @@ -151,12 +150,9 @@ 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.AlertDialog; -import androidx.appcompat.app.AppCompatActivity; -import androidx.core.content.ContextCompat; import androidx.core.graphics.drawable.DrawableCompat; import androidx.lifecycle.ViewModelProvider; import autodagger.AutoInjector; @@ -165,11 +161,7 @@ import io.reactivex.Observer; import io.reactivex.android.schedulers.AndroidSchedulers; import io.reactivex.disposables.Disposable; import io.reactivex.schedulers.Schedulers; -import me.zhanghai.android.effortlesspermissions.AfterPermissionDenied; -import me.zhanghai.android.effortlesspermissions.EffortlessPermissions; -import me.zhanghai.android.effortlesspermissions.OpenAppDetailsDialogFragment; import okhttp3.Cache; -import pub.devrel.easypermissions.AfterPermissionGranted; import static android.app.PendingIntent.FLAG_IMMUTABLE; import static com.nextcloud.talk.utils.bundle.BundleKeys.KEY_CALL_VOICE_ONLY; @@ -220,11 +212,6 @@ public class CallActivity extends CallBaseActivity { public CallRecordingViewModel callRecordingViewModel; public RaiseHandViewModel raiseHandViewModel; - private static final String[] PERMISSIONS_CALL = { - Manifest.permission.CAMERA, - Manifest.permission.RECORD_AUDIO - }; - private static final String[] PERMISSIONS_CAMERA = { Manifest.permission.CAMERA }; @@ -362,13 +349,62 @@ public class CallActivity extends CallBaseActivity { private AudioOutputDialog audioOutputDialog; private MoreCallActionsDialog moreCallActionsDialog; - private final ActivityResultLauncher requestBluetoothPermissionLauncher = - registerForActivityResult(new ActivityResultContracts.RequestPermission(), isGranted -> { - if (isGranted) { - enableBluetoothManager(); + ActivityResultLauncher requestPermissionLauncher = + registerForActivityResult(new ActivityResultContracts.RequestMultiplePermissions(), permissionMap -> { + + List rationaleList = new ArrayList<>(); + + Boolean audioPermission = permissionMap.get(Manifest.permission.RECORD_AUDIO); + if (audioPermission != null) { + if (Boolean.TRUE.equals(audioPermission)) { + if (!microphoneOn) { + onMicrophoneClick(); + } + } else { + rationaleList.add((getResources().getString(R.string.nc_microphone_permission_hint))); + } + } + + Boolean cameraPermission = permissionMap.get(Manifest.permission.CAMERA); + if (cameraPermission != null) { + if (Boolean.TRUE.equals(cameraPermission)) { + if (!videoOn) { + onCameraClick(); + } + + if (cameraEnumerator.getDeviceNames().length == 0) { + binding.cameraButton.setVisibility(View.GONE); + } + + if (cameraEnumerator.getDeviceNames().length > 1) { + binding.switchSelfVideoButton.setVisibility(View.VISIBLE); + } + } else { + rationaleList.add((getResources().getString(R.string.nc_camera_permission_hint))); + } + } + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + Boolean bluetoothPermission = permissionMap.get(Manifest.permission.BLUETOOTH_CONNECT); + if (bluetoothPermission != null) { + if (Boolean.TRUE.equals(bluetoothPermission)) { + enableBluetoothManager(); + } else { + // Only ask for bluetooth when already asking to grant microphone or camera access. Asking + // for bluetooth solely is not important enough here and would most likely annoy the user. + if (!rationaleList.isEmpty()) { + rationaleList.add((getResources().getString(R.string.nc_bluetooth_permission_hint))); + } + } + } + } + + if (!rationaleList.isEmpty()) { + showRationaleDialogForSettings(rationaleList); } }); + private boolean canPublishAudioStream; private boolean canPublishVideoStream; @@ -502,16 +538,15 @@ public class CallActivity extends CallBaseActivity { .setRepeatCount(PulseAnimation.INFINITE) .setRepeatMode(PulseAnimation.REVERSE); - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { - requestBluetoothPermission(); - } basicInitialization(); callParticipants = new HashMap<>(); participantDisplayItems = new HashMap<>(); initViews(); + if (!isConnectionEstablished()) { initiateCall(); } + updateSelfVideoViewPosition(); reactionAnimator = new ReactionAnimator(context, binding.reactionAnimationWrapper, viewThemeUtils); @@ -546,15 +581,6 @@ public class CallActivity extends CallBaseActivity { active = false; } - @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(); @@ -936,36 +962,28 @@ public class CallActivity extends CallBaseActivity { } } - private void checkDevicePermissions() { - if (isVoiceOnlyCall) { - onMicrophoneClick(); - } else { - if (EffortlessPermissions.hasPermissions(this, PERMISSIONS_CALL)) { - onPermissionsGranted(); - } else { - requestPermissions(PERMISSIONS_CALL, 100); - } - } - - } - - private boolean isConnectionEstablished() { - return (currentCallStatus == CallStatus.JOINED || currentCallStatus == CallStatus.IN_CONVERSATION); - } - - @AfterPermissionGranted(100) - private void onPermissionsGranted() { - if (EffortlessPermissions.hasPermissions(this, PERMISSIONS_CALL)) { - if (!videoOn && !isVoiceOnlyCall) { - onCameraClick(); - } + List permissionsToRequest = new ArrayList<>(); + List rationaleList = new ArrayList<>(); + if (permissionUtil.isMicrophonePermissionGranted()) { if (!microphoneOn) { onMicrophoneClick(); } - if (!isVoiceOnlyCall) { + } else if (shouldShowRequestPermissionRationale(Manifest.permission.RECORD_AUDIO)) { + permissionsToRequest.add(Manifest.permission.RECORD_AUDIO); + rationaleList.add((getResources().getString(R.string.nc_microphone_permission_hint))); + } else { + permissionsToRequest.add(Manifest.permission.RECORD_AUDIO); + } + + if (!isVoiceOnlyCall) { + if (permissionUtil.isCameraPermissionGranted()) { + if (!videoOn) { + onCameraClick(); + } + if (cameraEnumerator.getDeviceNames().length == 0) { binding.cameraButton.setVisibility(View.GONE); } @@ -973,44 +991,32 @@ public class CallActivity extends CallBaseActivity { if (cameraEnumerator.getDeviceNames().length > 1) { binding.switchSelfVideoButton.setVisibility(View.VISIBLE); } - } - - if (!isConnectionEstablished()) { - fetchSignalingSettings(); - } - } else if (EffortlessPermissions.somePermissionPermanentlyDenied(this, PERMISSIONS_CALL)) { - checkIfSomeAreApproved(); - } - - } - - private void checkIfSomeAreApproved() { - if (!isVoiceOnlyCall) { - if (cameraEnumerator.getDeviceNames().length == 0) { - binding.cameraButton.setVisibility(View.GONE); - } - - if (cameraEnumerator.getDeviceNames().length > 1) { - binding.switchSelfVideoButton.setVisibility(View.VISIBLE); - } - - if (EffortlessPermissions.hasPermissions(this, PERMISSIONS_CAMERA) && canPublishVideoStream) { - if (!videoOn) { - onCameraClick(); - } + } else if (shouldShowRequestPermissionRationale(Manifest.permission.CAMERA)) { + permissionsToRequest.add(Manifest.permission.CAMERA); + rationaleList.add((getResources().getString(R.string.nc_camera_permission_hint))); } else { - binding.cameraButton.setImageResource(R.drawable.ic_videocam_off_white_24px); - binding.cameraButton.setAlpha(0.7f); - binding.switchSelfVideoButton.setVisibility(View.GONE); + permissionsToRequest.add(Manifest.permission.CAMERA); } } - if (EffortlessPermissions.hasPermissions(this, PERMISSIONS_MICROPHONE) && canPublishAudioStream) { - if (!microphoneOn) { - onMicrophoneClick(); + + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { + if (permissionUtil.isBluetoothPermissionGranted()) { + enableBluetoothManager(); + } else if (shouldShowRequestPermissionRationale(Manifest.permission.BLUETOOTH_CONNECT)) { + permissionsToRequest.add(Manifest.permission.BLUETOOTH_CONNECT); + rationaleList.add((getResources().getString(R.string.nc_bluetooth_permission_hint))); + } else { + permissionsToRequest.add(Manifest.permission.BLUETOOTH_CONNECT); + } + } + + if (!permissionsToRequest.isEmpty()) { + if (!rationaleList.isEmpty()) { + showRationaleDialog(permissionsToRequest, rationaleList); + } else { + requestPermissionLauncher.launch(permissionsToRequest.toArray(new String[permissionsToRequest.size()])); } - } else { - binding.microphoneButton.setImageResource(R.drawable.ic_mic_off_white_24px); } if (!isConnectionEstablished()) { @@ -1018,22 +1024,63 @@ public class CallActivity extends CallBaseActivity { } } - @AfterPermissionDenied(100) - private void onPermissionsDenied() { - if (!isVoiceOnlyCall) { - if (cameraEnumerator.getDeviceNames().length == 0) { - binding.cameraButton.setVisibility(View.GONE); - } else if (cameraEnumerator.getDeviceNames().length == 1) { - binding.switchSelfVideoButton.setVisibility(View.GONE); - } + private void showRationaleDialog(String permissionToRequest, String rationale) { + List rationaleList = new ArrayList(); + List permissionsToRequest = new ArrayList(); + + rationaleList.add(rationale); + permissionsToRequest.add(permissionToRequest); + + showRationaleDialog(permissionsToRequest, rationaleList); + } + + private void showRationaleDialog(List permissionsToRequest, List rationaleList) { + StringBuilder rationalesWithLineBreaks = new StringBuilder(); + + for (String rationale : rationaleList) { + rationalesWithLineBreaks.append(rationale).append("\n\n"); } - if ((EffortlessPermissions.hasPermissions(this, PERMISSIONS_CAMERA) || - EffortlessPermissions.hasPermissions(this, PERMISSIONS_MICROPHONE))) { - checkIfSomeAreApproved(); - } else if (!isConnectionEstablished()) { - fetchSignalingSettings(); + MaterialAlertDialogBuilder dialogBuilder = new MaterialAlertDialogBuilder(this) + .setTitle(R.string.nc_permissions_rationale_dialog_title) + .setMessage(rationalesWithLineBreaks) + .setPositiveButton(R.string.nc_permissions_ask, (dialog, which) -> + requestPermissionLauncher.launch( + permissionsToRequest.toArray(new String[permissionsToRequest.size()]) + ) + ) + .setNegativeButton(R.string.nc_common_dismiss, null); + viewThemeUtils.dialog.colorMaterialAlertDialogBackground(this, dialogBuilder); + dialogBuilder.show(); + } + + private void showRationaleDialogForSettings(List rationaleList) { + StringBuilder rationalesWithLineBreaks = new StringBuilder(); + rationalesWithLineBreaks.append(getResources().getString(R.string.nc_permissions_denied)); + rationalesWithLineBreaks.append('\n'); + rationalesWithLineBreaks.append(getResources().getString(R.string.nc_permissions_settings_hint)); + rationalesWithLineBreaks.append("\n\n"); + + for (String rationale : rationaleList) { + rationalesWithLineBreaks.append(rationale).append("\n\n"); } + + MaterialAlertDialogBuilder dialogBuilder = new MaterialAlertDialogBuilder(this) + .setTitle(R.string.nc_permissions_rationale_dialog_title) + .setMessage(rationalesWithLineBreaks) + .setPositiveButton(R.string.nc_permissions_settings, (dialog, which) -> { + Intent intent = new Intent(android.provider.Settings.ACTION_APPLICATION_DETAILS_SETTINGS); + intent.setData(Uri.fromParts("package", getPackageName(), null)); + startActivity(intent); + }) + .setNegativeButton(R.string.nc_common_dismiss, null); + viewThemeUtils.dialog.colorMaterialAlertDialogBackground(this, dialogBuilder); + dialogBuilder.show(); + } + + + private boolean isConnectionEstablished() { + return (currentCallStatus == CallStatus.JOINED || currentCallStatus == CallStatus.IN_CONVERSATION); } private void onAudioManagerDevicesChanged( @@ -1118,7 +1165,6 @@ public class CallActivity extends CallBaseActivity { } public void onMicrophoneClick() { - if (!canPublishAudioStream) { microphoneOn = false; binding.microphoneButton.setImageResource(R.drawable.ic_mic_off_white_24px); @@ -1134,7 +1180,7 @@ public class CallActivity extends CallBaseActivity { return; } - if (EffortlessPermissions.hasPermissions(this, PERMISSIONS_MICROPHONE)) { + if (permissionUtil.isMicrophonePermissionGranted()) { if (!appPreferences.getPushToTalkIntroShown()) { int primary = viewThemeUtils.getScheme(binding.audioOutputButton.getContext()).getPrimary(); @@ -1182,19 +1228,17 @@ public class CallActivity extends CallBaseActivity { pulseAnimation.start(); toggleMedia(true, false); } - } else if (EffortlessPermissions.somePermissionPermanentlyDenied(this, PERMISSIONS_MICROPHONE)) { - // Microphone permission is permanently denied so we cannot request it normally. - - OpenAppDetailsDialogFragment.show( - R.string.nc_microphone_permission_permanently_denied, - R.string.nc_permissions_settings, (AppCompatActivity) this); + } else if (shouldShowRequestPermissionRationale(Manifest.permission.RECORD_AUDIO)) { + showRationaleDialog( + Manifest.permission.RECORD_AUDIO, + getResources().getString(R.string.nc_microphone_permission_hint) + ); } else { - requestPermissions(PERMISSIONS_MICROPHONE, 100); + requestPermissionLauncher.launch(PERMISSIONS_MICROPHONE); } } public void onCameraClick() { - if (!canPublishVideoStream) { videoOn = false; binding.cameraButton.setImageResource(R.drawable.ic_videocam_off_white_24px); @@ -1202,7 +1246,7 @@ public class CallActivity extends CallBaseActivity { return; } - if (EffortlessPermissions.hasPermissions(this, PERMISSIONS_CAMERA)) { + if (permissionUtil.isCameraPermissionGranted()) { videoOn = !videoOn; if (videoOn) { @@ -1216,15 +1260,14 @@ public class CallActivity extends CallBaseActivity { } toggleMedia(videoOn, true); - } else if (EffortlessPermissions.somePermissionPermanentlyDenied(this, PERMISSIONS_CAMERA)) { - // Camera permission is permanently denied so we cannot request it normally. - OpenAppDetailsDialogFragment.show( - R.string.nc_camera_permission_permanently_denied, - R.string.nc_permissions_settings, (AppCompatActivity) this); + } else if (shouldShowRequestPermissionRationale(Manifest.permission.CAMERA)) { + showRationaleDialog( + Manifest.permission.CAMERA, + getResources().getString(R.string.nc_camera_permission_hint) + ); } else { - requestPermissions(PERMISSIONS_CAMERA, 100); + requestPermissionLauncher.launch(PERMISSIONS_CAMERA); } - } public void switchCamera() { @@ -2411,7 +2454,7 @@ public class CallActivity extends CallBaseActivity { if (!isVoiceOnlyCall) { boolean enableVideo = proximitySensorEvent.getProximitySensorEventType() == ProximitySensorEvent.ProximitySensorEventType.SENSOR_FAR && videoOn; - if (EffortlessPermissions.hasPermissions(this, PERMISSIONS_CAMERA) && + if (permissionUtil.isCameraPermissionGranted() && (currentCallStatus == CallStatus.CONNECTING || isConnectionEstablished()) && videoOn && enableVideo != localVideoTrack.enabled()) { toggleMedia(enableVideo, true); @@ -2459,15 +2502,6 @@ public class CallActivity extends CallBaseActivity { } } - @Override - public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, - @NonNull int[] grantResults) { - super.onRequestPermissionsResult(requestCode, permissions, grantResults); - - EffortlessPermissions.onRequestPermissionsResult(requestCode, permissions, grantResults, - this); - } - private void addParticipantDisplayItem(CallParticipantModel callParticipantModel, String videoStreamType) { if (callParticipantModel.isInternal() != null && callParticipantModel.isInternal()) { return; diff --git a/app/src/main/java/com/nextcloud/talk/utils/permissions/PlatformPermissionUtil.kt b/app/src/main/java/com/nextcloud/talk/utils/permissions/PlatformPermissionUtil.kt index 0acc73f19..a288de610 100644 --- a/app/src/main/java/com/nextcloud/talk/utils/permissions/PlatformPermissionUtil.kt +++ b/app/src/main/java/com/nextcloud/talk/utils/permissions/PlatformPermissionUtil.kt @@ -24,6 +24,7 @@ package com.nextcloud.talk.utils.permissions interface PlatformPermissionUtil { val privateBroadcastPermission: String fun isCameraPermissionGranted(): Boolean - + fun isMicrophonePermissionGranted(): Boolean + fun isBluetoothPermissionGranted(): Boolean fun isFilesPermissionGranted(): Boolean } diff --git a/app/src/main/java/com/nextcloud/talk/utils/permissions/PlatformPermissionUtilImpl.kt b/app/src/main/java/com/nextcloud/talk/utils/permissions/PlatformPermissionUtilImpl.kt index 3a358f3dc..cdfb232d1 100644 --- a/app/src/main/java/com/nextcloud/talk/utils/permissions/PlatformPermissionUtilImpl.kt +++ b/app/src/main/java/com/nextcloud/talk/utils/permissions/PlatformPermissionUtilImpl.kt @@ -25,6 +25,7 @@ import android.Manifest import android.content.Context import android.os.Build import android.util.Log +import androidx.annotation.RequiresApi import androidx.core.content.PermissionChecker import com.nextcloud.talk.BuildConfig @@ -39,6 +40,21 @@ class PlatformPermissionUtilImpl(private val context: Context) : PlatformPermiss ) == PermissionChecker.PERMISSION_GRANTED } + @RequiresApi(Build.VERSION_CODES.S) + override fun isBluetoothPermissionGranted(): Boolean { + return PermissionChecker.checkSelfPermission( + context, + Manifest.permission.BLUETOOTH_CONNECT + ) == PermissionChecker.PERMISSION_GRANTED + } + + override fun isMicrophonePermissionGranted(): Boolean { + return PermissionChecker.checkSelfPermission( + context, + Manifest.permission.RECORD_AUDIO + ) == PermissionChecker.PERMISSION_GRANTED + } + override fun isFilesPermissionGranted(): Boolean { return when { Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU -> { diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 38c78a60e..bc7d62c76 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -28,7 +28,7 @@ How to work with translations as a developer: - Only add new translations to values/stings.xml, don't do this for other languages (it will be done via transifex). - Never change the key of a translation. If it really has to be changed, delete it and create a new entry instead. -- If you change a value in values/stings.xml, also backport this to all stable branches. +- If you change a value in values/strings.xml, also backport this to all stable branches. - Translations are synced every night (or when manually triggered): - New entries are added to transifex.com - Updated translations from transifex are added via Nextcloud bot to this repo. @@ -216,9 +216,14 @@ How to translate with transifex: Done - To enable video communication please grant \"Camera\" permission in the system settings. - To enable voice communication please grant \"Microphone\" permission in the system settings. + Please allow permissions + Some permissions were denied. + Please grant permissions at Settings > Permissions Open settings + Set permissions + To enable video communication please grant \"Camera\" permission. + To enable voice communication please grant \"Microphone\" permission. + To enable bluetooth speakers please grant \"Nearby devices\" permission. %s voice call