Table of contents

Building the ScanMRZ Demo App

The MRZ Scanner User Guide builds ScanMRZBasic, the smallest app that scans an MRZ and shows the parsed data on a single screen. This page builds ScanMRZ, the complete demo app on top of it.

The MRZ Scanner Demo published on Google Play is this same implementation behind a more polished landing screen. Building ScanMRZ gives you the full scanning and result experience; the store app adds branding around it.

Everything on this page is presentation. It uses the same SDK calls the user guide covers — MRZScannerConfig, MRZScannerActivity.ResultContract, and the getters on MRZScanResult — and adds a result screen around them. None of it is required in order to use the MRZ Scanner.

Full source on GitHub: ScanMRZ (Java) and ScanMRZKt (Kotlin). Open android/samples in Android Studio and choose the matching run configuration.

What ScanMRZ adds

  ScanMRZBasic ScanMRZ
Screens one activity MainActivity plus a dedicated ResultActivity
Result data seven fields, plain text full field set, grouped into personal and document sections
Images portrait only portrait, plus a tabbed pager for the processed and original document images on both sides
Failed validation field turns amber amber, an error icon, an underline, and a tap-to-explain dialog
Camera denial error string error string plus an Open Settings action and recovery on resume

The result screen layouts

activity_results.xml

Create activity_results.xml in src/main/res/layout/. This layout contains the scrollable result area, an error text view for exception states, a header with the portrait and key identity details, a tab-based image pager for document photos, detailed personal and document info fields, the raw MRZ text, and the action buttons:

<?xml version="1.0" encoding="utf-8"?>
<androidx.constraintlayout.widget.ConstraintLayout xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    xmlns:tools="http://schemas.android.com/tools"
    android:id="@+id/main"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    tools:context=".ResultActivity">

    <TextView
        android:id="@+id/no_result_view"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:visibility="gone"
        android:gravity="center"
        android:textSize="16sp"
        app:layout_constraintBottom_toTopOf="@+id/ll_buttons"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

    <ScrollView
        android:id="@+id/result_view"
        android:layout_width="match_parent"
        android:layout_height="0dp"
        android:visibility="gone"
        app:layout_constraintBottom_toTopOf="@+id/ll_buttons"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent">

        <LinearLayout
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:orientation="vertical"
            android:padding="16dp">

            <LinearLayout
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:minHeight="100dp"
                android:orientation="horizontal">

                <LinearLayout
                    android:layout_width="0dp"
                    android:layout_height="match_parent"
                    android:layout_weight="4"
                    android:gravity="center_vertical"
                    android:orientation="vertical"
                    android:paddingVertical="8dp">

                    <TextView
                        android:id="@+id/tv_full_name"
                        android:layout_width="match_parent"
                        android:layout_height="wrap_content"
                        android:textSize="24sp"
                        android:textStyle="bold" />

                    <TextView
                        android:id="@+id/tv_gender_and_age"
                        android:layout_width="match_parent"
                        android:layout_height="wrap_content"
                        android:textSize="14sp" />

                    <TextView
                        android:id="@+id/tv_expiry"
                        android:layout_width="match_parent"
                        android:layout_height="wrap_content"
                        android:paddingTop="4dp"
                        android:textSize="14sp" />
                </LinearLayout>

                <View
                    android:layout_width="8dp"
                    android:layout_height="match_parent" />

                <ImageView
                    android:id="@+id/iv_portrait"
                    android:layout_width="0dp"
                    android:layout_height="match_parent"
                    android:layout_weight="1.2"
                    android:contentDescription="Portrait"
                    android:src="@drawable/ic_portrait_placeholder" />

            </LinearLayout>

            <TextView
                android:id="@+id/tv_images_header"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:paddingTop="24dp"
                android:textColor="@color/white"
                android:textStyle="bold"
                android:visibility="gone" />

            <com.google.android.material.tabs.TabLayout
                android:id="@+id/tab_images"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:layout_marginTop="24dp"
                app:tabGravity="center"
                app:tabMode="fixed" />

            <androidx.viewpager2.widget.ViewPager2
                android:id="@+id/vp_images"
                android:layout_width="match_parent"
                android:layout_height="162dp"
                android:layout_marginTop="8dp"
                android:overScrollMode="never" />

            <TextView
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:paddingTop="24dp"
                android:text="Personal Info"
                android:textStyle="bold" />

            <LinearLayout
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:gravity="center_vertical"
                android:orientation="horizontal"
                android:paddingTop="8dp">

                <TextView
                    android:layout_width="0dp"
                    android:layout_height="wrap_content"
                    android:layout_weight="1"
                    android:text="Given Name" />

                <TextView
                    android:id="@+id/tv_given_name"
                    android:layout_width="0dp"
                    android:layout_height="wrap_content"
                    android:layout_weight="1"
                    android:textStyle="bold" />
            </LinearLayout>

            <LinearLayout
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:gravity="center_vertical"
                android:orientation="horizontal"
                android:paddingTop="8dp">

                <TextView
                    android:layout_width="0dp"
                    android:layout_height="wrap_content"
                    android:layout_weight="1"
                    android:text="Surname" />

                <TextView
                    android:id="@+id/tv_surname"
                    android:layout_width="0dp"
                    android:layout_height="wrap_content"
                    android:layout_weight="1"
                    android:textStyle="bold" />
            </LinearLayout>

            <LinearLayout
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:gravity="center_vertical"
                android:orientation="horizontal"
                android:paddingTop="8dp">

                <TextView
                    android:layout_width="0dp"
                    android:layout_height="wrap_content"
                    android:layout_weight="1"
                    android:text="Date of Birth" />

                <TextView
                    android:id="@+id/tv_date_of_birth"
                    android:layout_width="0dp"
                    android:layout_height="wrap_content"
                    android:layout_weight="1"
                    android:textStyle="bold" />
            </LinearLayout>

            <LinearLayout
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:gravity="center_vertical"
                android:orientation="horizontal"
                android:paddingTop="8dp">

                <TextView
                    android:layout_width="0dp"
                    android:layout_height="wrap_content"
                    android:layout_weight="1"
                    android:text="Gender" />

                <TextView
                    android:id="@+id/tv_gender"
                    android:layout_width="0dp"
                    android:layout_height="wrap_content"
                    android:layout_weight="1"
                    android:textStyle="bold" />
            </LinearLayout>

            <LinearLayout
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:gravity="center_vertical"
                android:orientation="horizontal"
                android:paddingTop="8dp">

                <TextView
                    android:layout_width="0dp"
                    android:layout_height="wrap_content"
                    android:layout_weight="1"
                    android:text="Nationality" />

                <TextView
                    android:id="@+id/tv_nationality"
                    android:layout_width="0dp"
                    android:layout_height="wrap_content"
                    android:layout_weight="1"
                    android:textStyle="bold" />
            </LinearLayout>

            <TextView
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:paddingTop="24dp"
                android:text="Document Info"
                android:textStyle="bold" />

            <LinearLayout
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:gravity="center_vertical"
                android:orientation="horizontal"
                android:paddingTop="8dp">

                <TextView
                    android:layout_width="0dp"
                    android:layout_height="wrap_content"
                    android:layout_weight="1"
                    android:text="Doc. Type" />

                <TextView
                    android:id="@+id/tv_doc_type"
                    android:layout_width="0dp"
                    android:layout_height="wrap_content"
                    android:layout_weight="1"
                    android:textStyle="bold" />
            </LinearLayout>

            <LinearLayout
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:gravity="center_vertical"
                android:orientation="horizontal"
                android:paddingTop="8dp">

                <TextView
                    android:layout_width="0dp"
                    android:layout_height="wrap_content"
                    android:layout_weight="1"
                    android:text="Doc. Number" />

                <TextView
                    android:id="@+id/tv_doc_number"
                    android:layout_width="0dp"
                    android:layout_height="wrap_content"
                    android:layout_weight="1"
                    android:textStyle="bold" />
            </LinearLayout>

            <LinearLayout
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:gravity="center_vertical"
                android:orientation="horizontal"
                android:paddingTop="8dp">

                <TextView
                    android:layout_width="0dp"
                    android:layout_height="wrap_content"
                    android:layout_weight="1"
                    android:text="Expiry Date" />

                <TextView
                    android:id="@+id/tv_expiry_date"
                    android:layout_width="0dp"
                    android:layout_height="wrap_content"
                    android:layout_weight="1"
                    android:textStyle="bold" />
            </LinearLayout>

            <TextView
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:paddingTop="24dp"
                android:text="Raw MRZ Text"
                android:textStyle="bold" />

            <TextView
                android:id="@+id/tv_raw_mrz"
                android:layout_width="match_parent"
                android:layout_height="wrap_content"
                android:fontFamily="monospace"
                android:paddingTop="8dp"
                android:paddingBottom="16dp" />

        </LinearLayout>
    </ScrollView>

    <LinearLayout
        android:id="@+id/ll_buttons"
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="horizontal"
        android:padding="16dp"
        android:layout_marginBottom="16dp"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent">

        <androidx.appcompat.widget.AppCompatButton
            android:id="@+id/btn_rescan"
            android:layout_width="0dp"
            android:layout_height="48dp"
            android:layout_weight="1"
            android:text="Re-Scan" />

        <!-- Hidden by default. Takes the Re-Scan slot when the scan failed because
             camera access is unavailable — see Step 8. -->
        <androidx.appcompat.widget.AppCompatButton
            android:id="@+id/btn_open_settings"
            android:layout_width="0dp"
            android:layout_height="48dp"
            android:layout_weight="1"
            android:visibility="gone"
            android:text="Open Settings" />

        <androidx.appcompat.widget.AppCompatButton
            android:id="@+id/btn_return_home"
            android:layout_width="0dp"
            android:layout_height="48dp"
            android:layout_weight="1"
            android:text="Return Home" />

    </LinearLayout>

