DEV Community

Xiao Ling
Xiao Ling

Posted on • Originally published at dynamsoft.com

How to Build a US Driver's License Scanner for Android with PDF417 Barcode Recognition

US driver's licenses store personal information using PDF417 barcode symbology according to the American Association of Motor Vehicle Administrators (AAMVA) specification. This comprehensive guide shows you how to build a production-ready Android driver's license scanner app with camera resolution options (720P/1080P) to compare scanning performance between Dynamsoft Capture Vision and Google ML Kit.

Demo Video: Driver's License Scanner App on Android

Features of This Driver's License Scanner App

Key Features:

  • Dual Scanning Engines: Compare Dynamsoft and Google ML Kit side-by-side
  • Resolution Selection: Switch between 720P and 1080P for performance testing
  • Real-time Resolution Display: See actual camera resolution on both engines
  • Modern Material Design UI: Beautiful home screen with resolution settings
  • Complete AAMVA Data Extraction: Parse all driver's license fields including name, address, DOB, license number, and more

Prerequisites

  • Android Studio (latest version recommended)
  • Android device with camera (Android 5.0+)
  • 30-day Trial License for Dynamsoft Barcode Reader

Sample Driver's License Image for Testing

Use this sample driver's license image to test the barcode scanning functionality:

driver's license

Building the Driver's License Scanner with Dynamsoft Barcode Reader

Step 1: Android Project Configuration

  1. In your project's build.gradle file, add the Dynamsoft Maven repository:

    allprojects {
        repositories {
            google()
            mavenCentral()
            maven { url "https://download2.dynamsoft.com/maven/aar" }
        }
    }
    
  2. In the module's build.gradle file, add these dependencies:

    implementation 'com.dynamsoft:barcodereaderbundle:11.2.5000'
    

Step 2: License Activation

Initialize the Dynamsoft license in your MainActivity.java:

protected void onCreate(Bundle savedInstanceState) {
    super.onCreate(savedInstanceState);
    if (savedInstanceState == null) {
        LicenseManager.initLicense("YOUR-LICENSE-KEY", this, (isSuccessful, error) -> {
            if (!isSuccessful) {
                error.printStackTrace();
                runOnUiThread(() -> ((TextView) findViewById(R.id.tv_license_error))
                    .setText("License initialization failed: " + error.getMessage()));
            }
        });
    }
    ...
}
Enter fullscreen mode Exit fullscreen mode

Step 3: Creating the Home Screen with Resolution Settings

Home Screen

Create fragment_selection.xml with a modern Material Design interface:

