diff --git a/app/src/main/java/org/thoughtcrime/securesms/WebRtcCallActivity.java b/app/src/main/java/org/thoughtcrime/securesms/WebRtcCallActivity.java index 35e2f3c1b7..04d0b34f29 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/WebRtcCallActivity.java +++ b/app/src/main/java/org/thoughtcrime/securesms/WebRtcCallActivity.java @@ -29,13 +29,14 @@ import android.media.AudioManager; import android.os.Build; import android.os.Bundle; import android.util.Rational; +import android.view.View; import android.view.Window; import android.view.WindowManager; +import android.widget.Toast; import androidx.annotation.NonNull; import androidx.annotation.RequiresApi; import androidx.appcompat.app.AppCompatDelegate; -import androidx.core.app.PictureInPictureModeChangedInfo; import androidx.core.content.ContextCompat; import androidx.core.util.Consumer; import androidx.lifecycle.LiveDataReactiveStreams; @@ -116,6 +117,7 @@ import io.reactivex.rxjava3.core.BackpressureStrategy; import io.reactivex.rxjava3.disposables.Disposable; import static org.thoughtcrime.securesms.components.sensors.Orientation.PORTRAIT_BOTTOM_EDGE; +import static org.thoughtcrime.securesms.permissions.PermissionDeniedBottomSheet.showPermissionFragment; public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChangeDialog.Callback, ReactWithAnyEmojiBottomSheetDialogFragment.Callback { @@ -165,7 +167,7 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan private boolean enterPipOnResume; private long lastProcessedIntentTimestamp; private WebRtcViewModel previousEvent = null; - + private boolean isAskingForPermission; private Disposable ephemeralStateDisposable = Disposable.empty(); @Override @@ -237,6 +239,12 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan initializePendingParticipantFragmentListener(); WindowUtil.setNavigationBarColor(this, ContextCompat.getColor(this, R.color.signal_dark_colorSurface)); + + if (!hasCameraPermission() & !hasAudioPermission()) { + askCameraAudioPermissions(() -> handleSetMuteVideo(false)); + } else if (!hasAudioPermission()) { + askAudioPermissions(() -> {}); + } } private void registerSystemPipChangeListeners() { @@ -299,7 +307,7 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan Log.i(TAG, "onPause"); super.onPause(); - if (!viewModel.isCallStarting()) { + if (!isAskingForPermission && !viewModel.isCallStarting()) { CallParticipantsState state = viewModel.getCallParticipantsStateSnapshot(); if (state != null && state.getCallState().isPreJoinOrNetworkUnavailable()) { finish(); @@ -666,15 +674,8 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan Recipient recipient = viewModel.getRecipient().get(); if (!recipient.equals(Recipient.UNKNOWN)) { - String recipientDisplayName = recipient.getDisplayName(this); - - Permissions.with(this) - .request(Manifest.permission.CAMERA) - .ifNecessary() - .withRationaleDialog(getString(R.string.WebRtcCallActivity__to_call_s_signal_needs_access_to_your_camera, recipientDisplayName), R.drawable.ic_video_solid_24_tinted) - .withPermanentDenialDialog(getString(R.string.WebRtcCallActivity__to_call_s_signal_needs_access_to_your_camera, recipientDisplayName)) - .onAllGranted(() -> ApplicationDependencies.getSignalCallManager().setEnableVideo(!muted)) - .execute(); + Runnable onGranted = () -> ApplicationDependencies.getSignalCallManager().setEnableVideo(!muted); + askCameraPermissions(onGranted); } } @@ -683,36 +684,26 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan } private void handleAnswerWithAudio() { - Permissions.with(this) - .request(Manifest.permission.RECORD_AUDIO) - .ifNecessary() - .withRationaleDialog(getString(R.string.WebRtcCallActivity_to_answer_the_call_give_signal_access_to_your_microphone), - R.drawable.ic_mic_solid_24) - .withPermanentDenialDialog(getString(R.string.WebRtcCallActivity_signal_requires_microphone_and_camera_permissions_in_order_to_make_or_receive_calls)) - .onAllGranted(() -> { - callScreen.setStatus(getString(R.string.RedPhone_answering)); - - ApplicationDependencies.getSignalCallManager().acceptCall(false); - }) - .onAnyDenied(this::handleDenyCall) - .execute(); + Runnable onGranted = () -> { + callScreen.setStatus(getString(R.string.RedPhone_answering)); + ApplicationDependencies.getSignalCallManager().acceptCall(false); + }; + askAudioPermissions(onGranted); } private void handleAnswerWithVideo() { - Permissions.with(this) - .request(Manifest.permission.RECORD_AUDIO, Manifest.permission.CAMERA) - .ifNecessary() - .withRationaleDialog(getString(R.string.WebRtcCallActivity_to_answer_the_call_give_signal_access_to_your_microphone_and_camera), R.drawable.ic_mic_solid_24, R.drawable.ic_video_solid_24_tinted) - .withPermanentDenialDialog(getString(R.string.WebRtcCallActivity_signal_requires_microphone_and_camera_permissions_in_order_to_make_or_receive_calls)) - .onAllGranted(() -> { - callScreen.setStatus(getString(R.string.RedPhone_answering)); - - ApplicationDependencies.getSignalCallManager().acceptCall(true); - - handleSetMuteVideo(false); - }) - .onAnyDenied(this::handleDenyCall) - .execute(); + Runnable onGranted = () -> { + callScreen.setStatus(getString(R.string.RedPhone_answering)); + ApplicationDependencies.getSignalCallManager().acceptCall(true); + handleSetMuteVideo(false); + }; + if (!hasCameraPermission() &!hasAudioPermission()) { + askCameraAudioPermissions(onGranted); + } else if (!hasAudioPermission()) { + askAudioPermissions(onGranted); + } else { + askCameraPermissions(onGranted); + } } private void handleDenyCall() { @@ -996,6 +987,85 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan } } + private boolean hasCameraPermission() { + return Permissions.hasAll(this, Manifest.permission.CAMERA); + } + + private boolean hasAudioPermission() { + return Permissions.hasAll(this, Manifest.permission.RECORD_AUDIO); + } + + private void askCameraPermissions(@NonNull Runnable onGranted) { + if (!isAskingForPermission) { + isAskingForPermission = true; + Permissions.with(this) + .request(Manifest.permission.CAMERA) + .ifNecessary() + .withRationaleDialog(getString(R.string.WebRtcCallActivity__allow_access_camera), getString(R.string.WebRtcCallActivity__to_enable_video_allow_camera), false, R.drawable.symbol_video_24) + .onAnyResult(() -> isAskingForPermission = false) + .onAllGranted(() -> { + onGranted.run(); + findViewById(R.id.missing_permissions_container).setVisibility(View.GONE); + }) + .onAnyDenied(() -> Toast.makeText(this, R.string.WebRtcCallActivity__signal_needs_camera_access_enable_video, Toast.LENGTH_LONG).show()) + .onAnyPermanentlyDenied(() -> showPermissionFragment(R.string.WebRtcCallActivity__allow_access_camera, R.string.WebRtcCallActivity__to_enable_video).show(getSupportFragmentManager(), BottomSheetUtil.STANDARD_BOTTOM_SHEET_FRAGMENT_TAG)) + .execute(); + } + } + + private void askAudioPermissions(@NonNull Runnable onGranted) { + if (!isAskingForPermission) { + isAskingForPermission = true; + Permissions.with(this) + .request(Manifest.permission.RECORD_AUDIO) + .ifNecessary() + .withRationaleDialog(getString(R.string.WebRtcCallActivity__allow_access_microphone), getString(R.string.WebRtcCallActivity__to_start_call_microphone), false, R.drawable.ic_mic_24) + .onAnyResult(() -> isAskingForPermission = false) + .onAllGranted(onGranted) + .onAnyDenied(() -> { + Toast.makeText(this, R.string.WebRtcCallActivity__signal_needs_microphone_start_call, Toast.LENGTH_LONG).show(); + handleDenyCall(); + }) + .onAnyPermanentlyDenied(() -> showPermissionFragment(R.string.WebRtcCallActivity__allow_access_microphone, R.string.WebRtcCallActivity__to_start_call).show(getSupportFragmentManager(), BottomSheetUtil.STANDARD_BOTTOM_SHEET_FRAGMENT_TAG)) + .execute(); + } + } + + public void askCameraAudioPermissions(@NonNull Runnable onGranted) { + if (!isAskingForPermission) { + isAskingForPermission = true; + Permissions.with(this) + .request(Manifest.permission.RECORD_AUDIO, Manifest.permission.CAMERA) + .ifNecessary() + .withRationaleDialog(getString(R.string.WebRtcCallActivity__allow_access_camera_microphone), getString(R.string.WebRtcCallActivity__to_start_call_camera_microphone), false, R.drawable.ic_mic_24, R.drawable.symbol_video_24) + .onAnyResult(() -> isAskingForPermission = false) + .onSomePermanentlyDenied(deniedPermissions -> { + if (deniedPermissions.containsAll(List.of(Manifest.permission.CAMERA, Manifest.permission.RECORD_AUDIO))) { + showPermissionFragment(R.string.WebRtcCallActivity__allow_access_camera_microphone, R.string.WebRtcCallActivity__to_start_call).show(getSupportFragmentManager(), BottomSheetUtil.STANDARD_BOTTOM_SHEET_FRAGMENT_TAG); + } else if (deniedPermissions.contains(Manifest.permission.CAMERA)) { + showPermissionFragment(R.string.WebRtcCallActivity__allow_access_camera, R.string.WebRtcCallActivity__to_enable_video).show(getSupportFragmentManager(), BottomSheetUtil.STANDARD_BOTTOM_SHEET_FRAGMENT_TAG); + } else { + showPermissionFragment(R.string.WebRtcCallActivity__allow_access_microphone, R.string.WebRtcCallActivity__to_start_call).show(getSupportFragmentManager(), BottomSheetUtil.STANDARD_BOTTOM_SHEET_FRAGMENT_TAG); + } + }) + .onAllGranted(onGranted) + .onSomeGranted(permissions -> { + if (permissions.contains(Manifest.permission.CAMERA)) { + findViewById(R.id.missing_permissions_container).setVisibility(View.GONE); + } + }) + .onSomeDenied(deniedPermissions -> { + if (deniedPermissions.contains(Manifest.permission.RECORD_AUDIO)) { + Toast.makeText(this, R.string.WebRtcCallActivity__signal_needs_microphone_start_call, Toast.LENGTH_LONG).show(); + handleDenyCall(); + } else { + Toast.makeText(this, R.string.WebRtcCallActivity__signal_needs_camera_access_enable_video, Toast.LENGTH_LONG).show(); + } + }) + .execute(); + } + } + private void startCall(boolean isVideoCall) { enableVideoIfAvailable = isVideoCall; @@ -1037,6 +1107,11 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan } } + @Override + public void onAudioPermissionsRequested(Runnable onGranted) { + askAudioPermissions(onGranted); + } + @Override public void onAudioOutputChanged(@NonNull WebRtcAudioOutput audioOutput) { maybeDisplaySpeakerphonePopup(audioOutput); @@ -1072,9 +1147,12 @@ public class WebRtcCallActivity extends BaseActivity implements SafetyNumberChan @Override public void onMicChanged(boolean isMicEnabled) { - callStateUpdatePopupWindow.onCallStateUpdate(isMicEnabled ? CallStateUpdatePopupWindow.CallStateUpdate.MIC_ON - : CallStateUpdatePopupWindow.CallStateUpdate.MIC_OFF); - handleSetMuteAudio(!isMicEnabled); + Runnable onGranted = () -> { + callStateUpdatePopupWindow.onCallStateUpdate(isMicEnabled ? CallStateUpdatePopupWindow.CallStateUpdate.MIC_ON + : CallStateUpdatePopupWindow.CallStateUpdate.MIC_OFF); + handleSetMuteAudio(!isMicEnabled); + }; + askAudioPermissions(onGranted); } @Override diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/WebRtcCallView.java b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/WebRtcCallView.java index ea190d0996..be28cefbf0 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/WebRtcCallView.java +++ b/app/src/main/java/org/thoughtcrime/securesms/components/webrtc/WebRtcCallView.java @@ -5,6 +5,7 @@ package org.thoughtcrime.securesms.components.webrtc; +import android.Manifest; import android.content.Context; import android.graphics.ColorMatrix; import android.graphics.ColorMatrixColorFilter; @@ -54,6 +55,7 @@ import org.thoughtcrime.securesms.contacts.avatars.ProfileContactPhoto; import org.thoughtcrime.securesms.events.CallParticipant; import org.thoughtcrime.securesms.events.WebRtcViewModel; import org.thoughtcrime.securesms.keyvalue.SignalStore; +import org.thoughtcrime.securesms.permissions.Permissions; import org.thoughtcrime.securesms.recipients.Recipient; import org.thoughtcrime.securesms.recipients.RecipientId; import org.thoughtcrime.securesms.ringrtc.CameraState; @@ -127,6 +129,8 @@ public class WebRtcCallView extends InsetAwareConstraintLayout { private MultiReactionBurstLayout reactionViews; private ComposeView raiseHandSnackbar; private Barrier pipBottomBoundaryBarrier; + private View missingPermissionContainer; + private MaterialButton allowAccessButton; @@ -207,6 +211,8 @@ public class WebRtcCallView extends InsetAwareConstraintLayout { reactionViews = findViewById(R.id.call_screen_reactions_container); raiseHandSnackbar = findViewById(R.id.call_screen_raise_hand_view); pipBottomBoundaryBarrier = findViewById(R.id.pip_bottom_boundary_barrier); + missingPermissionContainer = findViewById(R.id.missing_permissions_container); + allowAccessButton = findViewById(R.id.allow_access_button); View decline = findViewById(R.id.call_screen_decline_call); View answerLabel = findViewById(R.id.call_screen_answer_call_label); @@ -262,10 +268,16 @@ public class WebRtcCallView extends InsetAwareConstraintLayout { }); videoToggle.setOnCheckedChangeListener((v, isOn) -> { + if (!hasCameraPermission()) { + videoToggle.setChecked(false); + } runIfNonNull(controlsListener, listener -> listener.onVideoChanged(isOn)); }); micToggle.setOnCheckedChangeListener((v, isOn) -> { + if (!hasAudioPermission()) { + micToggle.setChecked(false); + } runIfNonNull(controlsListener, listener -> listener.onMicChanged(isOn)); }); @@ -301,10 +313,13 @@ public class WebRtcCallView extends InsetAwareConstraintLayout { ViewUtil.setBottomMargin(smallLocalAudioIndicator, audioIndicatorMargin); startCall.setOnClickListener(v -> { - if (controlsListener != null) { - startCall.setEnabled(false); - controlsListener.onStartCall(videoToggle.isChecked()); - } + Runnable onGranted = () -> { + if (controlsListener != null) { + startCall.setEnabled(false); + controlsListener.onStartCall(videoToggle.isChecked()); + } + }; + runIfNonNull(controlsListener, listener -> listener.onAudioPermissionsRequested(onGranted)); }); ColorMatrix greyScaleMatrix = new ColorMatrix(); @@ -365,6 +380,12 @@ public class WebRtcCallView extends InsetAwareConstraintLayout { onBarrierBottomChanged(bottom); } }); + + missingPermissionContainer.setVisibility(hasCameraPermission() ? View.GONE : View.VISIBLE); + + allowAccessButton.setOnClickListener(v -> { + runIfNonNull(controlsListener, listener -> listener.onVideoChanged(videoToggle.isEnabled())); + }); } @Override @@ -405,7 +426,7 @@ public class WebRtcCallView extends InsetAwareConstraintLayout { } public void setMicEnabled(boolean isMicEnabled) { - micToggle.setChecked(isMicEnabled, false); + micToggle.setChecked(hasAudioPermission() && isMicEnabled, false); } public void setPendingParticipantsViewListener(@Nullable PendingParticipantsView.Listener listener) { @@ -424,6 +445,14 @@ public class WebRtcCallView extends InsetAwareConstraintLayout { } } + private boolean hasCameraPermission() { + return Permissions.hasAll(getContext(), Manifest.permission.CAMERA); + } + + private boolean hasAudioPermission() { + return Permissions.hasAll(getContext(), Manifest.permission.RECORD_AUDIO); + } + public void updateCallParticipants(@NonNull CallParticipantsViewState callParticipantsViewState) { lastState = callParticipantsViewState; @@ -503,7 +532,7 @@ public class WebRtcCallView extends InsetAwareConstraintLayout { }); - videoToggle.setChecked(localCallParticipant.isVideoEnabled(), false); + videoToggle.setChecked(hasCameraPermission() && localCallParticipant.isVideoEnabled(), false); smallLocalRender.setRenderInPip(true); smallLocalRender.setCallParticipant(localCallParticipant); smallLocalRender.setMirror(localCallParticipant.getCameraDirection() == CameraState.Direction.FRONT); @@ -984,5 +1013,6 @@ public class WebRtcCallView extends InsetAwareConstraintLayout { void onCallInfoClicked(); void onNavigateUpClicked(); void toggleControls(); + void onAudioPermissionsRequested(Runnable onGranted); } } diff --git a/app/src/main/java/org/thoughtcrime/securesms/permissions/Permissions.java b/app/src/main/java/org/thoughtcrime/securesms/permissions/Permissions.java index fb30608b5a..58e6e329cd 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/permissions/Permissions.java +++ b/app/src/main/java/org/thoughtcrime/securesms/permissions/Permissions.java @@ -113,6 +113,10 @@ public class Permissions { return withRationaleDialog(null, title, details, true, headers); } + public PermissionsBuilder withRationaleDialog(@NonNull String title, @NonNull String details, boolean cancelable, @NonNull @DrawableRes int... headers) { + return withRationaleDialog(null, title, details, cancelable, headers); + } + public PermissionsBuilder withRationaleDialog(@Nullable String message, @Nullable String title, @Nullable String details, boolean cancelable, @NonNull @DrawableRes int... headers) { this.rationalDialogHeader = headers; this.rationaleDialogMessage = message; diff --git a/app/src/main/java/org/thoughtcrime/securesms/util/CommunicationActions.java b/app/src/main/java/org/thoughtcrime/securesms/util/CommunicationActions.java index d03a1ca774..e16ce18388 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/util/CommunicationActions.java +++ b/app/src/main/java/org/thoughtcrime/securesms/util/CommunicationActions.java @@ -406,25 +406,15 @@ public class CommunicationActions { } private static void startVideoCallInternal(@NonNull CallContext callContext, @NonNull Recipient recipient, boolean fromCallLink) { - callContext.getPermissionsBuilder() - .request(Manifest.permission.RECORD_AUDIO, Manifest.permission.CAMERA) - .ifNecessary() - .withRationaleDialog(callContext.getContext().getString(R.string.ConversationActivity_signal_needs_the_microphone_and_camera_permissions_in_order_to_call_s, recipient.getDisplayName(callContext.getContext())), - R.drawable.ic_mic_solid_24, - R.drawable.ic_video_solid_24_tinted) - .withPermanentDenialDialog(callContext.getContext().getString(R.string.ConversationActivity_signal_needs_the_microphone_and_camera_permissions_in_order_to_call_s, recipient.getDisplayName(callContext.getContext()))) - .onAllGranted(() -> { - ApplicationDependencies.getSignalCallManager().startPreJoinCall(recipient); + ApplicationDependencies.getSignalCallManager().startPreJoinCall(recipient); - Intent activityIntent = new Intent(callContext.getContext(), WebRtcCallActivity.class); + Intent activityIntent = new Intent(callContext.getContext(), WebRtcCallActivity.class); - activityIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK) - .putExtra(WebRtcCallActivity.EXTRA_ENABLE_VIDEO_IF_AVAILABLE, true) - .putExtra(WebRtcCallActivity.EXTRA_STARTED_FROM_CALL_LINK, fromCallLink); + activityIntent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK) + .putExtra(WebRtcCallActivity.EXTRA_ENABLE_VIDEO_IF_AVAILABLE, true) + .putExtra(WebRtcCallActivity.EXTRA_STARTED_FROM_CALL_LINK, fromCallLink); - callContext.startActivity(activityIntent); - }) - .execute(); + callContext.startActivity(activityIntent); } private static void handleE164Link(Activity activity, String e164) { diff --git a/app/src/main/res/layout/webrtc_call_view_header_large.xml b/app/src/main/res/layout/webrtc_call_view_header_large.xml index 987d3ae8bd..45f5460f0e 100644 --- a/app/src/main/res/layout/webrtc_call_view_header_large.xml +++ b/app/src/main/res/layout/webrtc_call_view_header_large.xml @@ -64,10 +64,39 @@ android:gravity="center_horizontal" android:textAppearance="@style/Signal.Text.BodyMedium" android:textColor="@color/core_white" - app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/call_screen_recipient_name" tools:text="Signal Calling..." /> + + + + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index d8f4313f52..e03b2091e0 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1936,6 +1936,26 @@ Removed from call Someone has removed you from the call. + + Allow access to your camera and microphone + + Allow access to your microphone + + Allow access to your camera + + To start or join a call, allow Signal access to your camera and microphone. + + To start or join a call, allow Signal access to your microphone. + + To enable your video, allow Signal access to your camera. + + Signal needs microphone permissions to start or join a call. + + Signal needs camera access to enable your video + + To start or join a call: + + To enable your video: Signal Call