</androidx.constraintlayout.widget.ConstraintLayout>

ic_portrait_placeholder.xml

Create ic_portrait_placeholder.xml in src/main/res/drawable/. This vector drawable is shown as the portrait fallback when no portrait image was captured:

<vector xmlns:android="http://schemas.android.com/apk/res/android"
    android:width="88dp"
    android:height="88dp"
    android:viewportWidth="88"
    android:viewportHeight="88">
    <path
        android:pathData="M79.382,75.625C79.141,76.043 78.794,76.39 78.375,76.632C77.957,76.873 77.483,77 77,77H11C10.517,76.999 10.044,76.872 9.626,76.631C9.208,76.389 8.862,76.042 8.621,75.624C8.38,75.206 8.253,74.732 8.253,74.249C8.253,73.767 8.38,73.293 8.621,72.875C13.857,63.824 21.924,57.334 31.34,54.257C26.682,51.485 23.064,47.26 21.04,42.232C19.016,37.204 18.699,31.651 20.137,26.425C21.574,21.199 24.688,16.59 28.999,13.305C33.31,10.02 38.58,8.241 44,8.241C49.42,8.241 54.69,10.02 59.001,13.305C63.312,16.59 66.426,21.199 67.863,26.425C69.301,31.651 68.984,37.204 66.96,42.232C64.936,47.26 61.318,51.485 56.66,54.257C66.076,57.334 74.143,63.824 79.379,72.875C79.621,73.293 79.748,73.767 79.749,74.249C79.749,74.732 79.623,75.207 79.382,75.625Z"
        android:fillColor="#000000"/>
</vector>

colors.xml

Open colors.xml in src/main/res/values/ and add the amber used to flag MRZ fields that fail validation. Both the error icon below and ResultActivity reference it:

<color name="warning_amber">#FFC107</color>

ic_error_circle.xml

Create ic_error_circle.xml in src/main/res/drawable/. This vector drawable is the Material error glyph, displayed next to any field whose check digit fails validation:

<?xml version="1.0" encoding="utf-8"?>
<vector xmlns:android="http://schemas.android.com/apk/res/android"
    android:width="24dp"
    android:height="24dp"
    android:viewportWidth="24"
    android:viewportHeight="24">
    <path
        android:fillColor="@color/warning_amber"
        android:pathData="M12,2C6.48,2 2,6.48 2,12s4.48,10 10,10 10,-4.48 10,-10S17.52,2 12,2zM13,17h-2v-2h2v2zM13,13h-2V7h2v6z" />
</vector>

A circle-exclamation is used rather than a warning triangle because a failed check digit is an invalid-data state, not a caution. This matches Material’s convention of reserving the triangle for warnings.

Registering ResultActivity

Open AndroidManifest.xml and declare ResultActivity inside the <application> block:

<activity
    android:name=".ResultActivity"
    android:exported="false"
    android:screenOrientation="portrait" />

exported="false" matters here. MainActivity starts this screen with an explicit Intent, which never requires the activity to be exported — and leaving it exported would let any other app on the device launch it with a scan result of its own choosing.