<?xml version="1.0" encoding="utf-8"?>
<ScrollView xmlns:android="http://schemas.android.com/apk/res/android"
    xmlns:app="http://schemas.android.com/apk/res-auto"
    android:layout_width="match_parent"
    android:layout_height="match_parent"
    android:fillViewport="true"
    android:background="#F5F5F5">

    <LinearLayout
        android:layout_width="match_parent"
        android:layout_height="wrap_content"
        android:orientation="vertical"
        android:gravity="center"
        android:padding="24dp">

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Driver License Scanner"
            android:textSize="28sp"
            android:textStyle="bold"
            android:textColor="#212121"
            android:layout_marginBottom="8dp"/>

        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Choose your scanning engine"
            android:textSize="16sp"
            android:textColor="#757575"
            android:layout_marginBottom="32dp"/>

        <!-- Resolution Selection Card -->
        <androidx.cardview.widget.CardView
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginBottom="24dp"
            app:cardCornerRadius="16dp"
            app:cardElevation="2dp">

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

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

                    <ImageView
                        android:layout_width="24dp"
                        android:layout_height="24dp"
                        android:src="@android:drawable/ic_menu_manage"
                        android:tint="#1976D2"
                        android:layout_marginEnd="12dp"/>

                    <TextView
                        android:layout_width="wrap_content"
                        android:layout_height="wrap_content"
                        android:text="Camera Resolution"
                        android:textSize="18sp"
                        android:textStyle="bold"
                        android:textColor="#212121"/>
                </LinearLayout>

                <RadioGroup
                    android:id="@+id/radio_group_resolution"
                    android:layout_width="match_parent"
                    android:layout_height="wrap_content"
                    android:orientation="horizontal"
                    android:background="#F5F5F5"
                    android:padding="4dp">

                    <RadioButton
                        android:id="@+id/radio_720p"
                        android:layout_width="0dp"
                        android:layout_height="48dp"
                        android:layout_weight="1"
                        android:text="720P"
                        android:textSize="16sp"
                        android:textStyle="bold"
                        android:textColor="@color/radio_text_color"
                        android:gravity="center"
                        android:button="@null"
                        android:background="@drawable/radio_selector"
                        android:layout_marginEnd="4dp"/>

                    <RadioButton
                        android:id="@+id/radio_1080p"
                        android:layout_width="0dp"
                        android:layout_height="48dp"
                        android:layout_weight="1"
                        android:text="1080P"
                        android:textSize="16sp"
                        android:textStyle="bold"
                        android:textColor="@color/radio_text_color"
                        android:gravity="center"
                        android:button="@null"
                        android:background="@drawable/radio_selector"
                        android:layout_marginStart="4dp"/>
                </RadioGroup>
            </LinearLayout>
        </androidx.cardview.widget.CardView>

        <!-- Engine Selection Cards -->
        <TextView
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:text="Scanning Engines"
            android:textSize="18sp"
            android:textStyle="bold"
            android:textColor="#212121"
            android:layout_marginBottom="16dp"
            android:layout_gravity="start"/>

        <!-- Dynamsoft Card -->
        <androidx.cardview.widget.CardView
            android:id="@+id/card_dynamsoft"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:layout_marginBottom="16dp"
            android:clickable="true"
            android:focusable="true"
            android:foreground="?android:attr/selectableItemBackground"
            app:cardCornerRadius="16dp"
            app:cardElevation="4dp">

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

                <LinearLayout
                    android:layout_width="56dp"
                    android:layout_height="56dp"
                    android:background="@drawable/circle_background_blue"
                    android:gravity="center"
                    android:layout_marginEnd="16dp">

                    <TextView
                        android:layout_width="wrap_content"
                        android:layout_height="wrap_content"
                        android:text="D"
                        android:textSize="24sp"
                        android:textStyle="bold"
                        android:textColor="#FFFFFF"/>
                </LinearLayout>

                <LinearLayout
                    android:layout_width="0dp"
                    android:layout_height="wrap_content"
                    android:layout_weight="1"
                    android:orientation="vertical">

                    <TextView
                        android:layout_width="wrap_content"
                        android:layout_height="wrap_content"
                        android:text="Dynamsoft"
                        android:textSize="20sp"
                        android:textStyle="bold"
                        android:textColor="#212121"
                        android:layout_marginBottom="4dp"/>

                    <TextView
                        android:layout_width="wrap_content"
                        android:layout_height="wrap_content"
                        android:text="Commercial-grade barcode scanner SDK"
                        android:textSize="14sp"
                        android:textColor="#757575"/>
                </LinearLayout>

                <ImageView
                    android:layout_width="24dp"
                    android:layout_height="24dp"
                    android:src="@android:drawable/ic_menu_send"
                    android:tint="#BDBDBD"/>
            </LinearLayout>
        </androidx.cardview.widget.CardView>

        <!-- Google Card -->
        <androidx.cardview.widget.CardView
            android:id="@+id/card_google"
            android:layout_width="match_parent"
            android:layout_height="wrap_content"
            android:clickable="true"
            android:focusable="true"
            android:foreground="?android:attr/selectableItemBackground"
            app:cardCornerRadius="16dp"
            app:cardElevation="4dp">

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

                <LinearLayout
                    android:layout_width="56dp"
                    android:layout_height="56dp"
                    android:background="@drawable/circle_background_green"
                    android:gravity="center"
                    android:layout_marginEnd="16dp">

                    <TextView
                        android:layout_width="wrap_content"
                        android:layout_height="wrap_content"
                        android:text="G"
                        android:textSize="24sp"
                        android:textStyle="bold"
                        android:textColor="#FFFFFF"/>
                </LinearLayout>

                <LinearLayout
                    android:layout_width="0dp"
                    android:layout_height="wrap_content"
                    android:layout_weight="1"
                    android:orientation="vertical">

                    <TextView
                        android:layout_width="wrap_content"
                        android:layout_height="wrap_content"
                        android:text="Google Mobile Vision"
                        android:textSize="20sp"
                        android:textStyle="bold"
                        android:textColor="#212121"
                        android:layout_marginBottom="4dp"/>

                    <TextView
                        android:layout_width="wrap_content"
                        android:layout_height="wrap_content"
                        android:text="Free on-device vision API"
                        android:textSize="14sp"
                        android:textColor="#757575"/>
                </LinearLayout>

                <ImageView
                    android:layout_width="24dp"
                    android:layout_height="24dp"
                    android:src="@android:drawable/ic_menu_send"
                    android:tint="#BDBDBD"/>
            </LinearLayout>
        </androidx.cardview.widget.CardView>

    </LinearLayout>
</ScrollView>
Enter fullscreen mode Exit fullscreen mode

Step 4: Creating the ViewModel for Shared State

Create MainViewModel.java to share data across fragments:

package com.dynamsoft.dcv.driverslicensescanner;

import androidx.lifecycle.ViewModel;

public class MainViewModel extends ViewModel {
    public String[] results;
    public String parsedText;

    // 0: 720P (1280x720), 1: 1080P (1920x1080)
    public int resolutionIndex = 0;

    public void reset() {
        results = null;
        parsedText = null;
    }
}
Enter fullscreen mode Exit fullscreen mode

Step 5: Implementing the Selection Fragment

