diff --git a/app/src/main/java/org/thoughtcrime/securesms/DeviceActivity.java b/app/src/main/java/org/thoughtcrime/securesms/DeviceActivity.java index 4333ca36c2..575b665961 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/DeviceActivity.java +++ b/app/src/main/java/org/thoughtcrime/securesms/DeviceActivity.java @@ -131,14 +131,15 @@ public class DeviceActivity extends PassphraseRequiredActivity Permissions.with(this) .request(Manifest.permission.CAMERA) .ifNecessary() - .withPermanentDenialDialog(getString(R.string.DeviceActivity_signal_needs_the_camera_permission_in_order_to_scan_a_qr_code)) + .withRationaleDialog(getString(R.string.CameraXFragment_allow_access_camera), getString(R.string.CameraXFragment_to_scan_qr_code_allow_camera), R.drawable.symbol_camera_24) + .withPermanentDenialDialog(getString(R.string.DeviceActivity_signal_needs_the_camera_permission_in_order_to_scan_a_qr_code), null, R.string.CameraXFragment_allow_access_camera, R.string.CameraXFragment_to_scan_qr_codes, getSupportFragmentManager()) .onAllGranted(() -> { getSupportFragmentManager().beginTransaction() .replace(R.id.fragment_container, deviceAddFragment) .addToBackStack(null) .commitAllowingStateLoss(); }) - .onAnyDenied(() -> Toast.makeText(this, R.string.DeviceActivity_unable_to_scan_a_qr_code_without_the_camera_permission, Toast.LENGTH_LONG).show()) + .onAnyDenied(() -> Toast.makeText(this, R.string.CameraXFragment_signal_needs_camera_access_scan_qr_code, Toast.LENGTH_LONG).show()) .execute(); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/avatar/picker/AvatarPickerFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/avatar/picker/AvatarPickerFragment.kt index e2b7b4fce7..7b95255e1a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/avatar/picker/AvatarPickerFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/avatar/picker/AvatarPickerFragment.kt @@ -30,6 +30,7 @@ import org.thoughtcrime.securesms.components.recyclerview.GridDividerDecoration import org.thoughtcrime.securesms.groups.ParcelableGroupId import org.thoughtcrime.securesms.mediasend.AvatarSelectionActivity import org.thoughtcrime.securesms.mediasend.Media +import org.thoughtcrime.securesms.mediasend.camerax.CameraXUtil import org.thoughtcrime.securesms.permissions.PermissionCompat import org.thoughtcrime.securesms.permissions.Permissions import org.thoughtcrime.securesms.util.ViewUtil @@ -222,18 +223,22 @@ class AvatarPickerFragment : Fragment(R.layout.avatar_picker_fragment) { @Suppress("DEPRECATION") private fun openCameraCapture() { - Permissions.with(this) - .request(Manifest.permission.CAMERA) - .ifNecessary() - .onAllGranted { - val intent = AvatarSelectionActivity.getIntentForCameraCapture(requireContext()) - startActivityForResult(intent, REQUEST_CODE_SELECT_IMAGE) - } - .onAnyDenied { - Toast.makeText(requireContext(), R.string.AvatarSelectionBottomSheetDialogFragment__taking_a_photo_requires_the_camera_permission, Toast.LENGTH_SHORT) - .show() - } - .execute() + if (CameraXUtil.isSupported()) { + val intent = AvatarSelectionActivity.getIntentForCameraCapture(requireContext()) + startActivityForResult(intent, REQUEST_CODE_SELECT_IMAGE) + } else { + Permissions.with(this) + .request(Manifest.permission.CAMERA) + .ifNecessary() + .onAllGranted { + val intent = AvatarSelectionActivity.getIntentForCameraCapture(requireContext()) + startActivityForResult(intent, REQUEST_CODE_SELECT_IMAGE) + } + .withRationaleDialog(getString(R.string.CameraXFragment_allow_access_camera), getString(R.string.CameraXFragment_to_capture_photos_allow_camera), R.drawable.symbol_camera_24) + .withPermanentDenialDialog(getString(R.string.AvatarSelectionBottomSheetDialogFragment__taking_a_photo_requires_the_camera_permission), null, R.string.CameraXFragment_allow_access_camera, R.string.CameraXFragment_to_capture_photos, getParentFragmentManager()) + .onAnyDenied { Toast.makeText(requireContext(), R.string.AvatarSelectionBottomSheetDialogFragment__taking_a_photo_requires_the_camera_permission, Toast.LENGTH_SHORT).show() } + .execute() + } } @Suppress("DEPRECATION") diff --git a/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/ConversationSettingsFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/ConversationSettingsFragment.kt index 72e6123036..27419feb2d 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/ConversationSettingsFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/components/settings/conversation/ConversationSettingsFragment.kt @@ -78,6 +78,7 @@ import org.thoughtcrime.securesms.groups.ui.managegroup.dialogs.GroupInviteSentD import org.thoughtcrime.securesms.groups.ui.managegroup.dialogs.GroupsLearnMoreBottomSheetDialogFragment import org.thoughtcrime.securesms.mediaoverview.MediaOverviewActivity import org.thoughtcrime.securesms.mediapreview.MediaIntentFactory +import org.thoughtcrime.securesms.mediasend.camerax.CameraXUtil import org.thoughtcrime.securesms.messagerequests.MessageRequestRepository import org.thoughtcrime.securesms.nicknames.NicknameActivity import org.thoughtcrime.securesms.permissions.Permissions @@ -420,14 +421,16 @@ class ConversationSettingsFragment : DSLSettingsFragment( .setMessage(R.string.ConversationSettingsFragment__only_admins_of_this_group_can_add_to_its_story) .setPositiveButton(android.R.string.ok) { d, _ -> d.dismiss() } .show() + } else if (CameraXUtil.isSupported()) { + addToGroupStoryDelegate.addToStory(state.recipient.id) } else { Permissions.with(this@ConversationSettingsFragment) .request(Manifest.permission.CAMERA) .ifNecessary() - .withRationaleDialog(getString(R.string.ConversationActivity_to_capture_photos_and_video_allow_signal_access_to_the_camera), R.drawable.symbol_camera_24) - .withPermanentDenialDialog(getString(R.string.ConversationActivity_signal_needs_the_camera_permission_to_take_photos_or_video)) + .withRationaleDialog(getString(R.string.CameraXFragment_allow_access_camera), getString(R.string.CameraXFragment_to_capture_photos_and_video_allow_camera), R.drawable.symbol_camera_24) + .withPermanentDenialDialog(getString(R.string.CameraXFragment_signal_needs_camera_access_capture_photos), null, R.string.CameraXFragment_allow_access_camera, R.string.CameraXFragment_to_capture_photos_videos, getParentFragmentManager()) .onAllGranted { addToGroupStoryDelegate.addToStory(state.recipient.id) } - .onAnyDenied { Toast.makeText(requireContext(), R.string.ConversationActivity_signal_needs_camera_permissions_to_take_photos_or_video, Toast.LENGTH_LONG).show() } + .onAnyDenied { Toast.makeText(requireContext(), R.string.CameraXFragment_signal_needs_camera_access_capture_photos, Toast.LENGTH_LONG).show() } .execute() } }, diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityResultContracts.kt b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityResultContracts.kt index 7f27799662..36a8efecbf 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityResultContracts.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/conversation/v2/ConversationActivityResultContracts.kt @@ -28,6 +28,7 @@ import org.thoughtcrime.securesms.giph.ui.GiphyActivity import org.thoughtcrime.securesms.maps.PlacePickerActivity import org.thoughtcrime.securesms.mediasend.Media import org.thoughtcrime.securesms.mediasend.MediaSendActivityResult +import org.thoughtcrime.securesms.mediasend.camerax.CameraXUtil import org.thoughtcrime.securesms.mediasend.v2.MediaSelectionActivity import org.thoughtcrime.securesms.permissions.Permissions import org.thoughtcrime.securesms.recipients.RecipientId @@ -74,17 +75,22 @@ class ConversationActivityResultContracts(private val fragment: Fragment, privat } fun launchCamera(recipientId: RecipientId, isReply: Boolean) { - Permissions.with(fragment) - .request(Manifest.permission.CAMERA) - .ifNecessary() - .withRationaleDialog(fragment.getString(R.string.ConversationActivity_to_capture_photos_and_video_allow_signal_access_to_the_camera), R.drawable.symbol_camera_24) - .withPermanentDenialDialog(fragment.getString(R.string.ConversationActivity_signal_needs_the_camera_permission_to_take_photos_or_video)) - .onAllGranted { - cameraLauncher.launch(MediaSelectionInput(emptyList(), recipientId, null, isReply)) - fragment.requireActivity().overridePendingTransition(R.anim.camera_slide_from_bottom, R.anim.stationary) - } - .onAnyDenied { Toast.makeText(fragment.requireContext(), R.string.ConversationActivity_signal_needs_camera_permissions_to_take_photos_or_video, Toast.LENGTH_LONG).show() } - .execute() + if (CameraXUtil.isSupported()) { + cameraLauncher.launch(MediaSelectionInput(emptyList(), recipientId, null, isReply)) + fragment.requireActivity().overridePendingTransition(R.anim.camera_slide_from_bottom, R.anim.stationary) + } else { + Permissions.with(fragment) + .request(Manifest.permission.CAMERA) + .ifNecessary() + .withRationaleDialog(fragment.getString(R.string.CameraXFragment_allow_access_camera), fragment.getString(R.string.CameraXFragment_to_capture_photos_and_video_allow_camera), R.drawable.symbol_camera_24) + .withPermanentDenialDialog(fragment.getString(R.string.CameraXFragment_signal_needs_camera_access_capture_photos), null, R.string.CameraXFragment_allow_access_camera, R.string.CameraXFragment_to_capture_photos_videos, fragment.parentFragmentManager) + .onAllGranted { + cameraLauncher.launch(MediaSelectionInput(emptyList(), recipientId, null, isReply)) + fragment.requireActivity().overridePendingTransition(R.anim.camera_slide_from_bottom, R.anim.stationary) + } + .onAnyDenied { Toast.makeText(fragment.requireContext(), R.string.CameraXFragment_signal_needs_camera_access_capture_photos, Toast.LENGTH_LONG).show() } + .execute() + } } fun launchMediaEditor(mediaList: List, recipientId: RecipientId, text: CharSequence?) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListFragment.java b/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListFragment.java index 45b71dd741..67a1af9a09 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/conversationlist/ConversationListFragment.java @@ -144,6 +144,7 @@ import org.thoughtcrime.securesms.keyvalue.SignalStore; import org.thoughtcrime.securesms.lock.v2.CreateSvrPinActivity; import org.thoughtcrime.securesms.main.Material3OnScrollHelperBinder; import org.thoughtcrime.securesms.main.SearchBinder; +import org.thoughtcrime.securesms.mediasend.camerax.CameraXUtil; import org.thoughtcrime.securesms.mediasend.v2.MediaSelectionActivity; import org.thoughtcrime.securesms.megaphone.Megaphone; import org.thoughtcrime.securesms.megaphone.MegaphoneActionController; @@ -392,14 +393,18 @@ public class ConversationListFragment extends MainFragment implements ActionMode fab.setOnClickListener(v -> startActivity(new Intent(getActivity(), NewConversationActivity.class))); cameraFab.setOnClickListener(v -> { - Permissions.with(this) - .request(Manifest.permission.CAMERA) - .ifNecessary() - .withRationaleDialog(getString(R.string.ConversationActivity_to_capture_photos_and_video_allow_signal_access_to_the_camera), R.drawable.symbol_camera_24) - .withPermanentDenialDialog(getString(R.string.ConversationActivity_signal_needs_the_camera_permission_to_take_photos_or_video)) - .onAllGranted(() -> startActivity(MediaSelectionActivity.camera(requireContext()))) - .onAnyDenied(() -> Toast.makeText(requireContext(), R.string.ConversationActivity_signal_needs_camera_permissions_to_take_photos_or_video, Toast.LENGTH_LONG).show()) - .execute(); + if (CameraXUtil.isSupported()) { + startActivity(MediaSelectionActivity.camera(requireContext())); + } else { + Permissions.with(this) + .request(Manifest.permission.CAMERA) + .ifNecessary() + .withRationaleDialog(getString(R.string.CameraXFragment_allow_access_camera), getString(R.string.CameraXFragment_to_capture_photos_and_video_allow_camera), R.drawable.symbol_camera_24) + .withPermanentDenialDialog(getString(R.string.CameraXFragment_signal_needs_camera_access_capture_photos), null, R.string.CameraXFragment_allow_access_camera, R.string.CameraXFragment_to_capture_photos_videos, getParentFragmentManager()) + .onAllGranted(() -> startActivity(MediaSelectionActivity.camera(requireContext()))) + .onAnyDenied(() -> Toast.makeText(requireContext(), R.string.CameraXFragment_signal_needs_camera_access_capture_photos, Toast.LENGTH_LONG).show()) + .execute(); + } }); initializeViewModel(); diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/CameraXFragment.java b/app/src/main/java/org/thoughtcrime/securesms/mediasend/CameraXFragment.java index 078a0f04da..5c67fad673 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/CameraXFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/mediasend/CameraXFragment.java @@ -1,11 +1,11 @@ package org.thoughtcrime.securesms.mediasend; +import android.Manifest; import android.animation.Animator; import android.annotation.SuppressLint; import android.app.Activity; import android.content.Context; import android.content.pm.ActivityInfo; -import android.content.res.Configuration; import android.content.res.Resources; import android.graphics.Color; import android.os.Build; @@ -24,6 +24,8 @@ import android.view.animation.AnimationUtils; import android.view.animation.DecelerateInterpolator; import android.view.animation.RotateAnimation; import android.widget.ImageView; +import android.widget.TextView; +import android.widget.Toast; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -37,6 +39,7 @@ import androidx.constraintlayout.widget.ConstraintSet; import androidx.core.content.ContextCompat; import com.bumptech.glide.Glide; +import com.google.android.material.button.MaterialButton; import com.google.android.material.card.MaterialCardView; import org.signal.core.util.Stopwatch; @@ -57,6 +60,8 @@ import org.thoughtcrime.securesms.mediasend.v2.MediaAnimations; import org.thoughtcrime.securesms.mediasend.v2.MediaCountIndicatorButton; import org.thoughtcrime.securesms.mms.DecryptableStreamUriLoader.DecryptableUri; import org.thoughtcrime.securesms.mms.MediaConstraints; +import org.thoughtcrime.securesms.permissions.Permissions; +import org.thoughtcrime.securesms.util.BottomSheetUtil; import org.thoughtcrime.securesms.util.FeatureFlags; import org.thoughtcrime.securesms.util.MemoryFileDescriptor; import org.thoughtcrime.securesms.util.TextSecurePreferences; @@ -65,6 +70,7 @@ import org.thoughtcrime.securesms.video.VideoUtil; import java.io.FileDescriptor; import java.io.IOException; +import java.util.List; import java.util.concurrent.Executor; import java.util.concurrent.Executors; @@ -72,6 +78,8 @@ import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers; import io.reactivex.rxjava3.disposables.Disposable; import kotlin.Unit; +import static org.thoughtcrime.securesms.permissions.PermissionDeniedBottomSheet.showPermissionFragment; + /** * Camera captured implemented using the CameraX SDK, which uses Camera2 under the hood. Should be * preferred whenever possible. @@ -98,6 +106,9 @@ public class CameraXFragment extends LoggingFragment implements CameraFragment { private CameraXModePolicy cameraXModePolicy; private CameraScreenBrightnessController cameraScreenBrightnessController; private boolean isMediaSelected; + private View missingPermissionsContainer; + private TextView missingPermissionsText; + private MaterialButton allowAccessButton; private final Executor qrAnalysisExecutor = Executors.newSingleThreadExecutor(); private final QrProcessor qrProcessor = new QrProcessor(); @@ -149,13 +160,18 @@ public class CameraXFragment extends LoggingFragment implements CameraFragment { @SuppressLint("MissingPermission") @Override public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) { - this.cameraParent = view.findViewById(R.id.camerax_camera_parent); + this.cameraParent = view.findViewById(R.id.camerax_camera_parent); - this.previewView = view.findViewById(R.id.camerax_camera); - this.controlsContainer = view.findViewById(R.id.camerax_controls_container); - this.cameraXModePolicy = CameraXModePolicy.acquire(requireContext(), - controller.getMediaConstraints(), - requireArguments().getBoolean(IS_VIDEO_ENABLED, true)); + this.previewView = view.findViewById(R.id.camerax_camera); + this.controlsContainer = view.findViewById(R.id.camerax_controls_container); + this.cameraXModePolicy = CameraXModePolicy.acquire(requireContext(), + controller.getMediaConstraints(), + requireArguments().getBoolean(IS_VIDEO_ENABLED, true)); + this.missingPermissionsContainer = view.findViewById(R.id.missing_permissions_container); + this.missingPermissionsText = view.findViewById(R.id.missing_permissions_text); + this.allowAccessButton = view.findViewById(R.id.allow_access_button); + + checkPermissions(requireArguments().getBoolean(IS_VIDEO_ENABLED, true)); Log.d(TAG, "Starting CameraX with mode policy " + cameraXModePolicy.getClass().getSimpleName()); @@ -218,6 +234,9 @@ public class CameraXFragment extends LoggingFragment implements CameraFragment { cameraController.bindToLifecycle(getViewLifecycleOwner(), () -> Log.d(TAG, "Camera init complete from onResume")); requireActivity().setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT); + if (hasCameraPermission()) { + missingPermissionsContainer.setVisibility(View.GONE); + } } @Override @@ -259,6 +278,61 @@ public class CameraXFragment extends LoggingFragment implements CameraFragment { }); } + @Override + public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { + Permissions.onRequestPermissionsResult(this, requestCode, permissions, grantResults); + } + + private void checkPermissions(boolean includeAudio) { + if (hasCameraPermission()) { + missingPermissionsContainer.setVisibility(View.GONE); + } else { + boolean hasAudioPermission = Permissions.hasAll(requireContext(), Manifest.permission.RECORD_AUDIO); + missingPermissionsContainer.setVisibility(View.VISIBLE); + int textResId = (!includeAudio || hasAudioPermission) ? R.string.CameraXFragment_to_capture_photos_and_video_allow_camera : R.string.CameraXFragment_to_capture_photos_and_video_allow_camera_microphone; + missingPermissionsText.setText(textResId); + allowAccessButton.setOnClickListener(v -> requestPermissions(includeAudio)); + } + } + + private void requestPermissions(boolean includeAudio) { + if (includeAudio) { + Permissions.with(this) + .request(Manifest.permission.CAMERA, Manifest.permission.RECORD_AUDIO) + .ifNecessary() + .onSomeGranted(permissions -> { + if (permissions.contains(Manifest.permission.CAMERA)) { + missingPermissionsContainer.setVisibility(View.GONE); + } + }) + .onSomePermanentlyDenied(deniedPermissions -> { + if (deniedPermissions.containsAll(List.of(Manifest.permission.CAMERA, Manifest.permission.RECORD_AUDIO))) { + showPermissionFragment(R.string.CameraXFragment_allow_access_camera_microphone, R.string.CameraXFragment_to_capture_photos_videos).show(getParentFragmentManager(), BottomSheetUtil.STANDARD_BOTTOM_SHEET_FRAGMENT_TAG); + } else if (deniedPermissions.contains(Manifest.permission.CAMERA)) { + showPermissionFragment(R.string.CameraXFragment_allow_access_camera, R.string.CameraXFragment_to_capture_photos_videos).show(getParentFragmentManager(), BottomSheetUtil.STANDARD_BOTTOM_SHEET_FRAGMENT_TAG); + } + }) + .onSomeDenied(deniedPermissions -> { + if (deniedPermissions.contains(Manifest.permission.CAMERA)) { + Toast.makeText(requireContext(), R.string.CameraXFragment_signal_needs_camera_access_capture_photos, Toast.LENGTH_LONG).show(); + } + }) + .execute(); + } else { + Permissions.with(this) + .request(Manifest.permission.CAMERA) + .ifNecessary() + .onAllGranted (() -> missingPermissionsContainer.setVisibility(View.GONE)) + .onAnyDenied(() -> Toast.makeText(requireContext(), R.string.CameraXFragment_signal_needs_camera_access_capture_photos, Toast.LENGTH_LONG).show()) + .withPermanentDenialDialog(getString(R.string.CameraXFragment_signal_needs_camera_access_capture_photos), null, R.string.CameraXFragment_allow_access_camera, R.string.CameraXFragment_to_capture_photos, getParentFragmentManager()) + .execute(); + } + } + + private boolean hasCameraPermission() { + return Permissions.hasAll(requireContext(), Manifest.permission.CAMERA); + } + private void onOrientationChanged() { int layout = R.layout.camera_controls_portrait; @@ -356,7 +430,7 @@ public class CameraXFragment extends LoggingFragment implements CameraFragment { selfieFlash = requireView().findViewById(R.id.camera_selfie_flash); captureButton.setOnClickListener(v -> { - if (cameraController.isInitialized()) { + if (hasCameraPermission() && cameraController.isInitialized()) { captureButton.setEnabled(false); flipButton.setEnabled(false); flashButton.setEnabled(false); diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/CameraXVideoCaptureHelper.java b/app/src/main/java/org/thoughtcrime/securesms/mediasend/CameraXVideoCaptureHelper.java index 93fce43023..e6ce4d06b1 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/CameraXVideoCaptureHelper.java +++ b/app/src/main/java/org/thoughtcrime/securesms/mediasend/CameraXVideoCaptureHelper.java @@ -118,13 +118,17 @@ class CameraXVideoCaptureHelper implements CameraButtonView.VideoCaptureListener public void onVideoCaptureStarted() { Log.d(TAG, "onVideoCaptureStarted"); - if (canRecordAudio()) { + if (canUseCamera() && canRecordAudio()) { beginCameraRecording(); - } else { + } else if (!canRecordAudio()) { displayAudioRecordingPermissionsDialog(); } } + private boolean canUseCamera() { + return Permissions.hasAll(fragment.requireContext(), Manifest.permission.CAMERA); + } + private boolean canRecordAudio() { return Permissions.hasAll(fragment.requireContext(), Manifest.permission.RECORD_AUDIO); } @@ -133,9 +137,9 @@ class CameraXVideoCaptureHelper implements CameraButtonView.VideoCaptureListener Permissions.with(fragment) .request(Manifest.permission.RECORD_AUDIO) .ifNecessary() - .withRationaleDialog(fragment.getString(R.string.ConversationActivity_enable_the_microphone_permission_to_capture_videos_with_sound), R.drawable.ic_mic_solid_24) - .withPermanentDenialDialog(fragment.getString(R.string.ConversationActivity_signal_needs_the_recording_permissions_to_capture_video)) - .onAnyDenied(() -> Toast.makeText(fragment.requireContext(), R.string.ConversationActivity_signal_needs_recording_permissions_to_capture_video, Toast.LENGTH_LONG).show()) + .withRationaleDialog(fragment.getString(R.string.CameraXFragment_allow_access_microphone), fragment.getString(R.string.CameraXFragment_to_capture_videos_with_sound), R.drawable.ic_mic_24) + .withPermanentDenialDialog(fragment.getString(R.string.ConversationActivity_signal_needs_the_recording_permissions_to_capture_video), null, R.string.CameraXFragment_allow_access_microphone, R.string.CameraXFragment_to_capture_videos, fragment.getParentFragmentManager()) + .onAnyDenied(() -> Toast.makeText(fragment.requireContext(), R.string.CameraXFragment_signal_needs_microphone_access_video, Toast.LENGTH_LONG).show()) .execute(); } diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/MediaSelectionNavigator.kt b/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/MediaSelectionNavigator.kt index c55efd9858..bca57bf84e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/MediaSelectionNavigator.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/MediaSelectionNavigator.kt @@ -5,6 +5,7 @@ import android.widget.Toast import androidx.fragment.app.Fragment import androidx.navigation.NavController import org.thoughtcrime.securesms.R +import org.thoughtcrime.securesms.mediasend.camerax.CameraXUtil import org.thoughtcrime.securesms.permissions.PermissionCompat import org.thoughtcrime.securesms.permissions.Permissions import org.thoughtcrime.securesms.util.navigation.safeNavigate @@ -33,14 +34,18 @@ class MediaSelectionNavigator( fun Fragment.requestPermissionsForCamera( onGranted: () -> Unit ) { - Permissions.with(this) - .request(Manifest.permission.CAMERA) - .ifNecessary() - .withRationaleDialog(getString(R.string.ConversationActivity_to_capture_photos_and_video_allow_signal_access_to_the_camera), R.drawable.ic_camera_24) - .withPermanentDenialDialog(getString(R.string.ConversationActivity_signal_needs_the_camera_permission_to_take_photos_or_video)) - .onAllGranted(onGranted) - .onAnyDenied { Toast.makeText(requireContext(), R.string.ConversationActivity_signal_needs_camera_permissions_to_take_photos_or_video, Toast.LENGTH_LONG).show() } - .execute() + if (CameraXUtil.isSupported()) { + onGranted() + } else { + Permissions.with(this) + .request(Manifest.permission.CAMERA) + .ifNecessary() + .withRationaleDialog(getString(R.string.CameraXFragment_allow_access_camera), getString(R.string.CameraXFragment_to_capture_photos_and_video_allow_camera), R.drawable.ic_camera_24) + .withPermanentDenialDialog(getString(R.string.CameraXFragment_signal_needs_camera_access_capture_photos), null, R.string.CameraXFragment_allow_access_camera, R.string.CameraXFragment_to_capture_photos_videos, getParentFragmentManager()) + .onAllGranted(onGranted) + .onAnyDenied { Toast.makeText(requireContext(), R.string.CameraXFragment_signal_needs_camera_access_capture_photos, Toast.LENGTH_LONG).show() } + .execute() + } } fun Fragment.requestPermissionsForGallery( diff --git a/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/gallery/MediaGalleryFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/gallery/MediaGalleryFragment.kt index 916966cb22..b1353dcf2a 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/gallery/MediaGalleryFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/mediasend/v2/gallery/MediaGalleryFragment.kt @@ -1,5 +1,6 @@ package org.thoughtcrime.securesms.mediasend.v2.gallery +import android.Manifest import android.os.Bundle import android.view.View import android.widget.Toast @@ -19,6 +20,7 @@ import org.thoughtcrime.securesms.components.recyclerview.GridDividerDecoration import org.thoughtcrime.securesms.databinding.V2MediaGalleryFragmentBinding import org.thoughtcrime.securesms.mediasend.Media import org.thoughtcrime.securesms.mediasend.MediaRepository +import org.thoughtcrime.securesms.mediasend.camerax.CameraXUtil import org.thoughtcrime.securesms.permissions.PermissionCompat import org.thoughtcrime.securesms.permissions.Permissions import org.thoughtcrime.securesms.util.Material3OnScrollHelper @@ -94,7 +96,18 @@ class MediaGalleryFragment : Fragment(R.layout.v2_media_gallery_fragment) { if (callbacks.isCameraEnabled()) { binding.mediaGalleryToolbar.setOnMenuItemClickListener { item -> if (item.itemId == R.id.action_camera) { - callbacks.onNavigateToCamera() + if (CameraXUtil.isSupported()) { + callbacks.onNavigateToCamera() + } else { + Permissions.with(this) + .request(Manifest.permission.CAMERA) + .ifNecessary() + .onAllGranted { callbacks.onNavigateToCamera() } + .withRationaleDialog(getString(R.string.CameraXFragment_allow_access_camera), getString(R.string.CameraXFragment_to_capture_photos_and_video_allow_camera), R.drawable.ic_camera_24) + .withPermanentDenialDialog(getString(R.string.CameraXFragment_signal_needs_camera_access_capture_photos), null, R.string.CameraXFragment_allow_access_camera, R.string.CameraXFragment_to_capture_photos_videos, getParentFragmentManager()) + .onAnyDenied { Toast.makeText(requireContext(), R.string.CameraXFragment_signal_needs_camera_access_capture_photos, Toast.LENGTH_LONG).show() } + .execute() + } true } else { false diff --git a/app/src/main/java/org/thoughtcrime/securesms/payments/preferences/transfer/PaymentsTransferFragment.java b/app/src/main/java/org/thoughtcrime/securesms/payments/preferences/transfer/PaymentsTransferFragment.java index 3ead250b21..6cc5d179e4 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/payments/preferences/transfer/PaymentsTransferFragment.java +++ b/app/src/main/java/org/thoughtcrime/securesms/payments/preferences/transfer/PaymentsTransferFragment.java @@ -96,22 +96,13 @@ public final class PaymentsTransferFragment extends LoggingFragment { Permissions.with(this) .request(Manifest.permission.CAMERA) .ifNecessary() - .withRationaleDialog(getString(R.string.PaymentsTransferFragment__to_scan_a_qr_code_signal_needs), R.drawable.ic_camera_24) - .onAnyPermanentlyDenied(this::onCameraPermissionPermanentlyDenied) + .withRationaleDialog(getString(R.string.CameraXFragment_allow_access_camera), getString(R.string.PaymentsTransferFragment__to_scan_a_qr_code_signal_needs), R.drawable.ic_camera_24) + .withPermanentDenialDialog(getString(R.string.PaymentsTransferFragment__to_scan_a_qr_code_signal_needs_access_to_the_camera), null, R.string.CameraXFragment_allow_access_camera, R.string.CameraXFragment_to_scan_qr_codes, getParentFragmentManager()) .onAllGranted(() -> SafeNavigation.safeNavigate(Navigation.findNavController(requireView()), R.id.action_paymentsTransfer_to_paymentsScanQr)) .onAnyDenied(() -> Toast.makeText(requireContext(), R.string.PaymentsTransferFragment__to_scan_a_qr_code_signal_needs_access_to_the_camera, Toast.LENGTH_LONG).show()) .execute(); } - private void onCameraPermissionPermanentlyDenied() { - new MaterialAlertDialogBuilder(requireContext()) - .setTitle(R.string.Permissions_permission_required) - .setMessage(R.string.PaymentsTransferFragment__signal_needs_the_camera_permission_to_capture_qr_code_go_to_settings) - .setPositiveButton(R.string.PaymentsTransferFragment__settings, (dialog, which) -> requireActivity().startActivity(Permissions.getApplicationSettingsIntent(requireContext()))) - .setNegativeButton(android.R.string.cancel, null) - .show(); - } - @Override @SuppressWarnings("deprecation") public void onRequestPermissionsResult(int requestCode, @NonNull String[] permissions, @NonNull int[] grantResults) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/stories/landing/StoriesLandingFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/stories/landing/StoriesLandingFragment.kt index b4ec71b525..10ec1b208e 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/stories/landing/StoriesLandingFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/stories/landing/StoriesLandingFragment.kt @@ -47,6 +47,7 @@ import org.thoughtcrime.securesms.dependencies.ApplicationDependencies import org.thoughtcrime.securesms.events.ReminderUpdateEvent import org.thoughtcrime.securesms.main.Material3OnScrollHelperBinder import org.thoughtcrime.securesms.main.SearchBinder +import org.thoughtcrime.securesms.mediasend.camerax.CameraXUtil import org.thoughtcrime.securesms.mediasend.v2.MediaSelectionActivity import org.thoughtcrime.securesms.permissions.Permissions import org.thoughtcrime.securesms.registration.RegistrationNavigationActivity @@ -224,16 +225,18 @@ class StoriesLandingFragment : DSLSettingsFragment(layoutId = R.layout.stories_l }) cameraFab.setOnClickListener { - Permissions.with(this) - .request(Manifest.permission.CAMERA) - .ifNecessary() - .withRationaleDialog(getString(R.string.ConversationActivity_to_capture_photos_and_video_allow_signal_access_to_the_camera), R.drawable.symbol_camera_24) - .withPermanentDenialDialog(getString(R.string.ConversationActivity_signal_needs_the_camera_permission_to_take_photos_or_video)) - .onAllGranted { - startActivityIfAble(MediaSelectionActivity.camera(requireContext(), isStory = true)) - } - .onAnyDenied { Toast.makeText(requireContext(), R.string.ConversationActivity_signal_needs_camera_permissions_to_take_photos_or_video, Toast.LENGTH_LONG).show() } - .execute() + if (CameraXUtil.isSupported()) { + startActivityIfAble(MediaSelectionActivity.camera(requireContext(), isStory = true)) + } else { + Permissions.with(this) + .request(Manifest.permission.CAMERA) + .ifNecessary() + .onAllGranted { startActivityIfAble(MediaSelectionActivity.camera(requireContext(), isStory = true)) } + .withRationaleDialog(getString(R.string.CameraXFragment_allow_access_camera), getString(R.string.CameraXFragment_to_capture_photos_and_video_allow_camera), R.drawable.symbol_camera_24) + .withPermanentDenialDialog(getString(R.string.CameraXFragment_signal_needs_camera_access_capture_photos), null, R.string.CameraXFragment_allow_access_camera, R.string.CameraXFragment_to_capture_photos_videos, getParentFragmentManager()) + .onAnyDenied { Toast.makeText(requireContext(), R.string.CameraXFragment_signal_needs_camera_access_capture_photos, Toast.LENGTH_LONG).show() } + .execute() + } } viewModel.state.observe(viewLifecycleOwner) { diff --git a/app/src/main/java/org/thoughtcrime/securesms/verify/VerifyIdentityFragment.kt b/app/src/main/java/org/thoughtcrime/securesms/verify/VerifyIdentityFragment.kt index d1e00399ee..824f173847 100644 --- a/app/src/main/java/org/thoughtcrime/securesms/verify/VerifyIdentityFragment.kt +++ b/app/src/main/java/org/thoughtcrime/securesms/verify/VerifyIdentityFragment.kt @@ -106,7 +106,8 @@ class VerifyIdentityFragment : Fragment(R.layout.fragment_container), ScanListen Permissions.with(this) .request(Manifest.permission.CAMERA) .ifNecessary() - .withPermanentDenialDialog(getString(R.string.VerifyIdentityActivity_signal_needs_the_camera_permission_in_order_to_scan_a_qr_code_but_it_has_been_permanently_denied)) + .withRationaleDialog(getString(R.string.CameraXFragment_allow_access_camera), getString(R.string.CameraXFragment_to_scan_qr_code_allow_camera), R.drawable.ic_camera_24) + .withPermanentDenialDialog(getString(R.string.VerifyIdentityActivity_signal_needs_the_camera_permission_in_order_to_scan_a_qr_code_but_it_has_been_permanently_denied), null, R.string.CameraXFragment_allow_access_camera, R.string.CameraXFragment_to_scan_qr_codes, getParentFragmentManager()) .onAllGranted { childFragmentManager.beginTransaction() .setCustomAnimations(R.anim.slide_from_top, R.anim.slide_to_bottom, R.anim.slide_from_bottom, R.anim.slide_to_top) @@ -114,7 +115,7 @@ class VerifyIdentityFragment : Fragment(R.layout.fragment_container), ScanListen .addToBackStack(null) .commitAllowingStateLoss() } - .onAnyDenied { Toast.makeText(requireContext(), R.string.VerifyIdentityActivity_unable_to_scan_qr_code_without_camera_permission, Toast.LENGTH_LONG).show() } + .onAnyDenied { Toast.makeText(requireContext(), R.string.CameraXFragment_signal_needs_camera_access_scan_qr_code, Toast.LENGTH_LONG).show() } .execute() } diff --git a/app/src/main/res/drawable/permission_camera.xml b/app/src/main/res/drawable/permission_camera.xml new file mode 100644 index 0000000000..f42854ff6a --- /dev/null +++ b/app/src/main/res/drawable/permission_camera.xml @@ -0,0 +1,23 @@ + + + + + + + diff --git a/app/src/main/res/layout/camerax_fragment.xml b/app/src/main/res/layout/camerax_fragment.xml index 2073853d6e..a0b42eb5d5 100644 --- a/app/src/main/res/layout/camerax_fragment.xml +++ b/app/src/main/res/layout/camerax_fragment.xml @@ -39,4 +39,43 @@ app:layout_constraintBottom_toBottomOf="parent" app:layout_constraintTop_toTopOf="@id/camerax_camera_parent" /> + + + + + + + + + + \ No newline at end of file diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 2addf51860..9280df226f 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -225,6 +225,38 @@ Capture Change camera Open gallery + + Allow access + + Allow access to your camera and microphone + + Allow access to your camera + + Allow access to your microphone + + To capture photos, allow Signal access to the camera. + + To capture photos and video, allow Signal access to the camera. + + To capture photos and video, allow Signal access to the camera and microphone. + + To capture videos with sound, allow Signal access to your microphone. + + To scan a QR code, allow Signal access to the camera. + + Signal needs camera access to capture photos + + Signal needs camera access to scan QR codes + + Signal needs microphone access to capture video + + To capture photos in Signal: + + To capture photos and videos in Signal: + + To capture videos with sound: + + To scan QR codes: Recent contacts