MRZScannerActivity is already declared in the library manifest with a default screenOrientation of portrait. If you need to override its orientation, redeclare it in your app manifest with tools:replace="android:screenOrientation".

ImagesFragment

ImagesFragment renders one or two document images side by side. The ViewPager2 adapter in ResultActivity uses it to show the cropped and original scan images.

The images reach the fragment through setArguments(Bundle) rather than a constructor. FragmentManager recreates fragments by reflection, so it needs a public no-arg constructor and can only restore state it finds in the arguments Bundle. Because ImageData is not itself serializable into a Bundle, each image is encoded to JPEG bytes on the way in and decoded on the way out. The JPEG quality and maximum dimension are sized against the roughly 1 MB Binder transaction that carries saved instance state: 85% quality and a 1024 px cap keep each image under about 100 KB, comfortably clear of the limit even when both slots are used.

Do not pass ImageData through the fragment’s constructor. That form compiles, but ResultActivity will crash at super.onCreate(...) whenever the activity is recreated — after a configuration change, under Don’t keep activities, or following process death.

  • Java
  • Kotlin
  1. package com.dynamsoft.scanmrz;
    import android.graphics.Bitmap;
    import android.graphics.BitmapFactory;
    import android.os.Bundle;
    import android.view.Gravity;
    import android.view.LayoutInflater;
    import android.view.View;
    import android.view.ViewGroup;
    import android.widget.ImageView;
    import android.widget.LinearLayout;
    import androidx.annotation.NonNull;
    import androidx.annotation.Nullable;
    import androidx.fragment.app.Fragment;
    import com.dynamsoft.core.basic_structures.CoreException;
    import com.dynamsoft.core.basic_structures.ImageData;
    import java.io.ByteArrayOutputStream;
    public class ImagesFragment extends Fragment {
       private static final String ARG_IMAGE_1 = "image1";
       private static final String ARG_IMAGE_2 = "image2";
       private static final int JPEG_QUALITY = 85;
       private static final int MAX_DIMENSION_PX = 1024;
       public ImagesFragment() {
          super();
       }
       @NonNull
       public static ImagesFragment newInstance(@Nullable ImageData imageData1, @Nullable ImageData imageData2) {
          ImagesFragment fragment = new ImagesFragment();
          Bundle args = new Bundle();
          byte[] bytes1 = encode(imageData1);
          byte[] bytes2 = encode(imageData2);
          if (bytes1 != null) args.putByteArray(ARG_IMAGE_1, bytes1);
          if (bytes2 != null) args.putByteArray(ARG_IMAGE_2, bytes2);
          fragment.setArguments(args);
          return fragment;
       }
       @Nullable
       private static byte[] encode(@Nullable ImageData imageData) {
          if (imageData == null) return null;
          try {
             Bitmap bmp = downscaleIfNeeded(imageData.toBitmap());
             ByteArrayOutputStream out = new ByteArrayOutputStream();
             bmp.compress(Bitmap.CompressFormat.JPEG, JPEG_QUALITY, out);
             return out.toByteArray();
          } catch (CoreException ignored) {
             return null;
          }
       }
       @NonNull
       private static Bitmap downscaleIfNeeded(@NonNull Bitmap src) {
          int w = src.getWidth();
          int h = src.getHeight();
          int max = Math.max(w, h);
          if (max <= MAX_DIMENSION_PX) return src;
          float scale = (float) MAX_DIMENSION_PX / max;
          return Bitmap.createScaledBitmap(src, Math.round(w * scale), Math.round(h * scale), true);
       }
       @Nullable
       @Override
       public View onCreateView(@NonNull LayoutInflater inflater,
                                @Nullable ViewGroup container,
                                @Nullable Bundle savedInstanceState) {
          LinearLayout root = new LinearLayout(requireContext());
          root.setLayoutParams(new LinearLayout.LayoutParams(
                  ViewGroup.LayoutParams.MATCH_PARENT,
                  ViewGroup.LayoutParams.MATCH_PARENT
          ));
          root.setOrientation(LinearLayout.HORIZONTAL);
          root.setGravity(Gravity.CENTER_VERTICAL);
          root.setBaselineAligned(false);
          root.setClipToPadding(false);
          root.setClipChildren(false);
          return root;
       }
       @Override
       public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
          Bundle args = getArguments();
          if (args == null) return;
          LinearLayout root = (LinearLayout) view;
          byte[] bytes1 = args.getByteArray(ARG_IMAGE_1);
          byte[] bytes2 = args.getByteArray(ARG_IMAGE_2);
          addImageView(root, bytes1);
          if (bytes1 != null && bytes2 != null) {
             root.addView(new View(requireContext()),
                     new LinearLayout.LayoutParams(
                             (int) (16 * getResources().getDisplayMetrics().density),
                             ViewGroup.LayoutParams.MATCH_PARENT));
          }
          addImageView(root, bytes2);
       }
       private void addImageView(@NonNull LinearLayout root, @Nullable byte[] bytes) {
          if (bytes == null) return;
          Bitmap bmp = BitmapFactory.decodeByteArray(bytes, 0, bytes.length);
          if (bmp == null) return;
          ImageView iv = new ImageView(requireContext());
          LinearLayout.LayoutParams lp = new LinearLayout.LayoutParams(
                  0, ViewGroup.LayoutParams.MATCH_PARENT, 1f);
          iv.setLayoutParams(lp);
          iv.setScaleType(ImageView.ScaleType.FIT_CENTER);
          iv.setAdjustViewBounds(true);
          iv.setImageBitmap(bmp);
          root.addView(iv);
       }
    }
    
  2. package com.dynamsoft.scanmrz
    import android.graphics.Bitmap
    import android.graphics.BitmapFactory
    import android.os.Bundle
    import android.view.Gravity
    import android.view.LayoutInflater
    import android.view.View
    import android.view.ViewGroup
    import android.widget.ImageView
    import android.widget.LinearLayout
    import androidx.fragment.app.Fragment
    import com.dynamsoft.core.basic_structures.CoreException
    import com.dynamsoft.core.basic_structures.ImageData
    import java.io.ByteArrayOutputStream
    import kotlin.math.max
    import kotlin.math.roundToInt
    class ImagesFragment : Fragment() {
       override fun onCreateView(
          inflater: LayoutInflater,
          container: ViewGroup?,
          savedInstanceState: Bundle?
       ): View {
          val root = LinearLayout(requireContext())
          root.layoutParams = LinearLayout.LayoutParams(
             ViewGroup.LayoutParams.MATCH_PARENT,
             ViewGroup.LayoutParams.MATCH_PARENT
          )
          root.orientation = LinearLayout.HORIZONTAL
          root.gravity = Gravity.CENTER_VERTICAL
          root.isBaselineAligned = false
          root.clipToPadding = false
          root.clipChildren = false
          return root
       }
       override fun onViewCreated(view: View, savedInstanceState: Bundle?) {
          val args = arguments ?: return
          val root = view as LinearLayout
          val bytes1 = args.getByteArray(ARG_IMAGE_1)
          val bytes2 = args.getByteArray(ARG_IMAGE_2)
          addImageView(root, bytes1)
          if (bytes1 != null && bytes2 != null) {
             root.addView(
                View(requireContext()),
                LinearLayout.LayoutParams(
                   (16 * resources.displayMetrics.density).toInt(),
                   ViewGroup.LayoutParams.MATCH_PARENT
                )
             )
          }
          addImageView(root, bytes2)
       }
       private fun addImageView(root: LinearLayout, bytes: ByteArray?) {
          if (bytes == null) return
          val bmp = BitmapFactory.decodeByteArray(bytes, 0, bytes.size) ?: return
          val iv = ImageView(requireContext())
          iv.layoutParams = LinearLayout.LayoutParams(
             0, ViewGroup.LayoutParams.MATCH_PARENT, 1f
          )
          iv.scaleType = ImageView.ScaleType.FIT_CENTER
          iv.adjustViewBounds = true
          iv.setImageBitmap(bmp)
          root.addView(iv)
       }
       companion object {
          private const val ARG_IMAGE_1 = "image1"
          private const val ARG_IMAGE_2 = "image2"
          private const val JPEG_QUALITY = 85
          private const val MAX_DIMENSION_PX = 1024
          fun newInstance(imageData1: ImageData?, imageData2: ImageData?): ImagesFragment {
             val fragment = ImagesFragment()
             val args = Bundle()
             encode(imageData1)?.let { args.putByteArray(ARG_IMAGE_1, it) }
             encode(imageData2)?.let { args.putByteArray(ARG_IMAGE_2, it) }
             fragment.arguments = args
             return fragment
          }
          private fun encode(imageData: ImageData?): ByteArray? {
             if (imageData == null) return null
             return try {
                val bmp = downscaleIfNeeded(imageData.toBitmap())
                val out = ByteArrayOutputStream()
                bmp.compress(Bitmap.CompressFormat.JPEG, JPEG_QUALITY, out)
                out.toByteArray()
             } catch (ignored: CoreException) {
                null
             }
          }
          private fun downscaleIfNeeded(src: Bitmap): Bitmap {
             val w = src.width
             val h = src.height
             val maxDimension = max(w, h)
             if (maxDimension <= MAX_DIMENSION_PX) return src
             val scale = MAX_DIMENSION_PX.toFloat() / maxDimension
             return Bitmap.createScaledBitmap(src, (w * scale).roundToInt(), (h * scale).roundToInt(), true)
          }
       }
    }
    