Create SelectionFragment.java to handle resolution and engine selection:

package com.dynamsoft.dcv.driverslicensescanner.fragments;

import android.os.Bundle;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.cardview.widget.CardView;
import androidx.fragment.app.Fragment;
import androidx.lifecycle.ViewModelProvider;
import androidx.navigation.Navigation;

import com.dynamsoft.dcv.driverslicensescanner.MainViewModel;
import com.dynamsoft.dcv.driverslicensescanner.R;

public class SelectionFragment extends Fragment {
    private MainViewModel viewModel;

    @Nullable
    @Override
    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
                             @Nullable Bundle savedInstanceState) {
        return inflater.inflate(R.layout.fragment_selection, container, false);
    }

    @Override
    public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);

        viewModel = new ViewModelProvider(requireActivity()).get(MainViewModel.class);

        // Resolution selection
        android.widget.RadioGroup radioGroup = view.findViewById(R.id.radio_group_resolution);
        if (viewModel.resolutionIndex == 0) {
            radioGroup.check(R.id.radio_720p);
        } else {
            radioGroup.check(R.id.radio_1080p);
        }

        radioGroup.setOnCheckedChangeListener((group, checkedId) -> {
            if (checkedId == R.id.radio_720p) {
                viewModel.resolutionIndex = 0;
            } else if (checkedId == R.id.radio_1080p) {
                viewModel.resolutionIndex = 1;
            }
        });

        // Engine selection
        CardView cardDynamsoft = view.findViewById(R.id.card_dynamsoft);
        CardView cardGoogle = view.findViewById(R.id.card_google);

        cardDynamsoft.setOnClickListener(v -> {
            Navigation.findNavController(v).navigate(R.id.action_SelectionFragment_to_ScannerFragment);
        });

        cardGoogle.setOnClickListener(v -> {
            Navigation.findNavController(v).navigate(R.id.action_SelectionFragment_to_GoogleScannerFragment);
        });
    }
}
Enter fullscreen mode Exit fullscreen mode

Step 6: Creating the Scanner Fragment with Resolution Display

