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.

What you’ll build: A production-ready Android driver’s license scanner app with a Material Design UI that decodes PDF417 barcodes in real time at 720P or 1080P, extracts all AAMVA fields (name, DOB, address, license number), and lets you compare Dynamsoft Capture Vision against Google ML Kit side-by-side.

Key Takeaways

  • Dynamsoft barcodereaderbundle decodes PDF417 driver’s license barcodes following the AAMVA specification entirely on-device — no server call required — and supports runtime resolution switching via EnumResolution.
  • CaptureVisionRouter combined with a drivers-license.json template file handles both barcode detection and field parsing in a single pipeline call.
  • Google ML Kit’s Barcode.DriverLicense class provides a simpler integration path but lacks runtime resolution control — useful for a quick baseline comparison.
  • Dense PDF417 symbols on some state driver’s licenses require 1080P resolution; the app’s resolution selector lets you benchmark accuracy across hardware.

Common Developer Questions

  • How do I scan and parse a US driver’s license barcode on Android?
  • How do I switch camera resolution between 720P and 1080P in an Android barcode scanner?
  • What is the difference between Dynamsoft Barcode Reader and Google ML Kit for PDF417 driver’s license recognition on Android?

Demo Video: Driver’s License Scanner App on Android

What This Driver’s License Scanner App Does

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

Sample Driver’s License Image for Testing

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

driver's license

Build the Android Driver’s License Scanner with Dynamsoft Barcode Reader

Step 1: Set Up the Android Project

  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: Activate the Dynamsoft SDK License

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()));
            }
        });
    }
    ...
}

Step 3: Create 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>

Step 4: Create 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;
    }
}

Step 5: Implement 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);
        });
    }
}

Step 6: Create the Scanner Fragment with Camera 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: Create Custom Drawable Resources for the 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: Create the Results Fragment and Display Parsed Driver’s License Fields

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();
         }
        
     }
    

Integrate 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 the Google ML Kit Scanner 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'

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>

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();
        }
    }
}

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);
                }
            }
        }
    }
}

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

Common Issues and Edge Cases

  • LicenseManager.initLicense() called too late: Initializing the license inside a Fragment’s onViewCreated() instead of at the top of MainActivity.onCreate() causes the first scan to fail silently. Always call initLicense() before any CaptureVisionRouter is instantiated.
  • PDF417 not detected at 720P on certain state licenses: Some densely encoded driver’s licenses (e.g., newer California or Texas formats) require 1080P. If CapturedResultReceiver returns zero items at 720P, switch to RESOLUTION_1080P via the resolution selector and retry before treating it as an unsupported card.
  • drivers-license.json template not found at runtime: The template must be in the assets/ folder (not res/raw/). If FileUtil.readStringFromAssets() returns null or an empty string, verify the file is named exactly drivers-license.json with no subdirectory prefix inside assets/.

Source Code

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