ResultActivity

ResultActivity receives the MRZScanResult passed from MainActivity, handles all three result statuses, and populates the result screen with the extracted MRZ data, portrait image, and document images.

Per-field validation. The applyField helper renders any field whose getFieldValidationStatus is VS_FAILED in amber with a trailing error icon, underlines it to signal that it is tappable, and opens a short explanation when tapped. The top summary block is deliberately left unstyled: it combines several values on one line, so flagging it on a single field’s status would imply that everything on that line is suspect.

Reading the data. A finished scan normally carries data, but getData() is guarded rather than assumed — a null there shows the same empty-state text as an error instead of crashing. The gender field is title-cased with Locale.ROOT, since it is an ICAO code rather than localized text. Doc. Type is passed VS_NONE because it is derived from the MRZ code type rather than read from a field with its own check digit. The raw MRZ text is tappable for a reason that is easy to miss: a line-level failure can flag it when no individual field failed, which is what happens when the corruption lands in a field that carries no check digit of its own, such as name, nationality or sex.

Camera-permission failures. showCameraPermissionAction replaces Re-Scan with Open Settings for EC_CAMERA_PERMISSION_DENIED, and onResume re-checks the permission so that granting it in Settings and returning starts a new scan instead of leaving a stale error on screen. Open Settings is not offered for EC_CAMERA_PERMISSION_RESTRICTED, because device policy withholds the camera and the per-app toggle is absent from Settings in that state.

The code below reads one string resource, so add it to strings.xml alongside the entries the layouts use:

<string name="scan_no_data">Scan returned no data</string>