Scanner Fragment

  1. Create a new layout file named fragment_scanner.xml in the layout folder:

    <?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:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".fragments.ScannerFragment">
    
        <com.dynamsoft.dce.CameraView
            android:id="@+id/camera_view"
            android:layout_width="match_parent"
            android:layout_height="match_parent"/>
    
        <TextView
            android:id="@+id/tv_resolution"
            android:layout_width="wrap_content"
            android:layout_height="wrap_content"
            android:layout_margin="16dp"
            android:textColor="@android:color/white"
            android:textSize="16sp"
            app:layout_constraintStart_toStartOf="parent"
            app:layout_constraintTop_toTopOf="parent" />
    
    </androidx.constraintlayout.widget.ConstraintLayout>
    

    The layout includes a CameraView for displaying the camera preview and a TextView for showing the camera resolution.

  2. Place a drivers-license.json template file in the assets folder. This template defines how to decode the PDF417 barcode and extract driver's license information. Below is an example of the template:

    {
        "BarcodeFormatSpecificationOptions" : 
        [
            {
                "Name" : "bfs_pdf_417",
                "BarcodeBytesLengthRangeArray" : 
                [
                    {
                        "MaxValue" : 2147483647,
                        "MinValue" : 0
                    }
                ],
                "BarcodeFormatIds" : 
                [
                    "BF_DEFAULT",
                    "BF_POSTALCODE",
                    "BF_PHARMACODE",
                    "BF_NONSTANDARD_BARCODE",
                    "BF_DOTCODE"
                ],
                "BarcodeHeightRangeArray" : null,
                "BarcodeTextLengthRangeArray" : 
                [
                    {
                        "MaxValue" : 2147483647,
                        "MinValue" : 0
                    }
                ],
                "MinResultConfidence" : 30,
                "MirrorMode" : "MM_NORMAL",
                "PartitionModes" : 
                [
                    "PM_WHOLE_BARCODE",
                    "PM_ALIGNMENT_PARTITION"
                ]
            }
        ],
        "BarcodeReaderTaskSettingOptions" : 
        [
            {
                "Name" : "pdf_417_task",
                "BarcodeColourModes" : 
                [
                    {
                        "LightReflection" : 1,
                        "Mode" : "BICM_DARK_ON_LIGHT"
                    }
                ],
                "BarcodeFormatSpecificationNameArray" : 
                [
                    "bfs_pdf_417"
                ],
                "DeblurModes" : null,
                "LocalizationModes" : 
                [
                    {
                        "Mode" : "LM_CONNECTED_BLOCKS"
                    },
                    {
                        "Mode" : "LM_LINES"
                    },
                    {
                        "Mode" : "LM_STATISTICS"
                    }
                ],
                "MaxThreadsInOneTask" : 1,
                "SectionImageParameterArray" : 
                [
                    {
                        "ContinueWhenPartialResultsGenerated" : 1,
                        "ImageParameterName" : "ip_localize_barcode",
                        "Section" : "ST_REGION_PREDETECTION"
                    },
                    {
                        "ContinueWhenPartialResultsGenerated" : 1,
                        "ImageParameterName" : "ip_localize_barcode",
                        "Section" : "ST_BARCODE_LOCALIZATION"
                    },
                    {
                        "ContinueWhenPartialResultsGenerated" : 1,
                        "ImageParameterName" : "ip_decode_barcode",
                        "Section" : "ST_BARCODE_DECODING"
                    }
                ]
            }
        ],
        "CaptureVisionTemplates" : 
        [
            {
                "Name" : "ReadPDF417",
                "ImageROIProcessingNameArray" : 
                [
                    "roi_pdf_417"
                ],
                "ImageSource" : "",
                "MaxParallelTasks" : 4,
                "MinImageCaptureInterval" : 0,
                "OutputOriginalImage" : 0,
                "SemanticProcessingNameArray": [ "sp_pdf_417" ],
                "Timeout" : 10000
            }
        ],
        "GlobalParameter" : 
        {
            "MaxTotalImageDimension" : 0
        },
        "ImageParameterOptions" : 
        [
            {
                "Name" : "ip_localize_barcode",
                "BinarizationModes" : 
                [
                    {
                        "BinarizationThreshold" : -1,
                        "BlockSizeX" : 71,
                        "BlockSizeY" : 71,
                        "EnableFillBinaryVacancy" : 0,
                        "GrayscaleEnhancementModesIndex" : -1,
                        "Mode" : "BM_LOCAL_BLOCK",
                        "ThresholdCompensation" : 10
                    }
                ],
                "GrayscaleEnhancementModes" : 
                [
                    {
                        "Mode" : "GEM_GENERAL"
                    }
                ]
            },
            {
                "Name" : "ip_decode_barcode",
                "ScaleDownThreshold" : 99999
            },
            {
                "Name": "ip_recognize_text",
                "TextDetectionMode": {
                    "Mode": "TTDM_LINE",
                    "Direction": "HORIZONTAL",
                    "CharHeightRange": [
                        20,
                        1000,
                        1
                    ],
                    "Sensitivity": 7
                }
            }
        ],
        "TargetROIDefOptions" : 
        [
            {
                "Name" : "roi_pdf_417",
                "TaskSettingNameArray" : 
                [
                    "pdf_417_task"
                ]
            }
        ],
        "CharacterModelOptions": [
            {
                "Name" : "NumberLetter"
            }
        ],
        "SemanticProcessingOptions": [
            {
                "Name": "sp_pdf_417",
                "ReferenceObjectFilter": {
                    "ReferenceTargetROIDefNameArray": [
                        "roi_pdf_417"
                    ]
                },
                "TaskSettingNameArray": [
                    "dcp_pdf_417"
                ]
            }
        ],
        "CodeParserTaskSettingOptions": [
            {
                "Name": "dcp_pdf_417",
                "CodeSpecifications": ["AAMVA_DL_ID","AAMVA_DL_ID_WITH_MAG_STRIPE","SOUTH_AFRICA_DL"]
            }
        ]
    }
    
    
  3. Implement the barcode scanning functionality with camera resolution control in ScannerFragment.java:

    package com.dynamsoft.dcv.driverslicensescanner.fragments;
    
    import android.app.AlertDialog;
    import android.os.Bundle;
    import android.view.LayoutInflater;
    import android.view.View;
    import android.view.ViewGroup;
    
    import androidx.annotation.NonNull;
    import androidx.fragment.app.Fragment;
    import androidx.lifecycle.ViewModelProvider;
    import androidx.navigation.fragment.NavHostFragment;
    
    import com.dynamsoft.core.basic_structures.CompletionListener;
    import com.dynamsoft.cvr.CaptureVisionRouter;
    import com.dynamsoft.cvr.CaptureVisionRouterException;
    import com.dynamsoft.cvr.CapturedResultReceiver;
    import com.dynamsoft.dbr.DecodedBarcodesResult;
    import com.dynamsoft.dce.CameraEnhancer;
    import com.dynamsoft.dce.EnumResolution;
    import com.dynamsoft.dcp.ParsedResult;
    import com.dynamsoft.dcv.driverslicensescanner.FileUtil;
    import com.dynamsoft.dcv.driverslicensescanner.MainViewModel;
    import com.dynamsoft.dcv.driverslicensescanner.ParseUtil;
    import com.dynamsoft.dcv.driverslicensescanner.R;
    import com.dynamsoft.dcv.driverslicensescanner.databinding.FragmentScannerBinding;
    
    import java.util.Locale;
    
    public class ScannerFragment extends Fragment {
        private static final String TEMPLATE_ASSETS_FILE_NAME = "drivers-license.json";
        private static final String TEMPLATE_READ_PDF417 = "ReadPDF417";
        private FragmentScannerBinding binding;
        private CameraEnhancer mCamera;
        private CaptureVisionRouter mRouter;
        private MainViewModel viewModel;
    
        @Override
        public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
            binding = FragmentScannerBinding.inflate(inflater, container, false);
            viewModel = new ViewModelProvider(requireActivity()).get(MainViewModel.class);
            viewModel.reset();
            mCamera = new CameraEnhancer(binding.cameraView, getViewLifecycleOwner());
    
            if (viewModel.resolutionIndex == 0) {
                mCamera.setResolution(EnumResolution.RESOLUTION_720P);
            } else {
                mCamera.setResolution(EnumResolution.RESOLUTION_1080P);
            }
    
            if (mRouter == null) {
                initCaptureVisionRouter();
            }
            try {
                mRouter.setInput(mCamera);
            } catch (CaptureVisionRouterException e) {
                e.printStackTrace();
            }
            return binding.getRoot();
        }
    
        @Override
        public void onResume() {
            super.onResume();
            mCamera.open();
            try {
                android.util.Size size = mCamera.getResolution();
                if (size != null) {
                    binding.tvResolution.setText("Resolution: " + size.getWidth() + "x" + size.getHeight());
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
            mRouter.startCapturing(TEMPLATE_READ_PDF417, new CompletionListener() {
    
                @Override
                public void onSuccess() {
    
                }
    
                @Override
                public void onFailure(int errorCode, String errorString) {
                    requireActivity().runOnUiThread(() -> showDialog("Error", String.format(Locale.getDefault(),
                            "ErrorCode: %d %nErrorMessage: %s", errorCode, errorString)));
                }
            });
        }
    
        @Override
        public void onPause() {
            super.onPause();
            mCamera.close();
            mRouter.stopCapturing();
        }
    
        @Override
        public void onDestroyView() {
            super.onDestroyView();
            binding = null;
        }
    
        private void initCaptureVisionRouter() {
            mRouter = new CaptureVisionRouter(requireContext());
            try {
                String template = FileUtil.readAssetFileToString(requireContext(), TEMPLATE_ASSETS_FILE_NAME);
                mRouter.initSettings(template);
            } catch (CaptureVisionRouterException e) {
                e.printStackTrace();
            }
            mRouter.addResultReceiver(new CapturedResultReceiver() {
                @Override
                public void onDecodedBarcodesReceived(DecodedBarcodesResult result) {
                    if (result.getItems().length > 0) {
                        viewModel.parsedText = result.getItems()[0].getText();
                    }
                }
    
                @Override
                public void onParsedResultsReceived(ParsedResult result) {
                    if (result.getItems().length > 0) {
                        String[] displayStrings = ParseUtil.parsedItemToDisplayStrings(result.getItems()[0]);
                        if (displayStrings == null || displayStrings.length <= 1) {
                            showParsedText();
                            return;
                        }
                        viewModel.results = displayStrings;
                        requireActivity().runOnUiThread(() -> NavHostFragment.findNavController(ScannerFragment.this)
                                .navigate(R.id.action_ScannerFragment_to_ResultFragment));
                        mRouter.stopCapturing();
                    } else {
                        showParsedText();
                    }
                }
            });
        }
    
        private void showParsedText() {
            if (viewModel.parsedText != null && !viewModel.parsedText.isEmpty()) {
                requireActivity().runOnUiThread(() -> {
                    if (binding != null) {
                        // Display raw barcode text if parsing fails
                    }
                });
            }
        }
    
        private void showDialog(String title, String message) {
            new AlertDialog.Builder(requireContext())
                    .setCancelable(true)
                    .setPositiveButton("OK", null)
                    .setTitle(title)
                    .setMessage(message)
                    .show();
        }
    }
    

    Key Implementation Details:

    • Resolution Control: Sets camera resolution using EnumResolution.RESOLUTION_720P or RESOLUTION_1080P based on user selection
    • Template-Based Scanning: Uses drivers-license.json template file for optimized PDF417 recognition
    • CapturedResultReceiver: Anonymous inner class receives both barcode and parsed results
    • Code Parsing: Uses ParseUtil to format driver's license data from ParsedResultItem
    • Resolution Display: Shows actual camera resolution in TextView using mCamera.getResolution()
    • Lifecycle Management: Properly opens/closes camera in onResume()/onPause() to conserve resources
    • Error Handling: CompletionListener displays error dialogs if capture fails

Step 7: Creating Custom Drawable Resources for Resolution Selector

Create custom drawable files for the modern resolution selector UI:

  1. Create radio_selector.xml in res/drawable/:

    <?xml version="1.0" encoding="utf-8"?>
    <selector xmlns:android="http://schemas.android.com/apk/res/android">
        <item android:state_checked="true">
            <shape android:shape="rectangle">
                <solid android:color="#1976D2"/>
                <corners android:radius="8dp"/>
            </shape>
        </item>
        <item android:state_checked="false">
            <shape android:shape="rectangle">
                <solid android:color="#FFFFFF"/>
                <corners android:radius="8dp"/>
            </shape>
        </item>
    </selector>
    
  2. Create circle_background_blue.xml in res/drawable/:

    <?xml version="1.0" encoding="utf-8"?>
    <shape xmlns:android="http://schemas.android.com/apk/res/android"
        android:shape="oval">
        <solid android:color="#1976D2"/>
    </shape>
    
  3. Create circle_background_green.xml in res/drawable/:

    <?xml version="1.0" encoding="utf-8"?>
    <shape xmlns:android="http://schemas.android.com/apk/res/android"
        android:shape="oval">
        <solid android:color="#4CAF50"/>
    </shape>
    
  4. Create radio_text_color.xml in res/color/:

    <?xml version="1.0" encoding="utf-8"?>
    <selector xmlns:android="http://schemas.android.com/apk/res/android">
        <item android:state_checked="true" android:color="#FFFFFF"/>
        <item android:state_checked="false" android:color="#757575"/>
    </selector>
    

Step 8: Creating the Results Fragment to Display Driver's License Information

driver's license recognition result

  1. Create a layout file named fragment_result.xml containing a ListView to display the driver's license information:

    <?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:layout_width="match_parent"
        android:layout_height="match_parent"
        tools:context=".fragments.ResultFragment">
    
        <ListView
            android:id="@+id/lv_result"
            android:padding="10dp"
            android:layout_width="match_parent"
            android:layout_height="match_parent"/>
    
    </androidx.constraintlayout.widget.ConstraintLayout>
    
  2. Inflate the layout and load data from viewModel in the ResultFragment.java file:

    package com.dynamsoft.dcv.driverslicensescanner.fragments;
    
    import android.os.Bundle;
    import android.view.LayoutInflater;
    import android.view.View;
    import android.view.ViewGroup;
    import android.widget.ArrayAdapter;
    
    import androidx.annotation.NonNull;
    import androidx.fragment.app.Fragment;
    import androidx.lifecycle.ViewModelProvider;
    
    import com.dynamsoft.dcv.driverslicensescanner.MainViewModel;
    import com.dynamsoft.dcv.driverslicensescanner.databinding.FragmentResultBinding;
    
    public class ResultFragment extends Fragment {
        @Override
        public View onCreateView(@NonNull LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState) {
            FragmentResultBinding binding = FragmentResultBinding.inflate(inflater, container, false);
            MainViewModel viewModel = new ViewModelProvider(requireActivity()).get(MainViewModel.class);
            ArrayAdapter<String> adapter = new ArrayAdapter<>(requireContext(), android.R.layout.simple_list_item_1, viewModel.results);
            binding.lvResult.setAdapter(adapter);
            return binding.getRoot();
        }
    
    }
    

Driver's License Recognition with Google ML Kit

Google's ML Kit provides barcode scanning capabilities that can detect PDF417 barcodes commonly used on US driver's licenses. The Barcode.DriverLicense class provides predefined fields for extracting driver's license information.

Our implementation integrates ML Kit with a custom camera UI to provide resolution control and real-time scanning with visual feedback.

Complete Google ML Kit Implementation with Resolution Control

For a production-ready implementation that matches our Dynamsoft scanner with resolution selection, follow these steps:

Step 1: Add Google ML Kit Dependencies

In your module's build.gradle:

implementation 'com.google.mlkit:barcode-scanning:17.3.0'
Enter fullscreen mode Exit fullscreen mode

Step 2: Create Google Scanner Fragment Layout

Create fragment_google_scanner.xml:

<?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"
    android:layout_width="match_parent"
    android:layout_height="match_parent">

    <com.dynamsoft.dcv.driverslicensescanner.google.ui.camera.CameraSourcePreview
        android:id="@+id/preview"
        android:layout_width="match_parent"
        android:layout_height="match_parent"
        app:layout_constraintBottom_toBottomOf="parent"
        app:layout_constraintEnd_toEndOf="parent"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent">

        <com.dynamsoft.dcv.driverslicensescanner.google.ui.camera.GraphicOverlay
            android:id="@+id/graphicOverlay"
            android:layout_width="match_parent"
            android:layout_height="match_parent" />

    </com.dynamsoft.dcv.driverslicensescanner.google.ui.camera.CameraSourcePreview>

    <TextView
        android:id="@+id/tv_resolution"
        android:layout_width="wrap_content"
        android:layout_height="wrap_content"
        android:layout_margin="16dp"
        android:textColor="@android:color/white"
        android:textSize="16sp"
        android:elevation="10dp"
        app:layout_constraintStart_toStartOf="parent"
        app:layout_constraintTop_toTopOf="parent" />

</androidx.constraintlayout.widget.ConstraintLayout>
Enter fullscreen mode Exit fullscreen mode

Step 3: Implement GoogleScannerFragment with Resolution Support

Note: The actual implementation in the repository uses ML Kit's BarcodeScanning API with a custom camera UI based on Google Vision samples. The code below shows the core concepts. For the complete implementation including CameraSourcePreview, FrameProcessor, and GraphicOverlay, see the full source code.

Create GoogleScannerFragment.java:

package com.dynamsoft.dcv.driverslicensescanner.google;

import android.Manifest;
import android.content.pm.PackageManager;
import android.os.Bundle;
import android.util.SparseArray;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.widget.TextView;

import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.core.app.ActivityCompat;
import androidx.fragment.app.Fragment;
import androidx.lifecycle.ViewModelProvider;
import androidx.navigation.fragment.NavHostFragment;

import com.dynamsoft.dcv.driverslicensescanner.MainViewModel;
import com.dynamsoft.dcv.driverslicensescanner.R;
import com.google.android.gms.vision.CameraSource;
import com.google.android.gms.vision.Detector;
import com.google.android.gms.vision.barcode.Barcode;
import com.google.android.gms.vision.barcode.BarcodeDetector;

import java.io.IOException;

public class GoogleScannerFragment extends Fragment implements CameraSourcePreview.OnCameraStartedListener {
    private CameraSource cameraSource;
    private CameraSourcePreview cameraSourcePreview;
    private MainViewModel viewModel;
    private TextView tvResolution;

    @Nullable
    @Override
    public View onCreateView(@NonNull LayoutInflater inflater, @Nullable ViewGroup container,
                             @Nullable Bundle savedInstanceState) {
        return inflater.inflate(R.layout.fragment_google_scanner, container, false);
    }

    @Override
    public void onViewCreated(@NonNull View view, @Nullable Bundle savedInstanceState) {
        super.onViewCreated(view, savedInstanceState);

        viewModel = new ViewModelProvider(requireActivity()).get(MainViewModel.class);
        viewModel.reset();

        cameraSourcePreview = view.findViewById(R.id.preview);
        tvResolution = view.findViewById(R.id.tv_resolution);

        cameraSourcePreview.setOnCameraStartedListener(this);

        createCameraSource();
        startCameraSource();
    }

    private void createCameraSource() {
        BarcodeDetector barcodeDetector = new BarcodeDetector.Builder(requireContext())
                .setBarcodeFormats(Barcode.PDF417)
                .build();

        barcodeDetector.setProcessor(new Detector.Processor<Barcode>() {
            @Override
            public void receiveDetections(Detector.Detections<Barcode> detections) {
                SparseArray<Barcode> barcodes = detections.getDetectedItems();
                if (barcodes.size() > 0) {
                    Barcode barcode = barcodes.valueAt(0);
                    if (barcode.format == Barcode.PDF417) {
                        Barcode.DriverLicense driverLicense = barcode.driverLicense;
                        if (driverLicense != null) {
                            viewModel.parsedText = formatDriverLicenseData(driverLicense);
                            requireActivity().runOnUiThread(() -> {
                                NavHostFragment.findNavController(GoogleScannerFragment.this)
                                    .navigate(R.id.action_GoogleScannerFragment_to_ResultFragment);
                            });
                        }
                    }
                }
            }

            @Override
            public void release() {}
        });

        // Set camera resolution based on user selection (0: 720P, 1: 1080P)
        int width, height;
        if (viewModel.resolutionIndex == 0) {
            width = 1280;
            height = 720;
        } else {
            width = 1920;
            height = 1080;
        }

        cameraSource = new CameraSource.Builder(requireContext(), barcodeDetector)
                .setFacing(CameraSource.CAMERA_FACING_BACK)
                .setRequestedPreviewSize(width, height)
                .setAutoFocusEnabled(true)
                .build();
    }

    private void startCameraSource() {
        if (cameraSource != null) {
            try {
                if (ActivityCompat.checkSelfPermission(requireContext(), 
                        Manifest.permission.CAMERA) != PackageManager.PERMISSION_GRANTED) {
                    return;
                }
                cameraSourcePreview.start(cameraSource);
            } catch (IOException e) {
                e.printStackTrace();
                cameraSource.release();
                cameraSource = null;
            }
        }
    }

    @Override
    public void onCameraStarted(int width, int height, int orientation) {
        // Handle orientation for consistent display
        int displayWidth = width;
        int displayHeight = height;

        if (orientation == 90 || orientation == 270) {
            // Swap width and height for portrait orientation
            displayWidth = height;
            displayHeight = width;
        }

        final String resolutionText = displayWidth + "x" + displayHeight;
        requireActivity().runOnUiThread(() -> {
            tvResolution.setText(resolutionText);
            tvResolution.setVisibility(View.VISIBLE);
        });
    }

    private String formatDriverLicenseData(Barcode.DriverLicense dl) {
        StringBuilder sb = new StringBuilder();
        sb.append("Document Type: ").append(dl.documentType).append("\n");
        sb.append("First Name: ").append(dl.firstName).append("\n");
        sb.append("Middle Name: ").append(dl.middleName).append("\n");
        sb.append("Last Name: ").append(dl.lastName).append("\n");
        sb.append("Gender: ").append(dl.gender).append("\n");
        sb.append("Street: ").append(dl.addressStreet).append("\n");
        sb.append("City: ").append(dl.addressCity).append("\n");
        sb.append("State: ").append(dl.addressState).append("\n");
        sb.append("Zip: ").append(dl.addressZip).append("\n");
        sb.append("License Number: ").append(dl.licenseNumber).append("\n");
        sb.append("Issue Date: ").append(dl.issueDate).append("\n");
        sb.append("Expiry Date: ").append(dl.expiryDate).append("\n");
        sb.append("Birth Date: ").append(dl.birthDate).append("\n");
        sb.append("Issuing Country: ").append(dl.issuingCountry).append("\n");
        return sb.toString();
    }

    @Override
    public void onResume() {
        super.onResume();
        startCameraSource();
    }

    @Override
    public void onPause() {
        super.onPause();
        if (cameraSourcePreview != null) {
            cameraSourcePreview.stop();
        }
    }

    @Override
    public void onDestroyView() {
        super.onDestroyView();
        if (cameraSource != null) {
            cameraSource.release();
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Step 4: Implement Custom CameraSourcePreview

Create CameraSourcePreview.java to handle camera lifecycle:

package com.dynamsoft.dcv.driverslicensescanner.google;

import android.content.Context;
import android.util.AttributeSet;
import android.view.SurfaceHolder;
import android.view.SurfaceView;
import android.view.ViewGroup;

import com.google.android.gms.vision.CameraSource;

import java.io.IOException;

public class CameraSourcePreview extends ViewGroup {
    private SurfaceView surfaceView;
    private boolean startRequested = false;
    private boolean surfaceAvailable = false;
    private CameraSource cameraSource;
    private OnCameraStartedListener mListener;

    public interface OnCameraStartedListener {
        void onCameraStarted(int width, int height, int orientation);
    }

    public CameraSourcePreview(Context context, AttributeSet attrs) {
        super(context, attrs);
        surfaceView = new SurfaceView(context);
        surfaceView.getHolder().addCallback(new SurfaceCallback());
        addView(surfaceView);
    }

    public void setOnCameraStartedListener(OnCameraStartedListener listener) {
        mListener = listener;
    }

    public void start(CameraSource cameraSource) throws IOException {
        if (cameraSource == null) {
            stop();
        }

        this.cameraSource = cameraSource;

        if (this.cameraSource != null) {
            startRequested = true;
            startIfReady();
        }
    }

    public void stop() {
        if (cameraSource != null) {
            cameraSource.stop();
        }
    }

    private void startIfReady() throws IOException {
        if (startRequested && surfaceAvailable) {
            cameraSource.start(surfaceView.getHolder());

            if (mListener != null) {
                mListener.onCameraStarted(
                    cameraSource.getPreviewSize().getWidth(),
                    cameraSource.getPreviewSize().getHeight(),
                    0
                );
            }

            requestLayout();
            startRequested = false;
        }
    }

    private class SurfaceCallback implements SurfaceHolder.Callback {
        @Override
        public void surfaceCreated(SurfaceHolder surface) {
            surfaceAvailable = true;
            try {
                startIfReady();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

        @Override
        public void surfaceDestroyed(SurfaceHolder surface) {
            surfaceAvailable = false;
        }

        @Override
        public void surfaceChanged(SurfaceHolder holder, int format, int width, int height) {
        }
    }

    @Override
    protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
        int width = right - left;
        int height = bottom - top;

        if (cameraSource != null) {
            android.util.Size size = cameraSource.getPreviewSize();
            if (size != null) {
                float ratio = (float) size.getWidth() / size.getHeight();
                int layoutWidth = width;
                int layoutHeight = (int) (width / ratio);

                if (layoutHeight < height) {
                    layoutHeight = height;
                    layoutWidth = (int) (height * ratio);
                }

                for (int i = 0; i < getChildCount(); ++i) {
                    getChildAt(i).layout(0, 0, layoutWidth, layoutHeight);
                }
            }
        }
    }
}
Enter fullscreen mode Exit fullscreen mode

Key Features:

  • Resolution Control: Dynamically sets camera preview size based on user selection (720P or 1080P)
  • OnCameraStartedListener: Callback interface to display actual camera resolution
  • Orientation Handling: Swaps width/height for portrait mode to ensure consistent resolution display
  • PDF417 Detection: Specifically configured for driver's license barcodes
  • Auto-parsing: Automatically extracts all AAMVA driver's license fields

Source Code

https://github.com/yushulx/android-camera-barcode-mrz-document-scanner/tree/main/examples/driver-license-scanner

Top comments (0)