mirror of
https://github.com/signalapp/Signal-Android.git
synced 2025-12-23 12:38:33 +00:00
Detect username QR codes in our camera-first capture flow.
This commit is contained in:
committed by
Alex Hart
parent
3f89acf9bd
commit
6104ef62df
@@ -3,6 +3,7 @@ package org.thoughtcrime.securesms;
|
|||||||
import android.Manifest;
|
import android.Manifest;
|
||||||
import android.annotation.SuppressLint;
|
import android.annotation.SuppressLint;
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
|
import android.content.Intent;
|
||||||
import android.net.Uri;
|
import android.net.Uri;
|
||||||
import android.os.AsyncTask;
|
import android.os.AsyncTask;
|
||||||
import android.os.Bundle;
|
import android.os.Bundle;
|
||||||
@@ -48,6 +49,8 @@ public class DeviceActivity extends PassphraseRequiredActivity
|
|||||||
|
|
||||||
private static final String TAG = Log.tag(DeviceActivity.class);
|
private static final String TAG = Log.tag(DeviceActivity.class);
|
||||||
|
|
||||||
|
private static final String EXTRA_DIRECT_TO_SCANNER = "add";
|
||||||
|
|
||||||
private final DynamicTheme dynamicTheme = new DynamicNoActionBarTheme();
|
private final DynamicTheme dynamicTheme = new DynamicNoActionBarTheme();
|
||||||
private final DynamicLanguage dynamicLanguage = new DynamicLanguage();
|
private final DynamicLanguage dynamicLanguage = new DynamicLanguage();
|
||||||
|
|
||||||
@@ -56,6 +59,13 @@ public class DeviceActivity extends PassphraseRequiredActivity
|
|||||||
private DeviceLinkFragment deviceLinkFragment;
|
private DeviceLinkFragment deviceLinkFragment;
|
||||||
private MenuItem cameraSwitchItem = null;
|
private MenuItem cameraSwitchItem = null;
|
||||||
|
|
||||||
|
|
||||||
|
public static Intent getIntentForScanner(Context context) {
|
||||||
|
Intent intent = new Intent(context, DeviceActivity.class);
|
||||||
|
intent.putExtra(EXTRA_DIRECT_TO_SCANNER, true);
|
||||||
|
return intent;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onPreCreate() {
|
public void onPreCreate() {
|
||||||
dynamicTheme.onCreate(this);
|
dynamicTheme.onCreate(this);
|
||||||
@@ -79,7 +89,7 @@ public class DeviceActivity extends PassphraseRequiredActivity
|
|||||||
this.deviceListFragment.setAddDeviceButtonListener(this);
|
this.deviceListFragment.setAddDeviceButtonListener(this);
|
||||||
this.deviceAddFragment.setScanListener(this);
|
this.deviceAddFragment.setScanListener(this);
|
||||||
|
|
||||||
if (getIntent().getBooleanExtra("add", false)) {
|
if (getIntent().getBooleanExtra(EXTRA_DIRECT_TO_SCANNER, false)) {
|
||||||
initFragment(R.id.fragment_container, deviceAddFragment, dynamicLanguage.getCurrentLocale());
|
initFragment(R.id.fragment_container, deviceAddFragment, dynamicLanguage.getCurrentLocale());
|
||||||
} else {
|
} else {
|
||||||
initFragment(R.id.fragment_container, deviceListFragment, dynamicLanguage.getCurrentLocale());
|
initFragment(R.id.fragment_container, deviceListFragment, dynamicLanguage.getCurrentLocale());
|
||||||
|
|||||||
@@ -26,9 +26,7 @@ public class DeviceProvisioningActivity extends PassphraseRequiredActivity {
|
|||||||
.setTitle(getString(R.string.DeviceProvisioningActivity_link_a_signal_device))
|
.setTitle(getString(R.string.DeviceProvisioningActivity_link_a_signal_device))
|
||||||
.setMessage(getString(R.string.DeviceProvisioningActivity_it_looks_like_youre_trying_to_link_a_signal_device_using_a_3rd_party_scanner))
|
.setMessage(getString(R.string.DeviceProvisioningActivity_it_looks_like_youre_trying_to_link_a_signal_device_using_a_3rd_party_scanner))
|
||||||
.setPositiveButton(R.string.DeviceProvisioningActivity_continue, (dialog1, which) -> {
|
.setPositiveButton(R.string.DeviceProvisioningActivity_continue, (dialog1, which) -> {
|
||||||
Intent intent = new Intent(DeviceProvisioningActivity.this, DeviceActivity.class);
|
startActivity(DeviceActivity.getIntentForScanner(this));
|
||||||
intent.putExtra("add", true);
|
|
||||||
startActivity(intent);
|
|
||||||
finish();
|
finish();
|
||||||
})
|
})
|
||||||
.setNegativeButton(android.R.string.cancel, (dialog12, which) -> {
|
.setNegativeButton(android.R.string.cancel, (dialog12, which) -> {
|
||||||
|
|||||||
@@ -181,18 +181,20 @@ public class AvatarSelectionActivity extends AppCompatActivity implements Camera
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onMainImageLoaded() {
|
public void onMainImageLoaded() {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onMainImageFailedToLoad() {
|
public void onMainImageFailedToLoad() {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void restoreState() {
|
public void restoreState() {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onQrCodeFound(@NonNull String data) {
|
||||||
|
}
|
||||||
|
|
||||||
public boolean popToRoot() {
|
public boolean popToRoot() {
|
||||||
final int backStackCount = getSupportFragmentManager().getBackStackEntryCount();
|
final int backStackCount = getSupportFragmentManager().getBackStackEntryCount();
|
||||||
if (backStackCount == 0) {
|
if (backStackCount == 0) {
|
||||||
|
|||||||
@@ -22,9 +22,9 @@ public interface CameraFragment {
|
|||||||
float PORTRAIT_ASPECT_RATIO = 9 / 16f;
|
float PORTRAIT_ASPECT_RATIO = 9 / 16f;
|
||||||
|
|
||||||
@SuppressLint({ "RestrictedApi", "UnsafeOptInUsageError" })
|
@SuppressLint({ "RestrictedApi", "UnsafeOptInUsageError" })
|
||||||
static Fragment newInstance() {
|
static Fragment newInstance(boolean qrScanEnabled) {
|
||||||
if (CameraXUtil.isSupported()) {
|
if (CameraXUtil.isSupported()) {
|
||||||
return CameraXFragment.newInstance();
|
return CameraXFragment.newInstance(qrScanEnabled);
|
||||||
} else {
|
} else {
|
||||||
return Camera1Fragment.newInstance();
|
return Camera1Fragment.newInstance();
|
||||||
}
|
}
|
||||||
@@ -63,6 +63,7 @@ public interface CameraFragment {
|
|||||||
void onVideoCaptureError();
|
void onVideoCaptureError();
|
||||||
void onGalleryClicked();
|
void onGalleryClicked();
|
||||||
void onCameraCountButtonClicked();
|
void onCameraCountButtonClicked();
|
||||||
|
void onQrCodeFound(@NonNull String data);
|
||||||
@NonNull Flowable<Optional<Media>> getMostRecentMediaItem();
|
@NonNull Flowable<Optional<Media>> getMostRecentMediaItem();
|
||||||
@NonNull MediaConstraints getMediaConstraints();
|
@NonNull MediaConstraints getMediaConstraints();
|
||||||
int getMaxVideoDuration();
|
int getMaxVideoDuration();
|
||||||
|
|||||||
@@ -28,9 +28,12 @@ import android.widget.ImageView;
|
|||||||
import androidx.annotation.NonNull;
|
import androidx.annotation.NonNull;
|
||||||
import androidx.annotation.Nullable;
|
import androidx.annotation.Nullable;
|
||||||
import androidx.camera.core.CameraSelector;
|
import androidx.camera.core.CameraSelector;
|
||||||
|
import androidx.camera.core.ImageAnalysis;
|
||||||
import androidx.camera.core.ImageCapture;
|
import androidx.camera.core.ImageCapture;
|
||||||
import androidx.camera.core.ImageCaptureException;
|
import androidx.camera.core.ImageCaptureException;
|
||||||
import androidx.camera.core.ImageProxy;
|
import androidx.camera.core.ImageProxy;
|
||||||
|
import androidx.camera.core.resolutionselector.ResolutionSelector;
|
||||||
|
import androidx.camera.core.resolutionselector.ResolutionStrategy;
|
||||||
import androidx.camera.view.PreviewView;
|
import androidx.camera.view.PreviewView;
|
||||||
import androidx.constraintlayout.widget.ConstraintLayout;
|
import androidx.constraintlayout.widget.ConstraintLayout;
|
||||||
import androidx.constraintlayout.widget.ConstraintSet;
|
import androidx.constraintlayout.widget.ConstraintSet;
|
||||||
@@ -42,6 +45,7 @@ import com.google.android.material.card.MaterialCardView;
|
|||||||
import org.signal.core.util.Stopwatch;
|
import org.signal.core.util.Stopwatch;
|
||||||
import org.signal.core.util.concurrent.SimpleTask;
|
import org.signal.core.util.concurrent.SimpleTask;
|
||||||
import org.signal.core.util.logging.Log;
|
import org.signal.core.util.logging.Log;
|
||||||
|
import org.signal.qr.QrProcessor;
|
||||||
import org.thoughtcrime.securesms.LoggingFragment;
|
import org.thoughtcrime.securesms.LoggingFragment;
|
||||||
import org.thoughtcrime.securesms.R;
|
import org.thoughtcrime.securesms.R;
|
||||||
import org.thoughtcrime.securesms.animation.AnimationCompleteListener;
|
import org.thoughtcrime.securesms.animation.AnimationCompleteListener;
|
||||||
@@ -61,6 +65,8 @@ import org.thoughtcrime.securesms.video.VideoUtil;
|
|||||||
|
|
||||||
import java.io.FileDescriptor;
|
import java.io.FileDescriptor;
|
||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
|
import java.util.concurrent.Executor;
|
||||||
|
import java.util.concurrent.Executors;
|
||||||
|
|
||||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers;
|
||||||
import io.reactivex.rxjava3.disposables.Disposable;
|
import io.reactivex.rxjava3.disposables.Disposable;
|
||||||
@@ -73,6 +79,7 @@ public class CameraXFragment extends LoggingFragment implements CameraFragment {
|
|||||||
|
|
||||||
private static final String TAG = Log.tag(CameraXFragment.class);
|
private static final String TAG = Log.tag(CameraXFragment.class);
|
||||||
private static final String IS_VIDEO_ENABLED = "is_video_enabled";
|
private static final String IS_VIDEO_ENABLED = "is_video_enabled";
|
||||||
|
private static final String IS_QR_SCAN_ENABLED = "is_qr_scan_enabled";
|
||||||
|
|
||||||
|
|
||||||
private static final Rational ASPECT_RATIO_16_9 = new Rational(16, 9);
|
private static final Rational ASPECT_RATIO_16_9 = new Rational(16, 9);
|
||||||
@@ -90,24 +97,30 @@ public class CameraXFragment extends LoggingFragment implements CameraFragment {
|
|||||||
private CameraScreenBrightnessController cameraScreenBrightnessController;
|
private CameraScreenBrightnessController cameraScreenBrightnessController;
|
||||||
private boolean isMediaSelected;
|
private boolean isMediaSelected;
|
||||||
|
|
||||||
|
private final Executor qrAnalysisExecutor = Executors.newSingleThreadExecutor();
|
||||||
|
private final QrProcessor qrProcessor = new QrProcessor();
|
||||||
|
|
||||||
public static CameraXFragment newInstanceForAvatarCapture() {
|
public static CameraXFragment newInstanceForAvatarCapture() {
|
||||||
CameraXFragment fragment = new CameraXFragment();
|
CameraXFragment fragment = new CameraXFragment();
|
||||||
Bundle args = new Bundle();
|
Bundle args = new Bundle();
|
||||||
|
|
||||||
args.putBoolean(IS_VIDEO_ENABLED, false);
|
args.putBoolean(IS_VIDEO_ENABLED, false);
|
||||||
|
args.putBoolean(IS_QR_SCAN_ENABLED, false);
|
||||||
fragment.setArguments(args);
|
fragment.setArguments(args);
|
||||||
|
|
||||||
return fragment;
|
return fragment;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static CameraXFragment newInstance() {
|
public static CameraXFragment newInstance(boolean qrScanEnabled) {
|
||||||
CameraXFragment fragment = new CameraXFragment();
|
CameraXFragment fragment = new CameraXFragment();
|
||||||
|
|
||||||
fragment.setArguments(new Bundle());
|
Bundle args = new Bundle();
|
||||||
|
args.putBoolean(IS_QR_SCAN_ENABLED, qrScanEnabled);
|
||||||
|
|
||||||
|
fragment.setArguments(args);
|
||||||
|
|
||||||
return fragment;
|
return fragment;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onAttach(@NonNull Context context) {
|
public void onAttach(@NonNull Context context) {
|
||||||
super.onAttach(context);
|
super.onAttach(context);
|
||||||
@@ -156,6 +169,26 @@ public class CameraXFragment extends LoggingFragment implements CameraFragment {
|
|||||||
onOrientationChanged();
|
onOrientationChanged();
|
||||||
cameraController.bindToLifecycle(() -> Log.d(TAG, "Camera init complete from onViewCreated"));
|
cameraController.bindToLifecycle(() -> Log.d(TAG, "Camera init complete from onViewCreated"));
|
||||||
|
|
||||||
|
if (requireArguments().getBoolean(IS_QR_SCAN_ENABLED, false)) {
|
||||||
|
ImageAnalysis imageAnalysis = new ImageAnalysis.Builder()
|
||||||
|
.setResolutionSelector(new ResolutionSelector.Builder().setResolutionStrategy(ResolutionStrategy.HIGHEST_AVAILABLE_STRATEGY).build())
|
||||||
|
.setBackpressureStrategy(ImageAnalysis.STRATEGY_KEEP_ONLY_LATEST)
|
||||||
|
.build();
|
||||||
|
|
||||||
|
imageAnalysis.setAnalyzer(qrAnalysisExecutor, imageProxy -> {
|
||||||
|
try {
|
||||||
|
String data = qrProcessor.getScannedData(imageProxy);
|
||||||
|
if (data != null) {
|
||||||
|
controller.onQrCodeFound(data);
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
imageProxy.close();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
cameraController.addUseCase(imageAnalysis);
|
||||||
|
}
|
||||||
|
|
||||||
view.addOnLayoutChangeListener((v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> {
|
view.addOnLayoutChangeListener((v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> {
|
||||||
// Let's assume portrait for now, so 9:16
|
// Let's assume portrait for now, so 9:16
|
||||||
float aspectRatio = CameraFragment.getAspectRatioForOrientation(Configuration.ORIENTATION_PORTRAIT);
|
float aspectRatio = CameraFragment.getAspectRatioForOrientation(Configuration.ORIENTATION_PORTRAIT);
|
||||||
|
|||||||
@@ -15,7 +15,6 @@ import androidx.annotation.MainThread
|
|||||||
import androidx.annotation.RequiresApi
|
import androidx.annotation.RequiresApi
|
||||||
import androidx.annotation.RequiresPermission
|
import androidx.annotation.RequiresPermission
|
||||||
import androidx.camera.core.Camera
|
import androidx.camera.core.Camera
|
||||||
import androidx.camera.core.CameraProvider
|
|
||||||
import androidx.camera.core.CameraSelector
|
import androidx.camera.core.CameraSelector
|
||||||
import androidx.camera.core.FocusMeteringAction
|
import androidx.camera.core.FocusMeteringAction
|
||||||
import androidx.camera.core.FocusMeteringResult
|
import androidx.camera.core.FocusMeteringResult
|
||||||
@@ -72,6 +71,7 @@ class SignalCameraController(val context: Context, val lifecycleOwner: Lifecycle
|
|||||||
private val cameraProviderFuture: ListenableFuture<ProcessCameraProvider> = ProcessCameraProvider.getInstance(context)
|
private val cameraProviderFuture: ListenableFuture<ProcessCameraProvider> = ProcessCameraProvider.getInstance(context)
|
||||||
private val viewPort: ViewPort? = previewView.getViewPort(Surface.ROTATION_0)
|
private val viewPort: ViewPort? = previewView.getViewPort(Surface.ROTATION_0)
|
||||||
private val initializationCompleteListeners: MutableSet<InitializationListener> = mutableSetOf()
|
private val initializationCompleteListeners: MutableSet<InitializationListener> = mutableSetOf()
|
||||||
|
private val customUseCases: MutableList<UseCase> = mutableListOf()
|
||||||
|
|
||||||
private var imageRotation = 0
|
private var imageRotation = 0
|
||||||
private var recording: Recording? = null
|
private var recording: Recording? = null
|
||||||
@@ -129,6 +129,19 @@ class SignalCameraController(val context: Context, val lifecycleOwner: Lifecycle
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@MainThread
|
||||||
|
fun addUseCase(useCase: UseCase) {
|
||||||
|
ThreadUtil.assertMainThread()
|
||||||
|
|
||||||
|
customUseCases += useCase
|
||||||
|
|
||||||
|
if (isRecording()) {
|
||||||
|
stopRecording()
|
||||||
|
}
|
||||||
|
|
||||||
|
tryToBindCamera()
|
||||||
|
}
|
||||||
|
|
||||||
@MainThread
|
@MainThread
|
||||||
fun takePicture(executor: Executor, callback: ImageCapture.OnImageCapturedCallback) {
|
fun takePicture(executor: Executor, callback: ImageCapture.OnImageCapturedCallback) {
|
||||||
ThreadUtil.assertMainThread()
|
ThreadUtil.assertMainThread()
|
||||||
@@ -285,7 +298,7 @@ class SignalCameraController(val context: Context, val lifecycleOwner: Lifecycle
|
|||||||
}
|
}
|
||||||
|
|
||||||
@MainThread
|
@MainThread
|
||||||
private fun tryToBindCamera(restoreStateRunnable: (() -> Unit)?) {
|
private fun tryToBindCamera(restoreStateRunnable: (() -> Unit)? = null) {
|
||||||
ThreadUtil.assertMainThread()
|
ThreadUtil.assertMainThread()
|
||||||
try {
|
try {
|
||||||
bindToLifecycleInternal()
|
bindToLifecycleInternal()
|
||||||
@@ -353,6 +366,11 @@ class SignalCameraController(val context: Context, val lifecycleOwner: Lifecycle
|
|||||||
} else {
|
} else {
|
||||||
cameraProvider.unbind(videoCaptureUseCase)
|
cameraProvider.unbind(videoCaptureUseCase)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for (useCase in customUseCases) {
|
||||||
|
addUseCase(useCase)
|
||||||
|
}
|
||||||
|
|
||||||
if (viewPort != null) {
|
if (viewPort != null) {
|
||||||
setViewPort(viewPort)
|
setViewPort(viewPort)
|
||||||
} else {
|
} else {
|
||||||
@@ -425,6 +443,6 @@ class SignalCameraController(val context: Context, val lifecycleOwner: Lifecycle
|
|||||||
}
|
}
|
||||||
|
|
||||||
interface InitializationListener {
|
interface InitializationListener {
|
||||||
fun onInitialized(cameraProvider: CameraProvider)
|
fun onInitialized(cameraProvider: ProcessCameraProvider)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
package org.thoughtcrime.securesms.mediasend.v2.capture
|
package org.thoughtcrime.securesms.mediasend.v2.capture
|
||||||
|
|
||||||
import org.thoughtcrime.securesms.mediasend.Media
|
import org.thoughtcrime.securesms.mediasend.Media
|
||||||
|
import org.thoughtcrime.securesms.recipients.Recipient
|
||||||
|
|
||||||
sealed class MediaCaptureEvent {
|
sealed class MediaCaptureEvent {
|
||||||
data class MediaCaptureRendered(val media: Media) : MediaCaptureEvent()
|
data class MediaCaptureRendered(val media: Media) : MediaCaptureEvent()
|
||||||
|
data class UsernameScannedFromQrCode(val recipient: Recipient, val username: String) : MediaCaptureEvent()
|
||||||
|
object DeviceLinkScannedFromQrCode : MediaCaptureEvent()
|
||||||
object MediaCaptureRenderFailed : MediaCaptureEvent()
|
object MediaCaptureRenderFailed : MediaCaptureEvent()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,9 +8,11 @@ import androidx.fragment.app.Fragment
|
|||||||
import androidx.fragment.app.viewModels
|
import androidx.fragment.app.viewModels
|
||||||
import androidx.navigation.fragment.findNavController
|
import androidx.navigation.fragment.findNavController
|
||||||
import app.cash.exhaustive.Exhaustive
|
import app.cash.exhaustive.Exhaustive
|
||||||
|
import com.google.android.material.dialog.MaterialAlertDialogBuilder
|
||||||
import io.reactivex.rxjava3.core.Flowable
|
import io.reactivex.rxjava3.core.Flowable
|
||||||
import org.signal.core.util.concurrent.LifecycleDisposable
|
import org.signal.core.util.concurrent.LifecycleDisposable
|
||||||
import org.signal.core.util.logging.Log
|
import org.signal.core.util.logging.Log
|
||||||
|
import org.thoughtcrime.securesms.DeviceActivity
|
||||||
import org.thoughtcrime.securesms.R
|
import org.thoughtcrime.securesms.R
|
||||||
import org.thoughtcrime.securesms.mediasend.CameraFragment
|
import org.thoughtcrime.securesms.mediasend.CameraFragment
|
||||||
import org.thoughtcrime.securesms.mediasend.Media
|
import org.thoughtcrime.securesms.mediasend.Media
|
||||||
@@ -21,6 +23,7 @@ import org.thoughtcrime.securesms.mediasend.v2.MediaSelectionViewModel
|
|||||||
import org.thoughtcrime.securesms.mms.MediaConstraints
|
import org.thoughtcrime.securesms.mms.MediaConstraints
|
||||||
import org.thoughtcrime.securesms.permissions.Permissions
|
import org.thoughtcrime.securesms.permissions.Permissions
|
||||||
import org.thoughtcrime.securesms.stories.Stories
|
import org.thoughtcrime.securesms.stories.Stories
|
||||||
|
import org.thoughtcrime.securesms.util.CommunicationActions
|
||||||
import org.thoughtcrime.securesms.util.navigation.safeNavigate
|
import org.thoughtcrime.securesms.util.navigation.safeNavigate
|
||||||
import java.io.FileDescriptor
|
import java.io.FileDescriptor
|
||||||
import java.util.Optional
|
import java.util.Optional
|
||||||
@@ -47,7 +50,7 @@ class MediaCaptureFragment : Fragment(R.layout.fragment_container), CameraFragme
|
|||||||
private val lifecycleDisposable = LifecycleDisposable()
|
private val lifecycleDisposable = LifecycleDisposable()
|
||||||
|
|
||||||
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
|
||||||
captureChildFragment = CameraFragment.newInstance() as CameraFragment
|
captureChildFragment = CameraFragment.newInstance(sharedViewModel.isContactSelectionRequired) as CameraFragment
|
||||||
|
|
||||||
navigator = MediaSelectionNavigator(
|
navigator = MediaSelectionNavigator(
|
||||||
toGallery = R.id.action_mediaCaptureFragment_to_mediaGalleryFragment
|
toGallery = R.id.action_mediaCaptureFragment_to_mediaGalleryFragment
|
||||||
@@ -74,6 +77,28 @@ class MediaCaptureFragment : Fragment(R.layout.fragment_container), CameraFragme
|
|||||||
|
|
||||||
navigator.goToReview(findNavController())
|
navigator.goToReview(findNavController())
|
||||||
}
|
}
|
||||||
|
is MediaCaptureEvent.UsernameScannedFromQrCode -> {
|
||||||
|
MaterialAlertDialogBuilder(requireContext())
|
||||||
|
.setTitle(getString(R.string.MediaCaptureFragment_username_dialog_title, event.username))
|
||||||
|
.setMessage(getString(R.string.MediaCaptureFragment_username_dialog_body, event.username))
|
||||||
|
.setPositiveButton(R.string.MediaCaptureFragment_username_dialog_go_to_chat_button) { d, _ ->
|
||||||
|
CommunicationActions.startConversation(requireContext(), event.recipient, "")
|
||||||
|
requireActivity().finish()
|
||||||
|
}
|
||||||
|
.setNegativeButton(android.R.string.cancel, null)
|
||||||
|
.show()
|
||||||
|
}
|
||||||
|
is MediaCaptureEvent.DeviceLinkScannedFromQrCode -> {
|
||||||
|
MaterialAlertDialogBuilder(requireContext())
|
||||||
|
.setTitle(R.string.MediaCaptureFragment_device_link_dialog_title)
|
||||||
|
.setMessage(R.string.MediaCaptureFragment_device_link_dialog_body)
|
||||||
|
.setPositiveButton(R.string.MediaCaptureFragment_device_link_dialog_continue) { d, _ ->
|
||||||
|
startActivity(DeviceActivity.getIntentForScanner(requireContext()))
|
||||||
|
requireActivity().finish()
|
||||||
|
}
|
||||||
|
.setNegativeButton(android.R.string.cancel, null)
|
||||||
|
.show()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -149,6 +174,10 @@ class MediaCaptureFragment : Fragment(R.layout.fragment_container), CameraFragme
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
override fun onQrCodeFound(data: String) {
|
||||||
|
viewModel.onQrCodeFound(data)
|
||||||
|
}
|
||||||
|
|
||||||
override fun getMostRecentMediaItem(): Flowable<Optional<Media>> {
|
override fun getMostRecentMediaItem(): Flowable<Optional<Media>> {
|
||||||
return viewModel.getMostRecentMedia()
|
return viewModel.getMostRecentMedia()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,20 +5,34 @@ import androidx.lifecycle.ViewModelProvider
|
|||||||
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
import io.reactivex.rxjava3.android.schedulers.AndroidSchedulers
|
||||||
import io.reactivex.rxjava3.core.Flowable
|
import io.reactivex.rxjava3.core.Flowable
|
||||||
import io.reactivex.rxjava3.core.Observable
|
import io.reactivex.rxjava3.core.Observable
|
||||||
|
import io.reactivex.rxjava3.disposables.CompositeDisposable
|
||||||
|
import io.reactivex.rxjava3.kotlin.plusAssign
|
||||||
|
import io.reactivex.rxjava3.schedulers.Schedulers
|
||||||
import io.reactivex.rxjava3.subjects.PublishSubject
|
import io.reactivex.rxjava3.subjects.PublishSubject
|
||||||
import io.reactivex.rxjava3.subjects.Subject
|
import io.reactivex.rxjava3.subjects.Subject
|
||||||
|
import org.signal.core.util.logging.Log
|
||||||
|
import org.thoughtcrime.securesms.components.settings.app.usernamelinks.main.QrScanResult
|
||||||
import org.thoughtcrime.securesms.mediasend.Media
|
import org.thoughtcrime.securesms.mediasend.Media
|
||||||
|
import org.thoughtcrime.securesms.profiles.manage.UsernameRepository
|
||||||
|
import org.thoughtcrime.securesms.recipients.Recipient
|
||||||
import org.thoughtcrime.securesms.util.rx.RxStore
|
import org.thoughtcrime.securesms.util.rx.RxStore
|
||||||
import java.io.FileDescriptor
|
import java.io.FileDescriptor
|
||||||
import java.util.Optional
|
import java.util.Optional
|
||||||
|
import java.util.concurrent.TimeUnit
|
||||||
|
|
||||||
class MediaCaptureViewModel(private val repository: MediaCaptureRepository) : ViewModel() {
|
class MediaCaptureViewModel(private val repository: MediaCaptureRepository) : ViewModel() {
|
||||||
|
|
||||||
|
companion object {
|
||||||
|
private val TAG = Log.tag(MediaCaptureViewModel::class.java)
|
||||||
|
}
|
||||||
|
|
||||||
private val store: RxStore<MediaCaptureState> = RxStore(MediaCaptureState())
|
private val store: RxStore<MediaCaptureState> = RxStore(MediaCaptureState())
|
||||||
|
|
||||||
private val internalEvents: Subject<MediaCaptureEvent> = PublishSubject.create()
|
private val internalEvents: Subject<MediaCaptureEvent> = PublishSubject.create()
|
||||||
|
private val qrData: Subject<String> = PublishSubject.create()
|
||||||
|
|
||||||
val events: Observable<MediaCaptureEvent> = internalEvents.observeOn(AndroidSchedulers.mainThread())
|
val events: Observable<MediaCaptureEvent> = internalEvents.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
val disposables = CompositeDisposable()
|
||||||
|
|
||||||
init {
|
init {
|
||||||
repository.getMostRecentItem { media ->
|
repository.getMostRecentItem { media ->
|
||||||
@@ -26,10 +40,42 @@ class MediaCaptureViewModel(private val repository: MediaCaptureRepository) : Vi
|
|||||||
state.copy(mostRecentMedia = media)
|
state.copy(mostRecentMedia = media)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
disposables += qrData
|
||||||
|
.throttleFirst(5, TimeUnit.SECONDS)
|
||||||
|
.filter { UsernameRepository.isValidLink(it) }
|
||||||
|
.subscribeOn(Schedulers.io())
|
||||||
|
.flatMapSingle { url ->
|
||||||
|
UsernameRepository.fetchUsernameAndAciFromLink(url)
|
||||||
|
.map { result ->
|
||||||
|
when (result) {
|
||||||
|
is UsernameRepository.UsernameLinkConversionResult.Success -> QrScanResult.Success(result.username.toString(), Recipient.externalUsername(result.aci, result.username.toString()))
|
||||||
|
is UsernameRepository.UsernameLinkConversionResult.Invalid,
|
||||||
|
is UsernameRepository.UsernameLinkConversionResult.NotFound,
|
||||||
|
is UsernameRepository.UsernameLinkConversionResult.NetworkError -> QrScanResult.Failure
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.observeOn(AndroidSchedulers.mainThread())
|
||||||
|
.subscribe { data ->
|
||||||
|
if (data is QrScanResult.Success) {
|
||||||
|
internalEvents.onNext(MediaCaptureEvent.UsernameScannedFromQrCode(data.recipient, data.username))
|
||||||
|
} else {
|
||||||
|
Log.w(TAG, "Failed to scan QR code.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
disposables += qrData
|
||||||
|
.throttleFirst(5, TimeUnit.SECONDS)
|
||||||
|
.filter { it.startsWith("sgnl://linkdevice") }
|
||||||
|
.subscribe { data ->
|
||||||
|
internalEvents.onNext(MediaCaptureEvent.DeviceLinkScannedFromQrCode)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
override fun onCleared() {
|
override fun onCleared() {
|
||||||
store.dispose()
|
store.dispose()
|
||||||
|
disposables.dispose()
|
||||||
}
|
}
|
||||||
|
|
||||||
fun onImageCaptured(data: ByteArray, width: Int, height: Int) {
|
fun onImageCaptured(data: ByteArray, width: Int, height: Int) {
|
||||||
@@ -44,6 +90,10 @@ class MediaCaptureViewModel(private val repository: MediaCaptureRepository) : Vi
|
|||||||
return store.stateFlowable.map { Optional.ofNullable(it.mostRecentMedia) }
|
return store.stateFlowable.map { Optional.ofNullable(it.mostRecentMedia) }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun onQrCodeFound(data: String) {
|
||||||
|
qrData.onNext(data)
|
||||||
|
}
|
||||||
|
|
||||||
private fun onMediaRendered(media: Media) {
|
private fun onMediaRendered(media: Media) {
|
||||||
internalEvents.onNext(MediaCaptureEvent.MediaCaptureRendered(media))
|
internalEvents.onNext(MediaCaptureEvent.MediaCaptureRendered(media))
|
||||||
}
|
}
|
||||||
@@ -52,6 +102,11 @@ class MediaCaptureViewModel(private val repository: MediaCaptureRepository) : Vi
|
|||||||
internalEvents.onNext(MediaCaptureEvent.MediaCaptureRenderFailed)
|
internalEvents.onNext(MediaCaptureEvent.MediaCaptureRenderFailed)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private sealed class QrScanResult {
|
||||||
|
data class Success(val username: String, val recipient: Recipient) : QrScanResult()
|
||||||
|
object Failure : QrScanResult()
|
||||||
|
}
|
||||||
|
|
||||||
class Factory(private val repository: MediaCaptureRepository) : ViewModelProvider.Factory {
|
class Factory(private val repository: MediaCaptureRepository) : ViewModelProvider.Factory {
|
||||||
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
override fun <T : ViewModel> create(modelClass: Class<T>): T {
|
||||||
return requireNotNull(modelClass.cast(MediaCaptureViewModel(repository)))
|
return requireNotNull(modelClass.cast(MediaCaptureViewModel(repository)))
|
||||||
|
|||||||
@@ -311,6 +311,10 @@ object UsernameRepository {
|
|||||||
return BASE_URL + base64
|
return BASE_URL + base64
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fun isValidLink(url: String): Boolean {
|
||||||
|
return parseLink(url) != null
|
||||||
|
}
|
||||||
|
|
||||||
@JvmStatic
|
@JvmStatic
|
||||||
fun onUsernameConsistencyValidated() {
|
fun onUsernameConsistencyValidated() {
|
||||||
SignalStore.account().usernameSyncState = AccountValues.UsernameSyncState.IN_SYNC
|
SignalStore.account().usernameSyncState = AccountValues.UsernameSyncState.IN_SYNC
|
||||||
|
|||||||
@@ -4906,6 +4906,20 @@
|
|||||||
<string name="MediaReviewImagePageFragment__discard_changes">Discard changes?</string>
|
<string name="MediaReviewImagePageFragment__discard_changes">Discard changes?</string>
|
||||||
<string name="MediaReviewImagePageFragment__youll_lose_any_changes">You\'ll lose any changes you\'ve made to this photo.</string>
|
<string name="MediaReviewImagePageFragment__youll_lose_any_changes">You\'ll lose any changes you\'ve made to this photo.</string>
|
||||||
|
|
||||||
|
<!-- The title of a dialog notifying that a user was found matching a scanned QR code. The placeholder is a username. Usernames are always latin characters. -->
|
||||||
|
<string name="MediaCaptureFragment_username_dialog_title">Found %1$s</string>
|
||||||
|
<!-- The body of a dialog notifying that a user was found matching a scanned QR code, prompting the user to start a chat with them. The placeholder is a username. Usernames are always latin characters. -->
|
||||||
|
<string name="MediaCaptureFragment_username_dialog_body">Start a chat with \"%1$s\"</string>
|
||||||
|
<!-- The label of a dialog asking the user if they would like to start a chat with a specific user. -->
|
||||||
|
<string name="MediaCaptureFragment_username_dialog_go_to_chat_button">Go to chat</string>
|
||||||
|
|
||||||
|
<!-- The title of a dialog notifying that the user scanned a QR code that could be used to link a Signal device. -->
|
||||||
|
<string name="MediaCaptureFragment_device_link_dialog_title">Link device?</string>
|
||||||
|
<!-- The body of a dialog notifying that the user scanned a QR code that could be used to link a Signal device. -->
|
||||||
|
<string name="MediaCaptureFragment_device_link_dialog_body">It looks like you\'re trying to link a Signal device. Tap continue and scan the code again to link it.</string>
|
||||||
|
<!-- The label of a dialog asking the user if they would like to continue to the linked device settings screen. -->
|
||||||
|
<string name="MediaCaptureFragment_device_link_dialog_continue">Continue</string>
|
||||||
|
|
||||||
|
|
||||||
<string name="BadgesOverviewFragment__my_badges">My badges</string>
|
<string name="BadgesOverviewFragment__my_badges">My badges</string>
|
||||||
<string name="BadgesOverviewFragment__featured_badge">Featured badge</string>
|
<string name="BadgesOverviewFragment__featured_badge">Featured badge</string>
|
||||||
|
|||||||
Reference in New Issue
Block a user