The image tabs. Tabs appear only when both sets came back, since a tab bar with one tab is a control with nothing to switch between. returnOriginalImage is off by default, so the usual case is a single set, which gets a plain Processed Image(s) header styled like the sections below it. Page 0 is the processed pair whenever processed images exist, otherwise the single page holds the original pair.

  • Java
  • Kotlin
  1. package com.dynamsoft.scanmrz;
    import android.Manifest;
    import android.content.Intent;
    import android.content.pm.PackageManager;
    import android.graphics.drawable.Drawable;
    import android.net.Uri;
    import android.os.Bundle;
    import android.provider.Settings;
    import android.text.SpannableString;
    import android.text.Spanned;
    import android.text.style.ImageSpan;
    import android.text.style.UnderlineSpan;
    import android.view.View;
    import android.widget.ImageView;
    import android.widget.TextView;
    import androidx.annotation.NonNull;
    import androidx.appcompat.app.AlertDialog;
    import androidx.appcompat.app.AppCompatActivity;
    import androidx.core.content.ContextCompat;
    import androidx.core.graphics.Insets;
    import androidx.core.view.ViewCompat;
    import androidx.core.view.WindowInsetsCompat;
    import androidx.fragment.app.Fragment;
    import androidx.viewpager2.adapter.FragmentStateAdapter;
    import androidx.viewpager2.widget.ViewPager2;
    import com.dynamsoft.core.basic_structures.CoreException;
    import com.dynamsoft.core.basic_structures.ImageData;
    import com.dynamsoft.dcp.EnumValidationStatus;
    import com.dynamsoft.mrzscannerbundle.ui.EnumDocumentSide;
    import com.dynamsoft.mrzscannerbundle.ui.MRZData;
    import com.dynamsoft.mrzscannerbundle.ui.MRZScanResult;
    import com.google.android.material.tabs.TabLayout;
    import com.google.android.material.tabs.TabLayoutMediator;
    import java.util.Locale;
    public class ResultActivity extends AppCompatActivity {
       public static final int REQUEST_CODE = 1024;
       public static final String EXTRA_RESULT = "RESULT";
       public static final String EXTRA_ACTION = "ACTION";
       public static final int ACTION_RESCAN = 0;
       public static final int ACTION_RETURN_HOME = 1;
       private boolean isShowingCameraPermissionError = false;
       @Override
       protected void onCreate(Bundle savedInstanceState) {
          super.onCreate(savedInstanceState);
          setContentView(R.layout.activity_results);
          ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main), (v, insets) -> {
             Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars());
             v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom);
             return insets;
          });
          MRZScanResult scanResult = (MRZScanResult) getIntent().getParcelableExtra(EXTRA_RESULT);
          if (scanResult != null) {
             showMRZScanResult(scanResult);
          }
          findViewById(R.id.btn_rescan).setOnClickListener(v -> {
             setResult(RESULT_OK, getIntent().putExtra(EXTRA_ACTION, ACTION_RESCAN));
             finish();
          });
          findViewById(R.id.btn_return_home).setOnClickListener(v -> {
             setResult(RESULT_OK, getIntent().putExtra(EXTRA_ACTION, ACTION_RETURN_HOME));
             finish();
          });
       }
       @Override
       protected void onResume() {
          super.onResume();
          if (isShowingCameraPermissionError && hasCameraPermission()) {
             setResult(RESULT_OK, getIntent().putExtra(EXTRA_ACTION, ACTION_RESCAN));
             finish();
          }
       }
       private boolean hasCameraPermission() {
          return ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA)
                  == PackageManager.PERMISSION_GRANTED;
       }
       private void showMRZScanResult(MRZScanResult result) {
          if (result.getResultStatus() == MRZScanResult.EnumResultStatus.RS_CANCELED) {
             setResult(RESULT_OK, getIntent().putExtra(EXTRA_ACTION, ACTION_RETURN_HOME));
             finish();
             return;
          }
          if (result.getResultStatus() == MRZScanResult.EnumResultStatus.RS_EXCEPTION) {
             findViewById(R.id.result_view).setVisibility(View.GONE);
             TextView tvNoResult = findViewById(R.id.no_result_view);
             tvNoResult.setVisibility(View.VISIBLE);
             tvNoResult.setText(result.getErrorString());
             showCameraPermissionAction(result.getErrorCode());
             return;
          }
          MRZData data = result.getData();
          if (data == null) {
             findViewById(R.id.result_view).setVisibility(View.GONE);
             TextView tvNoData = findViewById(R.id.no_result_view);
             tvNoData.setVisibility(View.VISIBLE);
             tvNoData.setText(R.string.scan_no_data);
             return;
          }
          findViewById(R.id.result_view).setVisibility(View.VISIBLE);
          findViewById(R.id.no_result_view).setVisibility(View.GONE);
          String sexText = data.getSex();
          String genderText = sexText.isEmpty() ? "" : sexText.substring(0, 1).toUpperCase(Locale.ROOT)
                  + sexText.substring(1).toLowerCase(Locale.ROOT);
          ((TextView) findViewById(R.id.tv_full_name)).setText((data.getFirstName() + " " + data.getLastName()).trim());
          ((TextView) findViewById(R.id.tv_gender_and_age)).setText(
                  genderText.isEmpty() && data.getAge() == 0
                          ? ""
                          : genderText + ", " + data.getAge() + " years old");
          ((TextView) findViewById(R.id.tv_expiry)).setText(data.getDateOfExpire().isEmpty() ? "" : "Expiry: " + data.getDateOfExpire());
          ImageView ivPortrait = findViewById(R.id.iv_portrait);
          ImageData portraitImage = result.getPortraitImage();
          if (portraitImage != null) {
             try {
                ivPortrait.setImageBitmap(portraitImage.toBitmap());
             } catch (CoreException ignored) {
             }
          } else {
             ivPortrait.setImageResource(R.drawable.ic_portrait_placeholder);
          }
          showImages(result);
          applyField(findViewById(R.id.tv_given_name), data.getFirstName(), data.getFieldValidationStatus("firstName"));
          applyField(findViewById(R.id.tv_surname), data.getLastName(), data.getFieldValidationStatus("lastName"));
          applyField(findViewById(R.id.tv_date_of_birth), data.getDateOfBirth(), data.getFieldValidationStatus("dateOfBirth"));
          applyField(findViewById(R.id.tv_gender), genderText, data.getFieldValidationStatus("sex"));
          applyField(findViewById(R.id.tv_nationality), data.getNationality(), data.getFieldValidationStatus("nationality"));
          String docTypeText;
          switch (data.getDocumentType() == null ? "" : data.getDocumentType()) {
             case "MRTD_TD1_ID":       docTypeText = "ID (TD1)"; break;
             case "MRTD_TD2_ID":       docTypeText = "ID (TD2)"; break;
             case "MRTD_TD3_PASSPORT": docTypeText = "Passport (TD3)"; break;
             default:                  docTypeText = ""; break;
          }
          applyField(findViewById(R.id.tv_doc_type), docTypeText, EnumValidationStatus.VS_NONE);
          applyField(findViewById(R.id.tv_doc_number), data.getDocumentNumber(), data.getFieldValidationStatus("documentNumber"));
          applyField(findViewById(R.id.tv_expiry_date), data.getDateOfExpire(), data.getFieldValidationStatus("dateOfExpire"));
          applyField(findViewById(R.id.tv_raw_mrz), data.getMrzText(), data.getFieldValidationStatus("mrzText"));
       }
       private void applyField(TextView tv, String value, int status) {
          boolean failed = status == EnumValidationStatus.VS_FAILED;
          boolean empty = value == null || value.isEmpty();
          String text = empty ? "N/A" : value;
          if (failed) {
             Drawable icon = ContextCompat.getDrawable(this, R.drawable.ic_error_circle);
             SpannableString spannable = new SpannableString(icon == null ? text : text + "  ");
             if (icon != null) {
                int iconSize = Math.round(tv.getTextSize() * 1.2f);
                icon.setBounds(0, 0, iconSize, iconSize);
                spannable.setSpan(new ImageSpan(icon, ImageSpan.ALIGN_BOTTOM),
                        spannable.length() - 1, spannable.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
             }
             spannable.setSpan(new UnderlineSpan(), 0, text.length(), Spanned.SPAN_EXCLUSIVE_EXCLUSIVE);
             tv.setText(spannable);
          } else {
             tv.setText(text);
          }
          tv.setTextColor(ContextCompat.getColor(this, failed ? R.color.warning_amber : R.color.white));
          if (failed) {
             tv.setOnClickListener(v -> showValidationInfoDialog());
          } else {
             tv.setOnClickListener(null);
             tv.setClickable(false);
          }
       }
       private void showValidationInfoDialog() {
          new AlertDialog.Builder(this)
                  .setTitle("Field validation warning")
                  .setMessage("This value doesn't match its check digit. The document may be invalid or altered.")
                  .setPositiveButton("OK", null)
                  .show();
       }
       private void showCameraPermissionAction(int errorCode) {
          if (errorCode != MRZScanResult.EnumErrorCode.EC_CAMERA_PERMISSION_DENIED) {
             return;
          }
          isShowingCameraPermissionError = true;
          findViewById(R.id.btn_rescan).setVisibility(View.GONE);
          View btnOpenSettings = findViewById(R.id.btn_open_settings);
          btnOpenSettings.setVisibility(View.VISIBLE);
          btnOpenSettings.setOnClickListener(v -> startActivity(
                  new Intent(Settings.ACTION_APPLICATION_DETAILS_SETTINGS,
                          Uri.fromParts("package", getPackageName(), null))));
       }
       private void showImages(MRZScanResult result) {
          ImageData mrzSideDocumentImage = result.getDocumentImage(EnumDocumentSide.DS_MRZ);
          ImageData oppositeSideDocumentImage = result.getDocumentImage(EnumDocumentSide.DS_OPPOSITE);
          ImageData mrzSideOriginalImage = result.getOriginalImage(EnumDocumentSide.DS_MRZ);
          ImageData oppositeSideOriginalImage = result.getOriginalImage(EnumDocumentSide.DS_OPPOSITE);
          TabLayout tabImages = findViewById(R.id.tab_images);
          ViewPager2 vpImages = findViewById(R.id.vp_images);
          TextView tvImagesHeader = findViewById(R.id.tv_images_header);
          boolean hasProcessed = mrzSideDocumentImage != null || oppositeSideDocumentImage != null;
          boolean hasOriginal = mrzSideOriginalImage != null || oppositeSideOriginalImage != null;
          if (!hasProcessed && !hasOriginal) {
             tvImagesHeader.setVisibility(View.GONE);
             tabImages.setVisibility(View.GONE);
             vpImages.setVisibility(View.GONE);
             return;
          }
          boolean showsTabs = hasProcessed && hasOriginal;
          tabImages.setVisibility(showsTabs ? View.VISIBLE : View.GONE);
          tvImagesHeader.setVisibility(showsTabs ? View.GONE : View.VISIBLE);
          tvImagesHeader.setText(hasProcessed ? "Processed Image(s)" : "Original Image(s)");
          vpImages.setVisibility(View.VISIBLE);
          vpImages.setAdapter(new FragmentStateAdapter(this) {
             @NonNull
             @Override
             public Fragment createFragment(int position) {
                if (position == 0 && hasProcessed) {
                   return ImagesFragment.newInstance(mrzSideDocumentImage, oppositeSideDocumentImage);
                } else {
                   return ImagesFragment.newInstance(mrzSideOriginalImage, oppositeSideOriginalImage);
                }
             }
             @Override
             public int getItemCount() {
                return hasProcessed && hasOriginal ? 2 : 1;
             }
          });
          if (showsTabs) {
             new TabLayoutMediator(tabImages, vpImages, (tab, position) ->
                     tab.setText(position == 0 ? "Processed" : "Original")).attach();
          }
       }
    }
    
  2. package com.dynamsoft.scanmrz
    import android.Manifest
    import android.content.Intent
    import android.content.pm.PackageManager
    import android.net.Uri
    import android.os.Bundle
    import android.provider.Settings
    import android.text.SpannableString
    import android.text.Spanned
    import android.text.style.ImageSpan
    import android.text.style.UnderlineSpan
    import android.view.View
    import android.widget.ImageView
    import android.widget.TextView
    import androidx.appcompat.app.AlertDialog
    import androidx.appcompat.app.AppCompatActivity
    import androidx.core.content.ContextCompat
    import androidx.core.view.ViewCompat
    import androidx.core.view.WindowInsetsCompat
    import androidx.fragment.app.Fragment
    import androidx.viewpager2.adapter.FragmentStateAdapter
    import androidx.viewpager2.widget.ViewPager2
    import com.dynamsoft.core.basic_structures.CoreException
    import com.dynamsoft.dcp.EnumValidationStatus
    import com.dynamsoft.mrzscannerbundle.ui.EnumDocumentSide
    import com.dynamsoft.mrzscannerbundle.ui.MRZScanResult
    import com.google.android.material.tabs.TabLayout
    import com.google.android.material.tabs.TabLayoutMediator
    import java.util.Locale
    import kotlin.math.roundToInt
    class ResultActivity : AppCompatActivity() {
       private var isShowingCameraPermissionError = false
       override fun onCreate(savedInstanceState: Bundle?) {
          super.onCreate(savedInstanceState)
          setContentView(R.layout.activity_results)
          ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets ->
             val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
             v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
             insets
          }
          @Suppress("DEPRECATION")
          val scanResult = intent.getParcelableExtra<MRZScanResult>(EXTRA_RESULT)
          if (scanResult != null) {
             showMRZScanResult(scanResult)
          }
          findViewById<View>(R.id.btn_rescan).setOnClickListener {
             setResult(RESULT_OK, intent.putExtra(EXTRA_ACTION, ACTION_RESCAN))
             finish()
          }
          findViewById<View>(R.id.btn_return_home).setOnClickListener {
             setResult(RESULT_OK, intent.putExtra(EXTRA_ACTION, ACTION_RETURN_HOME))
             finish()
          }
       }
       override fun onResume() {
          super.onResume()
          if (isShowingCameraPermissionError && hasCameraPermission()) {
             setResult(RESULT_OK, intent.putExtra(EXTRA_ACTION, ACTION_RESCAN))
             finish()
          }
       }
       private fun hasCameraPermission(): Boolean =
          ContextCompat.checkSelfPermission(this, Manifest.permission.CAMERA) ==
                  PackageManager.PERMISSION_GRANTED
       private fun showMRZScanResult(result: MRZScanResult) {
          if (result.resultStatus == MRZScanResult.EnumResultStatus.RS_CANCELED) {
             setResult(RESULT_OK, intent.putExtra(EXTRA_ACTION, ACTION_RETURN_HOME))
             finish()
             return
          }
          if (result.resultStatus == MRZScanResult.EnumResultStatus.RS_EXCEPTION) {
             findViewById<View>(R.id.result_view).visibility = View.GONE
             val tvNoResult = findViewById<TextView>(R.id.no_result_view)
             tvNoResult.visibility = View.VISIBLE
             tvNoResult.text = result.errorString
             showCameraPermissionAction(result.errorCode)
             return
          }
          val data = result.data
          if (data == null) {
             findViewById<View>(R.id.result_view).visibility = View.GONE
             val tvNoData = findViewById<TextView>(R.id.no_result_view)
             tvNoData.visibility = View.VISIBLE
             tvNoData.setText(R.string.scan_no_data)
             return
          }
          findViewById<View>(R.id.result_view).visibility = View.VISIBLE
          findViewById<View>(R.id.no_result_view).visibility = View.GONE
          val sexText = data.sex
          val genderText = if (sexText.isEmpty()) ""
          else sexText.substring(0, 1).uppercase(Locale.ROOT) + sexText.substring(1).lowercase(Locale.ROOT)
          findViewById<TextView>(R.id.tv_full_name).text = (data.firstName + " " + data.lastName).trim()
          findViewById<TextView>(R.id.tv_gender_and_age).text =
             if (genderText.isEmpty() && data.age == 0) ""
             else "$genderText, ${data.age} years old"
          findViewById<TextView>(R.id.tv_expiry).text =
             if (data.dateOfExpire.isEmpty()) "" else "Expiry: ${data.dateOfExpire}"
          val ivPortrait = findViewById<ImageView>(R.id.iv_portrait)
          val portraitImage = result.portraitImage
          if (portraitImage != null) {
             try {
                ivPortrait.setImageBitmap(portraitImage.toBitmap())
             } catch (ignored: CoreException) {
             }
          } else {
             ivPortrait.setImageResource(R.drawable.ic_portrait_placeholder)
          }
          showImages(result)
          applyField(findViewById(R.id.tv_given_name), data.firstName, data.getFieldValidationStatus("firstName"))
          applyField(findViewById(R.id.tv_surname), data.lastName, data.getFieldValidationStatus("lastName"))
          applyField(findViewById(R.id.tv_date_of_birth), data.dateOfBirth, data.getFieldValidationStatus("dateOfBirth"))
          applyField(findViewById(R.id.tv_gender), genderText, data.getFieldValidationStatus("sex"))
          applyField(findViewById(R.id.tv_nationality), data.nationality, data.getFieldValidationStatus("nationality"))
          val docTypeText = when (data.documentType ?: "") {
             "MRTD_TD1_ID" -> "ID (TD1)"
             "MRTD_TD2_ID" -> "ID (TD2)"
             "MRTD_TD3_PASSPORT" -> "Passport (TD3)"
             else -> ""
          }
          applyField(findViewById(R.id.tv_doc_type), docTypeText, EnumValidationStatus.VS_NONE)
          applyField(findViewById(R.id.tv_doc_number), data.documentNumber, data.getFieldValidationStatus("documentNumber"))
          applyField(findViewById(R.id.tv_expiry_date), data.dateOfExpire, data.getFieldValidationStatus("dateOfExpire"))
          applyField(findViewById(R.id.tv_raw_mrz), data.mrzText, data.getFieldValidationStatus("mrzText"))
       }
       private fun applyField(tv: TextView, value: String?, status: Int) {
          val failed = status == EnumValidationStatus.VS_FAILED
          val text = if (value.isNullOrEmpty()) "N/A" else value
          if (failed) {
             val icon = ContextCompat.getDrawable(this, R.drawable.ic_error_circle)
             val spannable = SpannableString(if (icon == null) text else "$text  ")
             if (icon != null) {
                val iconSize = (tv.textSize * 1.2f).roundToInt()
                icon.setBounds(0, 0, iconSize, iconSize)
                spannable.setSpan(
                   ImageSpan(icon, ImageSpan.ALIGN_BOTTOM),
                   spannable.length - 1, spannable.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE
                )
             }
             spannable.setSpan(UnderlineSpan(), 0, text.length, Spanned.SPAN_EXCLUSIVE_EXCLUSIVE)
             tv.text = spannable
          } else {
             tv.text = text
          }
          tv.setTextColor(
             ContextCompat.getColor(this, if (failed) R.color.warning_amber else R.color.white)
          )
          if (failed) {
             tv.setOnClickListener { showValidationInfoDialog() }
          } else {
             tv.setOnClickListener(null)
             tv.isClickable = false
          }
       }
       private fun showValidationInfoDialog() {
          AlertDialog.Builder(this)
             .setTitle("Field validation warning")
             .setMessage("This value doesn't match its check digit. The document may be invalid or altered.")
             .setPositiveButton("OK", null)
             .show()
       }
       private fun showCameraPermissionAction(errorCode: Int) {
          if (errorCode != MRZScanResult.EnumErrorCode.EC_CAMERA_PERMISSION_DENIED) {
             return
          }
          isShowingCameraPermissionError = true
          findViewById<View>(R.id.btn_rescan).visibility = View.GONE
          val btnOpenSettings = findViewById<View>(R.id.btn_open_settings)
          btnOpenSettings.visibility = View.VISIBLE
          btnOpenSettings.setOnClickListener {
             startActivity(
                Intent(
                   Settings.ACTION_APPLICATION_DETAILS_SETTINGS,
                   Uri.fromParts("package", packageName, null)
                )
             )
          }
       }
       private fun showImages(result: MRZScanResult) {
          val mrzSideDocumentImage = result.getDocumentImage(EnumDocumentSide.DS_MRZ)
          val oppositeSideDocumentImage = result.getDocumentImage(EnumDocumentSide.DS_OPPOSITE)
          val mrzSideOriginalImage = result.getOriginalImage(EnumDocumentSide.DS_MRZ)
          val oppositeSideOriginalImage = result.getOriginalImage(EnumDocumentSide.DS_OPPOSITE)
          val tabImages = findViewById<TabLayout>(R.id.tab_images)
          val vpImages = findViewById<ViewPager2>(R.id.vp_images)
          val tvImagesHeader = findViewById<TextView>(R.id.tv_images_header)
          val hasProcessed = mrzSideDocumentImage != null || oppositeSideDocumentImage != null
          val hasOriginal = mrzSideOriginalImage != null || oppositeSideOriginalImage != null
          if (!hasProcessed && !hasOriginal) {
             tvImagesHeader.visibility = View.GONE
             tabImages.visibility = View.GONE
             vpImages.visibility = View.GONE
             return
          }
          val showsTabs = hasProcessed && hasOriginal
          tabImages.visibility = if (showsTabs) View.VISIBLE else View.GONE
          tvImagesHeader.visibility = if (showsTabs) View.GONE else View.VISIBLE
          tvImagesHeader.text = if (hasProcessed) "Processed Image(s)" else "Original Image(s)"
          vpImages.visibility = View.VISIBLE
          vpImages.adapter = object : FragmentStateAdapter(this) {
             override fun createFragment(position: Int): Fragment {
                return if (position == 0 && hasProcessed) {
                   ImagesFragment.newInstance(mrzSideDocumentImage, oppositeSideDocumentImage)
                } else {
                   ImagesFragment.newInstance(mrzSideOriginalImage, oppositeSideOriginalImage)
                }
             }
             override fun getItemCount(): Int {
                return if (hasProcessed && hasOriginal) 2 else 1
             }
          }
          if (showsTabs) {
             TabLayoutMediator(tabImages, vpImages) { tab, position ->
                tab.text = if (position == 0) "Processed" else "Original"
             }.attach()
          }
       }
       companion object {
          const val REQUEST_CODE = 1024
          const val EXTRA_RESULT = "RESULT"
          const val EXTRA_ACTION = "ACTION"
          const val ACTION_RESCAN = 0
          const val ACTION_RETURN_HOME = 1
       }
    }
    

