Update camera permission UI for voice calls.

This commit is contained in:
mtang-signal 2024-05-17 11:09:45 -07:00 committed by Cody Henthorne
parent a99db2b16e
commit b36b00a11c
6 changed files with 216 additions and 65 deletions

View file

@ -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

View file

@ -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);
}
}

View file

@ -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;

View file

@ -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) {

View file

@ -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..." />
<LinearLayout
android:id="@+id/missing_permissions_container"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
app:layout_constraintStart_toStartOf="parent"
app:layout_constraintEnd_toEndOf="parent"
app:layout_constraintTop_toBottomOf="@id/call_screen_status"
app:layout_constraintBottom_toBottomOf="parent"
android:gravity="center"
android:orientation="vertical">
<TextView
android:id="@+id/missing_permissions_text"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:gravity="center"
style="@style/Signal.Text.Body"
android:text="@string/WebRtcCallActivity__to_enable_video_allow_camera" />
<com.google.android.material.button.MaterialButton
android:id="@+id/allow_access_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_marginTop="10dp"
style="@style/Signal.Widget.Button.Large.Tonal"
android:text="@string/CameraXFragment_allow_access" />
</LinearLayout>
</androidx.constraintlayout.widget.ConstraintLayout>

View file

@ -1936,6 +1936,26 @@
<string name="WebRtcCallActivity__removed_from_call">Removed from call</string>
<!-- Message of dialog displayed when a user is removed from a call link -->
<string name="WebRtcCallActivity__someone_has_removed_you_from_the_call">Someone has removed you from the call.</string>
<!-- Dialog title asking users for camera and microphone permission -->
<string name="WebRtcCallActivity__allow_access_camera_microphone">Allow access to your camera and microphone</string>
<!-- Dialog title asking users for microphone permission -->
<string name="WebRtcCallActivity__allow_access_microphone">Allow access to your microphone</string>
<!-- Dialog title asking users for camera permission -->
<string name="WebRtcCallActivity__allow_access_camera">Allow access to your camera</string>
<!-- Dialog description explaining why camera and microphone permissions are needed to start or join a call -->
<string name="WebRtcCallActivity__to_start_call_camera_microphone">To start or join a call, allow Signal access to your camera and microphone.</string>
<!-- Dialog description explaining why microphone permissions are needed to start or join a call -->
<string name="WebRtcCallActivity__to_start_call_microphone">To start or join a call, allow Signal access to your microphone.</string>
<!-- Dialog description explaining why camera permissions are needed to enable a user's video in a call -->
<string name="WebRtcCallActivity__to_enable_video_allow_camera">To enable your video, allow Signal access to your camera.</string>
<!-- Toast describing why microphone permissions are needed to start or join a call -->
<string name="WebRtcCallActivity__signal_needs_microphone_start_call">Signal needs microphone permissions to start or join a call.</string>
<!-- Toast describing why camera permissions are needed to enable a video in a call -->
<string name="WebRtcCallActivity__signal_needs_camera_access_enable_video">Signal needs camera access to enable your video</string>
<!-- Dialog description that will explain the steps needed to give microphone permissions -->
<string name="WebRtcCallActivity__to_start_call">To start or join a call:</string>
<!-- Dialog description that will explain the steps needed to give camera permissions -->
<string name="WebRtcCallActivity__to_enable_video">To enable your video:</string>
<!-- WebRtcCallView -->
<string name="WebRtcCallView__signal_call">Signal Call</string>