package org.telegram.ui; import android.Manifest; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.AnimatorSet; import android.animation.ObjectAnimator; import android.animation.ValueAnimator; import android.annotation.TargetApi; import android.app.Activity; import android.content.Context; import android.content.Intent; import android.content.pm.ActivityInfo; import android.content.pm.PackageManager; import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Color; import android.graphics.ImageFormat; import android.graphics.Paint; import android.graphics.Path; import android.graphics.Point; import android.graphics.RectF; import android.graphics.Typeface; import android.graphics.drawable.ShapeDrawable; import android.net.Uri; import android.os.Build; import android.os.Handler; import android.os.HandlerThread; import android.os.SystemClock; import android.text.SpannableStringBuilder; import android.text.Spanned; import android.text.TextUtils; import android.util.TypedValue; import android.view.Gravity; import android.view.View; import android.view.ViewGroup; import android.view.Window; import android.view.WindowManager; import android.widget.ImageView; import android.widget.TextView; import androidx.core.graphics.ColorUtils; import androidx.dynamicanimation.animation.FloatValueHolder; import androidx.dynamicanimation.animation.SpringAnimation; import androidx.dynamicanimation.animation.SpringForce; import com.google.zxing.BinaryBitmap; import com.google.zxing.LuminanceSource; import com.google.zxing.NotFoundException; import com.google.zxing.PlanarYUVLuminanceSource; import com.google.zxing.RGBLuminanceSource; import com.google.zxing.Result; import com.google.zxing.ResultPoint; import com.google.zxing.common.GlobalHistogramBinarizer; import com.google.zxing.qrcode.QRCodeReader; import org.telegram.messenger.AndroidUtilities; import org.telegram.messenger.FileLog; import org.telegram.messenger.ImageLoader; import org.telegram.messenger.LocaleController; import org.telegram.messenger.MrzRecognizer; import org.telegram.messenger.R; import org.telegram.messenger.SendMessagesHelper; import org.telegram.messenger.SharedConfig; import org.telegram.messenger.camera.CameraController; import org.telegram.messenger.camera.CameraSession; import org.telegram.messenger.camera.CameraView; import org.telegram.messenger.camera.Size; import org.telegram.ui.ActionBar.ActionBar; import org.telegram.ui.ActionBar.ActionBarLayout; import org.telegram.ui.ActionBar.BaseFragment; import org.telegram.ui.ActionBar.BottomSheet; import org.telegram.ui.ActionBar.Theme; import org.telegram.ui.ActionBar.ThemeDescription; import org.telegram.ui.Components.AnimationProperties; import org.telegram.ui.Components.CubicBezierInterpolator; import org.telegram.ui.Components.LayoutHelper; import org.telegram.ui.Components.LinkPath; import org.telegram.ui.Components.TypefaceSpan; import org.telegram.ui.Components.URLSpanNoUnderline; import java.util.ArrayList; @TargetApi(18) public class CameraScanActivity extends BaseFragment { private TextView titleTextView; private TextView descriptionText; private CameraView cameraView; private HandlerThread backgroundHandlerThread = new HandlerThread("ScanCamera"); private Handler handler; private TextView recognizedMrzView; private Paint paint = new Paint(); private Paint cornerPaint = new Paint(Paint.ANTI_ALIAS_FLAG); private Path path = new Path(); private ImageView galleryButton; private ImageView flashButton; private AnimatorSet flashAnimator; private float backShadowAlpha = .5f; private SpringAnimation qrAppearing = null; private float qrAppearingValue = 0; private RectF fromBounds = new RectF(); private RectF bounds = new RectF(); private long lastBoundsUpdate = 0; private final long boundsUpdateDuration = 75; private CameraScanActivityDelegate delegate; private boolean recognized; private long recognizedStart; private int recognizeFailed = 0; private int recognizeIndex = 0; private ValueAnimator recognizedAnimator; private String recognizedText; private int sps; // samples per second private boolean qrLoading = false; private boolean qrLoaded = false; private QRCodeReader qrReader; //private BarcodeDetector visionQrReader; private boolean needGalleryButton; private boolean any; private int currentType; public static final int TYPE_MRZ = 0; public static final int TYPE_QR = 1; public static final int TYPE_QR_LOGIN = 2; public interface CameraScanActivityDelegate { default void didFindMrzInfo(MrzRecognizer.Result result) { } default void didFindQr(String text) { } default boolean processQr(String text, Runnable onLoadEnd) { return false; } } // Official Signature public static ActionBarLayout[] showAsSheet(BaseFragment parentFragment, boolean gallery, int type, CameraScanActivityDelegate cameraDelegate) { return showAsSheet(parentFragment, gallery, type, cameraDelegate, false); } // Add the any parameter public static ActionBarLayout[] showAsSheet(BaseFragment parentFragment, boolean gallery, int type, CameraScanActivityDelegate cameraDelegate, boolean any) { if (parentFragment == null || parentFragment.getParentActivity() == null) { return null; } ActionBarLayout[] actionBarLayout = new ActionBarLayout[]{new ActionBarLayout(parentFragment.getParentActivity())}; BottomSheet bottomSheet = new BottomSheet(parentFragment.getParentActivity(), false) { CameraScanActivity fragment; { actionBarLayout[0].init(new ArrayList<>()); fragment = new CameraScanActivity(type) { @Override public void finishFragment() { dismiss(); } @Override public void removeSelfFromStack() { dismiss(); } }; fragment.needGalleryButton = gallery; fragment.any = any; actionBarLayout[0].addFragmentToStack(fragment); actionBarLayout[0].showLastFragment(); actionBarLayout[0].setPadding(backgroundPaddingLeft, 0, backgroundPaddingLeft, 0); fragment.setDelegate(cameraDelegate); containerView = actionBarLayout[0]; setApplyBottomPadding(false); setApplyBottomPadding(false); setOnDismissListener(dialog -> fragment.onFragmentDestroy()); } @Override protected boolean canDismissWithSwipe() { return false; } @Override public void onBackPressed() { if (actionBarLayout[0] == null || actionBarLayout[0].fragmentsStack.size() <= 1) { super.onBackPressed(); } else { actionBarLayout[0].onBackPressed(); } } @Override public void dismiss() { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { final Window window = fragment.getParentActivity().getWindow(); fragment.updateNavigationBarColor(fragment.wasNavigationBarColor); AndroidUtilities.setLightNavigationBar(window, fragment.wasLightNavigationBar); } super.dismiss(); actionBarLayout[0] = null; } }; bottomSheet.show(); return actionBarLayout; } public static ActionBarLayout[] showAsSheet(BaseFragment parentFragment, CameraScanActivityDelegate cameraDelegate) { return showAsSheet(parentFragment, true, TYPE_QR, cameraDelegate, true); } public CameraScanActivity(int type) { super(); CameraController.getInstance().initCamera(null); currentType = type; if (isQr()) { qrReader = new QRCodeReader(); //visionQrReader = new BarcodeDetector.Builder(ApplicationLoader.applicationContext).setBarcodeFormats(Barcode.QR_CODE).build(); } switch (SharedConfig.getDevicePerformanceClass()) { case SharedConfig.PERFORMANCE_CLASS_LOW: sps = 8; break; case SharedConfig.PERFORMANCE_CLASS_AVERAGE: sps = 24; break; case SharedConfig.PERFORMANCE_CLASS_HIGH: default: sps = 40; } } private int wasNavigationBarColor; private boolean wasLightNavigationBar; private float navigationBarColorT = 0; private ValueAnimator navigationBarColorAnimator; @Override public void onResume() { final Window window = getParentActivity().getWindow(); if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { wasNavigationBarColor = window.getNavigationBarColor(); wasLightNavigationBar = AndroidUtilities.getLightNavigationBar(window); AndroidUtilities.setLightNavigationBar(window, false); updateNavigationBarColor(0xff000000); } super.onResume(); } protected void updateNavigationBarColor(int color) { if (Build.VERSION.SDK_INT >= 21) { if (navigationBarColorAnimator != null) { navigationBarColorAnimator.cancel(); } final Window window = getParentActivity().getWindow(); navigationBarColorAnimator = ValueAnimator.ofArgb(window.getNavigationBarColor(), color); navigationBarColorAnimator.addUpdateListener(a -> window.setNavigationBarColor((int) a.getAnimatedValue())); navigationBarColorAnimator.setDuration(200); navigationBarColorAnimator.setInterpolator(CubicBezierInterpolator.EASE_BOTH); navigationBarColorAnimator.start(); } } @Override public void onFragmentDestroy() { super.onFragmentDestroy(); destroy(false, null); if (getParentActivity() != null) { getParentActivity().setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED); } /*if (visionQrReader != null) { visionQrReader.release(); }*/ } @Override public View createView(Context context) { actionBar.setBackButtonImage(R.drawable.ic_ab_back); actionBar.setItemsColor(Theme.getColor(Theme.key_windowBackgroundWhiteGrayText2), false); actionBar.setItemsBackgroundColor(Theme.getColor(Theme.key_actionBarWhiteSelector), false); actionBar.setCastShadows(false); if (!AndroidUtilities.isTablet() && !isQr()) { actionBar.showActionModeTop(); } actionBar.setActionBarMenuOnItemClick(new ActionBar.ActionBarMenuOnItemClick() { @Override public void onItemClick(int id) { if (id == -1) { finishFragment(); } } }); paint.setColor(0x7f000000); cornerPaint.setColor(0xffffffff); cornerPaint.setStyle(Paint.Style.FILL); ViewGroup viewGroup = new ViewGroup(context) { @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int width = MeasureSpec.getSize(widthMeasureSpec); int height = MeasureSpec.getSize(heightMeasureSpec); actionBar.measure(widthMeasureSpec, heightMeasureSpec); if (currentType == TYPE_MRZ) { cameraView.measure(MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec((int) (width * 0.704f), MeasureSpec.EXACTLY)); } else { cameraView.measure(MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(height, MeasureSpec.EXACTLY)); recognizedMrzView.measure(MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(height, MeasureSpec.UNSPECIFIED)); if (galleryButton != null) { galleryButton.measure(MeasureSpec.makeMeasureSpec(AndroidUtilities.dp(60), MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(AndroidUtilities.dp(60), MeasureSpec.EXACTLY)); } flashButton.measure(MeasureSpec.makeMeasureSpec(AndroidUtilities.dp(60), MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(AndroidUtilities.dp(60), MeasureSpec.EXACTLY)); } titleTextView.measure(MeasureSpec.makeMeasureSpec(width - AndroidUtilities.dp(72), MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(height, MeasureSpec.UNSPECIFIED)); descriptionText.measure(MeasureSpec.makeMeasureSpec((int) (width * 0.9f), MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(height, MeasureSpec.UNSPECIFIED)); setMeasuredDimension(width, height); } @Override protected void onLayout(boolean changed, int l, int t, int r, int b) { int width = r - l; int height = b - t; int y = 0; if (currentType == TYPE_MRZ) { cameraView.layout(0, y, cameraView.getMeasuredWidth(), y + cameraView.getMeasuredHeight()); y = (int) (height * 0.65f); titleTextView.layout(AndroidUtilities.dp(36), y, AndroidUtilities.dp(36) + titleTextView.getMeasuredWidth(), y + titleTextView.getMeasuredHeight()); recognizedMrzView.setTextSize(TypedValue.COMPLEX_UNIT_PX, cameraView.getMeasuredHeight() / 22); recognizedMrzView.setPadding(0, 0, 0, cameraView.getMeasuredHeight() / 15); } else { actionBar.layout(0, 0, actionBar.getMeasuredWidth(), actionBar.getMeasuredHeight()); cameraView.layout(0, 0, cameraView.getMeasuredWidth(), cameraView.getMeasuredHeight()); int size = (int) (Math.min(cameraView.getWidth(), cameraView.getHeight()) / 1.5f); if (currentType == TYPE_QR) { y = (cameraView.getMeasuredHeight() - size) / 2 - titleTextView.getMeasuredHeight() - AndroidUtilities.dp(30); } else { y = (cameraView.getMeasuredHeight() - size) / 2 - titleTextView.getMeasuredHeight() - AndroidUtilities.dp(64); } titleTextView.layout(AndroidUtilities.dp(36), y, AndroidUtilities.dp(36) + titleTextView.getMeasuredWidth(), y + titleTextView.getMeasuredHeight()); recognizedMrzView.layout(0, getMeasuredHeight() - recognizedMrzView.getMeasuredHeight(), getMeasuredWidth(), getMeasuredHeight()); int x; if (needGalleryButton) { x = cameraView.getMeasuredWidth() / 2 + AndroidUtilities.dp(35); } else { x = cameraView.getMeasuredWidth() / 2 - flashButton.getMeasuredWidth() / 2; } y = (cameraView.getMeasuredHeight() - size) / 2 + size + AndroidUtilities.dp(80); flashButton.layout(x, y, x + flashButton.getMeasuredWidth(), y + flashButton.getMeasuredHeight()); if (galleryButton != null) { x = cameraView.getMeasuredWidth() / 2 - AndroidUtilities.dp(35) - galleryButton.getMeasuredWidth(); galleryButton.layout(x, y, x + galleryButton.getMeasuredWidth(), y + galleryButton.getMeasuredHeight()); } } y = (int) (height * 0.74f); int x = (int) (width * 0.05f); descriptionText.layout(x, y, x + descriptionText.getMeasuredWidth(), y + descriptionText.getMeasuredHeight()); if (!recognized) { int side = (int) (Math.min(width, height) / 1.5f); AndroidUtilities.rectTmp.set( (width - side) / 2f / (float) width, (height - side) / 2f / (float) height, (width + side) / 2f / (float) width, (height + side) / 2f / (float) height ); updateBounds(AndroidUtilities.rectTmp); } } @Override protected boolean drawChild(Canvas canvas, View child, long drawingTime) { boolean result = super.drawChild(canvas, child, drawingTime); if (isQr() && child == cameraView) { RectF bounds = getBounds(); int sizex = (int) (child.getWidth() * bounds.width()), sizey = (int) (child.getHeight() * bounds.height()), cx = (int) (child.getWidth() * bounds.centerX()), cy = (int) (child.getHeight() * bounds.centerY()); sizex *= (.5f + qrAppearingValue * .5f); sizey *= (.5f + qrAppearingValue * .5f); int x = cx - sizex / 2, y = cy - sizey / 2; paint.setAlpha((int) (255 * backShadowAlpha * Math.min(1, qrAppearingValue))); canvas.drawRect(0, 0, child.getMeasuredWidth(), y, paint); canvas.drawRect(0, y + sizey, child.getMeasuredWidth(), child.getMeasuredHeight(), paint); canvas.drawRect(0, y, x, y + sizey, paint); canvas.drawRect(x + sizex, y, child.getMeasuredWidth(), y + sizey, paint); final int lineWidth = AndroidUtilities.lerp(0, AndroidUtilities.dp(4), (float) Math.pow(qrAppearingValue, 0.125f)), halfLineWidth = lineWidth / 2; final int lineLength = AndroidUtilities.lerp(Math.min(sizex, sizey), AndroidUtilities.dp(20), Math.min(1.2f, (float) Math.pow(qrAppearingValue, 1.8f))); cornerPaint.setAlpha((int) (255 * Math.min(1, qrAppearingValue))); path.reset(); path.arcTo(aroundPoint(x, y + lineLength, halfLineWidth), 0, 180); path.arcTo(aroundPoint((int) (x + lineWidth * 1.5f), (int) (y + lineWidth * 1.5f), lineWidth * 2), 180, 90); path.arcTo(aroundPoint(x + lineLength, y, halfLineWidth), 270, 180); path.lineTo(x + halfLineWidth, y + halfLineWidth); path.arcTo(aroundPoint((int) (x + lineWidth * 1.5f), (int) (y + lineWidth * 1.5f), lineWidth), 270, -90); path.close(); canvas.drawPath(path, cornerPaint); path.reset(); path.arcTo(aroundPoint(x + sizex, y + lineLength, halfLineWidth), 180, -180); path.arcTo(aroundPoint((int) (x + sizex - lineWidth * 1.5f), (int) (y + lineWidth * 1.5f), lineWidth * 2), 0, -90); path.arcTo(aroundPoint(x + sizex- lineLength, y, halfLineWidth), 270, -180); path.arcTo(aroundPoint((int) (x + sizex - lineWidth * 1.5f), (int) (y + lineWidth * 1.5f), lineWidth), 270, 90); path.close(); canvas.drawPath(path, cornerPaint); path.reset(); path.arcTo(aroundPoint(x, y + sizey - lineLength, halfLineWidth), 0, -180); path.arcTo(aroundPoint((int) (x + lineWidth * 1.5f), (int) (y + sizey - lineWidth * 1.5f), lineWidth * 2), 180, -90); path.arcTo(aroundPoint(x + lineLength, y + sizey, halfLineWidth), 90, -180); path.arcTo(aroundPoint((int) (x + lineWidth * 1.5f), (int) (y + sizey - lineWidth * 1.5f), lineWidth), 90, 90); path.close(); canvas.drawPath(path, cornerPaint); path.reset(); path.arcTo(aroundPoint(x + sizex, y + sizey - lineLength, halfLineWidth), 180, 180); path.arcTo(aroundPoint((int) (x + sizex - lineWidth * 1.5f), (int) (y + sizey - lineWidth * 1.5f), lineWidth * 2), 0, 90); path.arcTo(aroundPoint(x + sizex - lineLength, y + sizey, halfLineWidth), 90, 180); path.arcTo(aroundPoint((int) (x + sizex - lineWidth * 1.5f), (int) (y + sizey - lineWidth * 1.5f), lineWidth), 90, -90); path.close(); canvas.drawPath(path, cornerPaint); } return result; } private RectF aroundPoint(int x, int y, int r) { AndroidUtilities.rectTmp.set(x - r, y - r, x + r, y + r); return AndroidUtilities.rectTmp; } }; viewGroup.setOnTouchListener((v, event) -> true); fragmentView = viewGroup; cameraView = new CameraView(context, false); cameraView.setUseMaxPreview(true); cameraView.setOptimizeForBarcode(true); cameraView.setDelegate(() -> { startRecognizing(); if (isQr()) { if (qrAppearing != null) { qrAppearing.cancel(); qrAppearing = null; } qrAppearing = new SpringAnimation(new FloatValueHolder(0)); qrAppearing.addUpdateListener((animation, value, velocity) -> { qrAppearingValue = value / 500f; fragmentView.invalidate(); }); qrAppearing.addEndListener((animation, canceled, value, velocity) -> { if (qrAppearing != null) { qrAppearing.cancel(); qrAppearing = null; } }); qrAppearing.setSpring(new SpringForce(500f)); qrAppearing.getSpring().setDampingRatio(0.8f); qrAppearing.getSpring().setStiffness(250.0f); qrAppearing.start(); } }); viewGroup.addView(cameraView, LayoutHelper.createFrame(LayoutHelper.MATCH_PARENT, LayoutHelper.MATCH_PARENT)); if (currentType == TYPE_MRZ) { actionBar.setBackgroundColor(Theme.getColor(Theme.key_windowBackgroundWhite)); fragmentView.setBackgroundColor(Theme.getColor(Theme.key_windowBackgroundWhite)); } else { actionBar.setBackgroundDrawable(null); actionBar.setAddToContainer(false); actionBar.setItemsColor(0xffffffff, false); actionBar.setItemsBackgroundColor(0x22ffffff, false); viewGroup.setBackgroundColor(Theme.getColor(Theme.key_wallet_blackBackground)); viewGroup.addView(actionBar); } if (currentType == TYPE_QR_LOGIN) { actionBar.setTitle(LocaleController.getString("AuthAnotherClientScan", R.string.AuthAnotherClientScan)); } Paint selectionPaint = new Paint(Paint.ANTI_ALIAS_FLAG); selectionPaint.setPathEffect(LinkPath.roundedEffect); selectionPaint.setColor(ColorUtils.setAlphaComponent(Color.WHITE, 50)); titleTextView = new TextView(context) { LinkPath textPath; @Override protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { super.onMeasure(widthMeasureSpec, heightMeasureSpec); if (getText() instanceof Spanned) { Spanned spanned = (Spanned) getText(); URLSpanNoUnderline[] innerSpans = spanned.getSpans(0, spanned.length(), URLSpanNoUnderline.class); if (innerSpans != null && innerSpans.length > 0) { textPath = new LinkPath(true); textPath.setAllowReset(false); for (int a = 0; a < innerSpans.length; a++) { int start = spanned.getSpanStart(innerSpans[a]); int end = spanned.getSpanEnd(innerSpans[a]); textPath.setCurrentLayout(getLayout(), start, 0); int shift = getText() != null ? getPaint().baselineShift : 0; textPath.setBaselineShift(shift != 0 ? shift + AndroidUtilities.dp(shift > 0 ? 5 : -2) : 0); getLayout().getSelectionPath(start, end, textPath); } textPath.setAllowReset(true); } } } @Override protected void onDraw(Canvas canvas) { if (textPath != null) { canvas.drawPath(textPath, selectionPaint); } super.onDraw(canvas); } }; titleTextView.setGravity(Gravity.CENTER_HORIZONTAL); titleTextView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 24); viewGroup.addView(titleTextView); descriptionText = new TextView(context); descriptionText.setTextColor(Theme.getColor(Theme.key_windowBackgroundWhiteGrayText6)); descriptionText.setGravity(Gravity.CENTER_HORIZONTAL); descriptionText.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 16); viewGroup.addView(descriptionText); recognizedMrzView = new TextView(context); recognizedMrzView.setTextColor(0xffffffff); recognizedMrzView.setGravity(Gravity.CENTER_HORIZONTAL | Gravity.BOTTOM); recognizedMrzView.setAlpha(0); if (currentType == TYPE_MRZ) { titleTextView.setText(LocaleController.getString("PassportScanPassport", R.string.PassportScanPassport)); descriptionText.setText(LocaleController.getString("PassportScanPassportInfo", R.string.PassportScanPassportInfo)); titleTextView.setTextColor(Theme.getColor(Theme.key_windowBackgroundWhiteBlackText)); recognizedMrzView.setTypeface(Typeface.MONOSPACE); cameraView.addView(recognizedMrzView); } else { if (needGalleryButton) { //titleTextView.setText(LocaleController.getString("WalletScanCode", R.string.WalletScanCode)); } else { if (currentType == TYPE_QR) { titleTextView.setText(LocaleController.getString("AuthAnotherClientScan", R.string.AuthAnotherClientScan)); } else { String text = LocaleController.getString("AuthAnotherClientInfo5", R.string.AuthAnotherClientInfo5); SpannableStringBuilder spanned = new SpannableStringBuilder(text); String[] links = new String[] { LocaleController.getString("AuthAnotherWebClientUrl", R.string.AuthAnotherWebClientUrl), LocaleController.getString("AuthAnotherClientDownloadClientUrl", R.string.AuthAnotherClientDownloadClientUrl) }; for (int i = 0; i < links.length; ++i) { text = spanned.toString(); int index1 = text.indexOf('*'); int index2 = text.indexOf('*', index1 + 1); if (index1 != -1 && index2 != -1 && index1 != index2) { titleTextView.setMovementMethod(new AndroidUtilities.LinkMovementMethodMy()); spanned.replace(index2, index2 + 1, " "); spanned.replace(index1, index1 + 1, " "); index1 += 1; index2 += 1; spanned.setSpan(new URLSpanNoUnderline(links[i]), index1, index2 - 1, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); spanned.setSpan(new TypefaceSpan(AndroidUtilities.getTypeface("fonts/rmedium.ttf")), index1, index2 - 1, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); } else { break; } } titleTextView.setLinkTextColor(Color.WHITE); titleTextView.setHighlightColor(Theme.getColor(Theme.key_windowBackgroundWhiteLinkSelection)); titleTextView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 16); titleTextView.setLineSpacing(AndroidUtilities.dp(2), 1.0f); titleTextView.setPadding(0, 0, 0, 0); titleTextView.setText(spanned); } } titleTextView.setTextColor(0xffffffff); recognizedMrzView.setTextSize(TypedValue.COMPLEX_UNIT_DIP, 16); recognizedMrzView.setPadding(AndroidUtilities.dp(10), 0, AndroidUtilities.dp(10), AndroidUtilities.dp(10)); if (needGalleryButton) { //recognizedMrzView.setText(LocaleController.getString("WalletScanCodeNotFound", R.string.WalletScanCodeNotFound)); } else { recognizedMrzView.setText(LocaleController.getString("AuthAnotherClientNotFound", R.string.AuthAnotherClientNotFound)); } viewGroup.addView(recognizedMrzView); if (needGalleryButton) { galleryButton = new ImageView(context); galleryButton.setScaleType(ImageView.ScaleType.CENTER); galleryButton.setImageResource(R.drawable.qr_gallery); galleryButton.setBackgroundDrawable(Theme.createSelectorDrawableFromDrawables(Theme.createCircleDrawable(AndroidUtilities.dp(60), 0x22ffffff), Theme.createCircleDrawable(AndroidUtilities.dp(60), 0x44ffffff))); viewGroup.addView(galleryButton); galleryButton.setOnClickListener(currentImage -> { if (getParentActivity() == null) { return; } if (Build.VERSION.SDK_INT >= 23) { if (getParentActivity().checkSelfPermission(Manifest.permission.READ_EXTERNAL_STORAGE) != PackageManager.PERMISSION_GRANTED) { getParentActivity().requestPermissions(new String[]{Manifest.permission.READ_EXTERNAL_STORAGE}, BasePermissionsActivity.REQUEST_CODE_EXTERNAL_STORAGE); return; } } PhotoAlbumPickerActivity fragment = new PhotoAlbumPickerActivity(PhotoAlbumPickerActivity.SELECT_TYPE_QR, false, false, null); fragment.setMaxSelectedPhotos(1, false); fragment.setAllowSearchImages(false); fragment.setDelegate(new PhotoAlbumPickerActivity.PhotoAlbumPickerActivityDelegate() { @Override public void didSelectPhotos(ArrayList photos, boolean notify, int scheduleDate) { try { if (!photos.isEmpty()) { SendMessagesHelper.SendingMediaInfo info = photos.get(0); if (info.path != null) { Point screenSize = AndroidUtilities.getRealScreenSize(); Bitmap bitmap = ImageLoader.loadBitmap(info.path, null, screenSize.x, screenSize.y, true); QrResult res = tryReadQr(null, null, 0, 0, 0, bitmap); if (res != null) { if (delegate != null) { delegate.didFindQr(res.text); } removeSelfFromStack(); } } } } catch (Throwable e) { FileLog.e(e); } } @Override public void startPhotoSelectActivity() { try { Intent photoPickerIntent = new Intent(Intent.ACTION_PICK); photoPickerIntent.setType("image/*"); getParentActivity().startActivityForResult(photoPickerIntent, 11); } catch (Exception e) { FileLog.e(e); } } }); presentFragment(fragment); }); } flashButton = new ImageView(context); flashButton.setScaleType(ImageView.ScaleType.CENTER); flashButton.setImageResource(R.drawable.qr_flashlight); flashButton.setBackgroundDrawable(Theme.createCircleDrawable(AndroidUtilities.dp(60), 0x22ffffff)); viewGroup.addView(flashButton); flashButton.setOnClickListener(currentImage -> { CameraSession session = cameraView.getCameraSession(); if (session != null) { ShapeDrawable shapeDrawable = (ShapeDrawable) flashButton.getBackground(); if (flashAnimator != null) { flashAnimator.cancel(); flashAnimator = null; } flashAnimator = new AnimatorSet(); ObjectAnimator animator = ObjectAnimator.ofInt(shapeDrawable, AnimationProperties.SHAPE_DRAWABLE_ALPHA, flashButton.getTag() == null ? 0x44 : 0x22); animator.addUpdateListener(animation -> flashButton.invalidate()); flashAnimator.playTogether(animator); flashAnimator.setDuration(200); flashAnimator.setInterpolator(CubicBezierInterpolator.DEFAULT); flashAnimator.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { flashAnimator = null; } }); flashAnimator.start(); if (flashButton.getTag() == null) { flashButton.setTag(1); session.setTorchEnabled(true); } else { flashButton.setTag(null); session.setTorchEnabled(false); } } }); } if (getParentActivity() != null) { getParentActivity().setRequestedOrientation(ActivityInfo.SCREEN_ORIENTATION_PORTRAIT); } fragmentView.setKeepScreenOn(true); return fragmentView; } private float recognizedT = 0; private void updateRecognized() { if (recognizedAnimator != null) { recognizedAnimator.cancel(); } float newRecorgnizedT = recognized ? 1f : 0f; recognizedAnimator = ValueAnimator.ofFloat(recognizedT, newRecorgnizedT); recognizedAnimator.addUpdateListener(a -> { recognizedT = (float) a.getAnimatedValue(); titleTextView.setAlpha(1f - recognizedT); flashButton.setAlpha(1f - recognizedT); backShadowAlpha = .5f + recognizedT * .25f; fragmentView.invalidate(); }); recognizedAnimator.setDuration((long) (200 * Math.abs(recognizedT - newRecorgnizedT))); recognizedAnimator.setInterpolator(CubicBezierInterpolator.EASE_BOTH); recognizedAnimator.start(); } private void updateBounds(RectF newBounds) { final long now = SystemClock.elapsedRealtime(); if (lastBoundsUpdate == 0) { // first update = set lastBoundsUpdate = now - boundsUpdateDuration; bounds.set(newBounds); fromBounds.set(newBounds); } else { // next updates = interpolate if (fromBounds != null && now - lastBoundsUpdate < boundsUpdateDuration) { float t = (now - lastBoundsUpdate) / (float) boundsUpdateDuration; // t = CubicBezierInterpolator.EASE_BOTH.getInterpolation(t); AndroidUtilities.lerp(fromBounds, bounds, t, fromBounds); } else { if (fromBounds == null) { fromBounds = new RectF(); } fromBounds.set(bounds); } bounds.set(newBounds); lastBoundsUpdate = now; } fragmentView.invalidate(); } private RectF getBounds() { if (fromBounds == null) { return bounds; } else { float t = (SystemClock.elapsedRealtime() - lastBoundsUpdate) / (float) boundsUpdateDuration; // t = CubicBezierInterpolator.EASE_BOTH.getInterpolation(t); if (t < 1f) { fragmentView.invalidate(); } AndroidUtilities.lerp(fromBounds, bounds, Math.min(1, Math.max(0, t)), AndroidUtilities.rectTmp); return AndroidUtilities.rectTmp; } } @Override public void onActivityResultFragment(int requestCode, int resultCode, Intent data) { if (resultCode == Activity.RESULT_OK && requestCode == 11 && data != null && data.getData() != null) { try { Point screenSize = AndroidUtilities.getRealScreenSize(); Bitmap bitmap = ImageLoader.loadBitmap(null, data.getData(), screenSize.x, screenSize.y, true); QrResult res = tryReadQr(null, null, 0, 0, 0, bitmap); if (res != null) { if (delegate != null) { delegate.didFindQr(res.text); } finishFragment(); } } catch (Throwable e) { FileLog.e(e); } } } public void setDelegate(CameraScanActivityDelegate cameraScanActivityDelegate) { delegate = cameraScanActivityDelegate; } public void destroy(boolean async, final Runnable beforeDestroyRunnable) { if (cameraView != null) { cameraView.destroy(async, beforeDestroyRunnable); cameraView = null; } backgroundHandlerThread.quitSafely(); } private Runnable requestShot = new Runnable() { @Override public void run() { if (cameraView != null && !recognized && cameraView.getCameraSession() != null) { handler.post(() -> { if (cameraView != null) { processShot(cameraView.getTextureView().getBitmap()); } }); } } }; private void startRecognizing() { backgroundHandlerThread.start(); handler = new Handler(backgroundHandlerThread.getLooper()); AndroidUtilities.runOnUIThread(requestShot, 0); } private void onNoQrFound() { AndroidUtilities.runOnUIThread(() -> { if (recognizedMrzView.getTag() != null) { recognizedMrzView.setTag(null); recognizedMrzView.animate().setDuration(200).alpha(0.0f).setInterpolator(CubicBezierInterpolator.DEFAULT).start(); } }); } private float averageProcessTime = 0; private long processTimesCount = 0; public void processShot(Bitmap bitmap) { final long from = SystemClock.elapsedRealtime(); try { Size size = cameraView.getPreviewSize(); if (currentType == TYPE_MRZ) { final MrzRecognizer.Result res = MrzRecognizer.recognize(bitmap, false); if (res != null && !TextUtils.isEmpty(res.firstName) && !TextUtils.isEmpty(res.lastName) && !TextUtils.isEmpty(res.number) && res.birthDay != 0 && (res.expiryDay != 0 || res.doesNotExpire) && res.gender != MrzRecognizer.Result.GENDER_UNKNOWN) { recognized = true; CameraController.getInstance().stopPreview(cameraView.getCameraSession()); AndroidUtilities.runOnUIThread(() -> { recognizedMrzView.setText(res.rawMRZ); recognizedMrzView.animate().setDuration(200).alpha(1f).setInterpolator(CubicBezierInterpolator.DEFAULT).start(); if (delegate != null) { delegate.didFindMrzInfo(res); } AndroidUtilities.runOnUIThread(this::finishFragment, 1200); }); return; } } else { int side = (int) (Math.min(size.getWidth(), size.getHeight()) / 1.5f); int x = (size.getWidth() - side) / 2; int y = (size.getHeight() - side) / 2; QrResult res = tryReadQr(null, size, x, y, side, bitmap); if (recognized) { recognizeIndex++; } if (res != null) { recognizeFailed = 0; recognizedText = res.text; if (!recognized) { recognized = true; qrLoading = delegate.processQr(recognizedText, () -> { if (cameraView != null && cameraView.getCameraSession() != null) { CameraController.getInstance().stopPreview(cameraView.getCameraSession()); } AndroidUtilities.runOnUIThread(() -> { if (delegate != null) { delegate.didFindQr(recognizedText); } finishFragment(); }); }); recognizedStart = SystemClock.elapsedRealtime(); AndroidUtilities.runOnUIThread(this::updateRecognized); } AndroidUtilities.runOnUIThread(() -> updateBounds(res.bounds)); } else if (recognized) { recognizeFailed++; if (recognizeFailed > 4 && !qrLoading) { recognized = false; recognizeIndex = 0; recognizedText = null; AndroidUtilities.runOnUIThread(() -> { updateRecognized(); int width = AndroidUtilities.displaySize.x, height = AndroidUtilities.displaySize.y, _side = (int) (Math.min(width, height) / 1.5f); AndroidUtilities.rectTmp.set( (width - _side) / 2f / (float) width, (height - _side) / 2f / (float) height, (width + _side) / 2f / (float) width, (height + _side) / 2f / (float) height ); updateBounds(AndroidUtilities.rectTmp); }); AndroidUtilities.runOnUIThread(requestShot, 500); return; } } if (((recognizeIndex == 0 && res != null && res.bounds == null) || (SystemClock.elapsedRealtime() - recognizedStart > 1000 && !qrLoading)) && recognizedText != null) { if (cameraView != null && cameraView.getCameraSession() != null) { CameraController.getInstance().stopPreview(cameraView.getCameraSession()); } AndroidUtilities.runOnUIThread(() -> { if (delegate != null) { delegate.didFindQr(recognizedText); } finishFragment(); }); } else if (recognized) { long delay = Math.max(16, 1000 / sps - (long) averageProcessTime); handler.postDelayed(() -> { if (cameraView != null) { processShot(cameraView.getTextureView().getBitmap()); } }, delay); } } } catch (Throwable ignore) { onNoQrFound(); } final long to = SystemClock.elapsedRealtime(); long timeout = to - from; averageProcessTime = (averageProcessTime * processTimesCount + timeout) / (++processTimesCount); FileLog.e("qr scan timeout = " + timeout + "ms (avg=" + averageProcessTime + "ms)"); processTimesCount = Math.max(processTimesCount, 30); if (!recognized) { AndroidUtilities.runOnUIThread(requestShot, 500); } } private class QrResult { String text; RectF bounds; public QrResult(String text, RectF bounds) { this.text = text; this.bounds = bounds; } public QrResult() {} } private QrResult tryReadQr(byte[] data, Size size, int x, int y, int side, Bitmap bitmap) { try { String text; RectF bounds = new RectF(); int width = 1, height = 1; LuminanceSource source; if (bitmap != null) { int[] intArray = new int[bitmap.getWidth() * bitmap.getHeight()]; bitmap.getPixels(intArray, 0, bitmap.getWidth(), 0, 0, bitmap.getWidth(), bitmap.getHeight()); source = new RGBLuminanceSource(bitmap.getWidth(), bitmap.getHeight(), intArray); width = bitmap.getWidth(); height = bitmap.getWidth(); } else { source = new PlanarYUVLuminanceSource(data, size.getWidth(), size.getHeight(), x, y, side, side, false); width = size.getWidth(); height = size.getHeight(); } Result result = qrReader.decode(new BinaryBitmap(new GlobalHistogramBinarizer(source))); if (result == null) { onNoQrFound(); return null; } text = result.getText(); if (result.getResultPoints() == null || result.getResultPoints().length == 0) { bounds = null; } else { float minX = Float.MAX_VALUE, maxX = Float.MIN_VALUE, minY = Float.MAX_VALUE, maxY = Float.MIN_VALUE; for (ResultPoint point : result.getResultPoints()) { minX = Math.min(minX, point.getX()); maxX = Math.max(maxX, point.getX()); minY = Math.min(minY, point.getY()); maxY = Math.max(maxY, point.getY()); } bounds.set(minX, minY, maxX, maxY); } if (TextUtils.isEmpty(text)) { onNoQrFound(); return null; } if (any) return new QrResult(text, bounds); if (needGalleryButton) { if (!text.startsWith("ton://transfer/")) { //onNoWalletFound(bitmap != null); return null; } Uri uri = Uri.parse(text); String path = uri.getPath().replace("/", ""); } QrResult qrResult = new QrResult(); if (bounds != null) { int paddingx = AndroidUtilities.dp(25), paddingy = AndroidUtilities.dp(15); bounds.set(bounds.left - paddingx, bounds.top - paddingy, bounds.right + paddingx, bounds.bottom + paddingy); bounds.set( bounds.left / (float) width, bounds.top / (float) height, bounds.right / (float) width, bounds.bottom / (float) height ); } qrResult.bounds = bounds; qrResult.text = text; return qrResult; } catch (Throwable ignore) { onNoQrFound(); } return null; } private boolean isQr() { return currentType == TYPE_QR || currentType == TYPE_QR_LOGIN; } @Override public ArrayList getThemeDescriptions() { ArrayList themeDescriptions = new ArrayList<>(); if (isQr()) { return themeDescriptions; } themeDescriptions.add(new ThemeDescription(fragmentView, ThemeDescription.FLAG_BACKGROUND, null, null, null, null, Theme.key_windowBackgroundWhite)); themeDescriptions.add(new ThemeDescription(actionBar, ThemeDescription.FLAG_BACKGROUND, null, null, null, null, Theme.key_windowBackgroundWhite)); themeDescriptions.add(new ThemeDescription(actionBar, ThemeDescription.FLAG_AB_ITEMSCOLOR, null, null, null, null, Theme.key_windowBackgroundWhiteGrayText2)); themeDescriptions.add(new ThemeDescription(actionBar, ThemeDescription.FLAG_AB_SELECTORCOLOR, null, null, null, null, Theme.key_actionBarWhiteSelector)); themeDescriptions.add(new ThemeDescription(titleTextView, ThemeDescription.FLAG_AB_SELECTORCOLOR, null, null, null, null, Theme.key_windowBackgroundWhiteBlackText)); themeDescriptions.add(new ThemeDescription(descriptionText, ThemeDescription.FLAG_AB_SELECTORCOLOR, null, null, null, null, Theme.key_windowBackgroundWhiteGrayText6)); return themeDescriptions; } }