MainActivity

The user guide’s MainActivity renders the result on its own screen. Here it does one thing differently: it hands the result to ResultActivity and waits to hear what the user chose.

Two changes make that work. The launcher callback packs the MRZScanResult into an Intent and starts ResultActivity, and onActivityResult reads the action that comes back — relaunching the scanner when the user tapped Re-Scan. onActivityResult is deprecated in favour of the Activity Result APIs; the sample keeps it so the hand-off mirrors the Java version one-for-one.

The license below is a trial key, which needs a network connection. Request your own through Request a Trial License.

  • Java
  • Kotlin
  1. package com.dynamsoft.scanmrz;
    import android.content.Intent;
    import android.os.Bundle;
    import androidx.activity.EdgeToEdge;
    import androidx.activity.result.ActivityResultLauncher;
    import androidx.annotation.Nullable;
    import androidx.appcompat.app.AppCompatActivity;
    import androidx.core.graphics.Insets;
    import androidx.core.view.ViewCompat;
    import androidx.core.view.WindowInsetsCompat;
    import com.dynamsoft.mrzscannerbundle.ui.MRZScannerActivity;
    import com.dynamsoft.mrzscannerbundle.ui.MRZScannerConfig;
    public class MainActivity extends AppCompatActivity {
       private ActivityResultLauncher<MRZScannerConfig> launcher;
       private final MRZScannerConfig config = new MRZScannerConfig();
       @Override
       protected void onCreate(Bundle savedInstanceState) {
          super.onCreate(savedInstanceState);
          EdgeToEdge.enable(this);
          setContentView(R.layout.activity_main);
          ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main), (v, insets) -> {
             Insets systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars());
             v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom);
             return insets;
          });
          config.setLicense("DLS2eyJvcmdhbml6YXRpb25JRCI6IjIwMDAwMSJ9");
          launcher = registerForActivityResult(new MRZScannerActivity.ResultContract(), result -> {
             Intent intent = new Intent(this, ResultActivity.class);
             intent.putExtra(ResultActivity.EXTRA_RESULT, result);
             startActivityForResult(intent, ResultActivity.REQUEST_CODE);
          });
          findViewById(R.id.btn_start).setOnClickListener(v -> launcher.launch(config));
       }
       @Override
       protected void onActivityResult(int requestCode, int resultCode, @Nullable Intent data) {
          super.onActivityResult(requestCode, resultCode, data);
          if (requestCode == ResultActivity.REQUEST_CODE && resultCode == RESULT_OK) {
             int action = data.getIntExtra(ResultActivity.EXTRA_ACTION, ResultActivity.ACTION_RETURN_HOME);
             if (action == ResultActivity.ACTION_RESCAN) {
                launcher.launch(config);
             }
          }
       }
    }
    
  2. package com.dynamsoft.scanmrz
    import android.content.Intent
    import android.os.Bundle
    import android.view.View
    import androidx.activity.enableEdgeToEdge
    import androidx.activity.result.ActivityResultLauncher
    import androidx.appcompat.app.AppCompatActivity
    import androidx.core.view.ViewCompat
    import androidx.core.view.WindowInsetsCompat
    import com.dynamsoft.mrzscannerbundle.ui.MRZScannerActivity
    import com.dynamsoft.mrzscannerbundle.ui.MRZScannerConfig
    class MainActivity : AppCompatActivity() {
       private lateinit var launcher: ActivityResultLauncher<MRZScannerConfig>
       private val config = MRZScannerConfig()
       override fun onCreate(savedInstanceState: Bundle?) {
          super.onCreate(savedInstanceState)
          enableEdgeToEdge()
          setContentView(R.layout.activity_main)
          ViewCompat.setOnApplyWindowInsetsListener(findViewById(R.id.main)) { v, insets ->
             val systemBars = insets.getInsets(WindowInsetsCompat.Type.systemBars())
             v.setPadding(systemBars.left, systemBars.top, systemBars.right, systemBars.bottom)
             insets
          }
          config.license = "DLS2eyJvcmdhbml6YXRpb25JRCI6IjIwMDAwMSJ9"
          launcher = registerForActivityResult(MRZScannerActivity.ResultContract()) { result ->
             val intent = Intent(this, ResultActivity::class.java)
             intent.putExtra(ResultActivity.EXTRA_RESULT, result)
             startActivityForResult(intent, ResultActivity.REQUEST_CODE)
          }
          findViewById<View>(R.id.btn_start).setOnClickListener { launcher.launch(config) }
       }
       @Deprecated("Deprecated in Java")
       override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
          @Suppress("DEPRECATION")
          super.onActivityResult(requestCode, resultCode, data)
          if (requestCode == ResultActivity.REQUEST_CODE && resultCode == RESULT_OK) {
             val action = data?.getIntExtra(ResultActivity.EXTRA_ACTION, ResultActivity.ACTION_RETURN_HOME)
                ?: ResultActivity.ACTION_RETURN_HOME
             if (action == ResultActivity.ACTION_RESCAN) {
                launcher.launch(config)
             }
          }
       }
    }
    

The MRZScanResult travels through the Intent with no extra handling. Its images are reference counted, and the instance ResultActivity unparcels takes its own reference. See Results and Image Lifetime.

Next steps

This page is compatible for: