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