diff --git a/bindings/java/jni/auto_db_java.cpp b/bindings/java/jni/auto_db_java.cpp
index 969d87ba117d3a9b4959ad27feb27c116d42b62f..eb009c7d3f80e656ef39259b29e6c02bffff095f 100644
--- a/bindings/java/jni/auto_db_java.cpp
+++ b/bindings/java/jni/auto_db_java.cpp
@@ -79,7 +79,7 @@ TskAutoDbJava::initializeJni(JNIEnv * jniEnv, jobject jobj) {
     }
     m_callbackClass = (jclass)m_jniEnv->NewGlobalRef(localCallbackClass);
 
-    m_addImageMethodID = m_jniEnv->GetMethodID(m_callbackClass, "addImageInfo", "(IJLjava/lang/String;JLjava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)J");
+    m_addImageMethodID = m_jniEnv->GetMethodID(m_callbackClass, "addImageInfo", "(IJLjava/lang/String;JLjava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;[Ljava/lang/String;)J");
     if (m_addImageMethodID == NULL) {
         return TSK_ERR;
     }
@@ -184,7 +184,8 @@ TskAutoDbJava::getObjectInfo(uint64_t objId, TSK_DB_OBJECT** obj_info) {
 */
 TSK_RETVAL_ENUM
 TskAutoDbJava::addImageInfo(int type, TSK_OFF_T ssize, int64_t & objId, const string & timezone, TSK_OFF_T size, const string &md5,
-    const string& sha1, const string& sha256, const string& deviceId, const string& collectionDetails) {
+    const string& sha1, const string& sha256, const string& deviceId, const string& collectionDetails,
+    char** img_ptrs, int num_imgs) {
 
     const char *tz_cstr = timezone.c_str();
     jstring tzj = m_jniEnv->NewStringUTF(tz_cstr);
@@ -204,8 +205,18 @@ TskAutoDbJava::addImageInfo(int type, TSK_OFF_T ssize, int64_t & objId, const st
     const char *coll_cstr = collectionDetails.c_str();
     jstring collj = m_jniEnv->NewStringUTF(coll_cstr);
 
+    jobjectArray imgNamesj = (jobjectArray)m_jniEnv->NewObjectArray(
+        num_imgs,
+        m_jniEnv->FindClass("java/lang/String"),
+        m_jniEnv->NewStringUTF(""));
+
+    for (int i = 0; i < num_imgs; i++) {
+        m_jniEnv->SetObjectArrayElement(
+            imgNamesj, i, m_jniEnv->NewStringUTF(img_ptrs[i]));
+    }
+
     jlong objIdj = m_jniEnv->CallLongMethod(m_javaDbObj, m_addImageMethodID,
-        type, ssize, tzj, size, md5j, sha1j, sha256j, devIdj, collj);
+        type, ssize, tzj, size, md5j, sha1j, sha256j, devIdj, collj, imgNamesj);
     objId = (int64_t)objIdj;
 
     if (objId < 0) {
@@ -1003,13 +1014,6 @@ TskAutoDbJava::addImageDetails(const char* deviceId)
     } else {
         devId = "";
     }
-    if (TSK_OK != addImageInfo(m_img_info->itype, m_img_info->sector_size,
-          m_curImgId, m_curImgTZone, m_img_info->size, md5, sha1, "", devId, collectionDetails)) {
-        registerError();
-        return 1;
-    }
-
-
 
     char **img_ptrs;
 #ifdef TSK_WIN32
@@ -1043,14 +1047,12 @@ TskAutoDbJava::addImageDetails(const char* deviceId)
     img_ptrs = m_img_info->images;
 #endif
 
-    // Add the image names
-    for (int i = 0; i < m_img_info->num_img; i++) {
-        const char *img_ptr = img_ptrs[i];
 
-        if (TSK_OK != addImageName(m_curImgId, img_ptr, i)) {
-            registerError();
-            return 1;
-        }
+    if (TSK_OK != addImageInfo(m_img_info->itype, m_img_info->sector_size,
+        m_curImgId, m_curImgTZone, m_img_info->size, md5, sha1, "", devId, collectionDetails,
+        img_ptrs, m_img_info->num_img)) {
+        registerError();
+        return 1;
     }
 
 #ifdef TSK_WIN32
diff --git a/bindings/java/jni/auto_db_java.h b/bindings/java/jni/auto_db_java.h
index e6c5e68dd7217d784695749cf53c24df090db75d..a4a55e509cf2c9cbac9bb8db521e3095e19953d1 100644
--- a/bindings/java/jni/auto_db_java.h
+++ b/bindings/java/jni/auto_db_java.h
@@ -204,7 +204,7 @@ class TskAutoDbJava :public TskAuto {
 
     // JNI methods
     TSK_RETVAL_ENUM addImageInfo(int type, TSK_OFF_T ssize, int64_t & objId, const string & timezone, TSK_OFF_T size, const string &md5,
-        const string& sha1, const string& sha256, const string& deviceId, const string& collectionDetails);
+        const string& sha1, const string& sha256, const string& deviceId, const string& collectionDetails, char** img_ptrs, int num_imgs);
     TSK_RETVAL_ENUM addImageName(int64_t objId, char const* imgName, int sequence);
     TSK_RETVAL_ENUM addVsInfo(const TSK_VS_INFO* vs_info, int64_t parObjId, int64_t& objId);
     TSK_RETVAL_ENUM addPoolInfoAndVS(const TSK_POOL_INFO *pool_info, int64_t parObjId, int64_t& objId);
diff --git a/bindings/java/src/org/sleuthkit/datamodel/AddDataSourceCallbacks.java b/bindings/java/src/org/sleuthkit/datamodel/AddDataSourceCallbacks.java
new file mode 100644
index 0000000000000000000000000000000000000000..2e5ee97dc7d1090fcd1963cd45d2aba474730f7c
--- /dev/null
+++ b/bindings/java/src/org/sleuthkit/datamodel/AddDataSourceCallbacks.java
@@ -0,0 +1,40 @@
+/*
+ * SleuthKit Java Bindings
+ *
+ * Copyright 2020 Basis Technology Corp.
+ * Contact: carrier <at> sleuthkit <dot> org
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.sleuthkit.datamodel;
+
+import java.util.List;
+
+/**
+ * Provides callbacks at key points during the process of adding a data source to a case database.
+ */
+public interface AddDataSourceCallbacks {
+    /**
+     * Call when the data source has been completely added to the case database.
+     * 
+     * @param dataSourceObjectId The object ID of the new data source
+     */
+    void onDataSourceAdded(long dataSourceObjectId);
+    
+    /**
+     * Call to add a set of file object IDs that have been added to the database.
+     * 
+     * @param fileObjectIds List of file object IDs.
+     */
+    void onFilesAdded(List<Long> fileObjectIds);
+}
diff --git a/bindings/java/src/org/sleuthkit/datamodel/DefaultAddDataSourceCallbacks.java b/bindings/java/src/org/sleuthkit/datamodel/DefaultAddDataSourceCallbacks.java
new file mode 100644
index 0000000000000000000000000000000000000000..920f6079dcb79813f1faaacbdd30eb40988441ac
--- /dev/null
+++ b/bindings/java/src/org/sleuthkit/datamodel/DefaultAddDataSourceCallbacks.java
@@ -0,0 +1,38 @@
+/*
+ * SleuthKit Java Bindings
+ *
+ * Copyright 2020 Basis Technology Corp.
+ * Contact: carrier <at> sleuthkit <dot> org
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.sleuthkit.datamodel;
+
+import java.util.List;
+
+/**
+ * Do-nothing version of AddDataSourceCallbacks
+ */
+public class DefaultAddDataSourceCallbacks implements AddDataSourceCallbacks {
+
+	@Override
+	public void onDataSourceAdded(long dataSourceObjectId) {
+		// Do nothing
+	}
+
+	@Override
+	public void onFilesAdded(List<Long> fileObjectIds) {
+		// Do nothing
+	}
+	
+}
diff --git a/bindings/java/src/org/sleuthkit/datamodel/JniDbHelper.java b/bindings/java/src/org/sleuthkit/datamodel/JniDbHelper.java
index 4476402935fdc0e9c1f41b9d1e3fdbb6bfd1143e..5d61e57c7d66e18cf876584eb7f58e0b99c89e00 100644
--- a/bindings/java/src/org/sleuthkit/datamodel/JniDbHelper.java
+++ b/bindings/java/src/org/sleuthkit/datamodel/JniDbHelper.java
@@ -40,6 +40,7 @@ class JniDbHelper {
     
     private final SleuthkitCase caseDb;
     private CaseDbTransaction trans = null;
+    private final AddDataSourceCallbacks addDataSourceCallbacks;
     
     private final Map<Long, Long> fsIdToRootDir = new HashMap<>();
     private final Map<Long, TskData.TSK_FS_TYPE_ENUM> fsIdToFsType = new HashMap<>();
@@ -49,8 +50,9 @@ class JniDbHelper {
     private final List<FileInfo> batchedFiles = new ArrayList<>();
     private final List<LayoutRangeInfo> batchedLayoutRanges = new ArrayList<>();
     
-    JniDbHelper(SleuthkitCase caseDb) {
+    JniDbHelper(SleuthkitCase caseDb, AddDataSourceCallbacks addDataSourceCallbacks) {
         this.caseDb = caseDb;
+        this.addDataSourceCallbacks = addDataSourceCallbacks;
         trans = null;
     }
     
@@ -113,18 +115,23 @@ void finish() {
      */
     long addImageInfo(int type, long ssize, String timezone, 
             long size, String md5, String sha1, String sha256, String deviceId, 
-            String collectionDetails) {    
+            String collectionDetails, String[] paths) {    
         try {
             beginTransaction();
             long objId = caseDb.addImageJNI(TskData.TSK_IMG_TYPE_ENUM.valueOf(type), ssize, size,
                     timezone, md5, sha1, sha256, deviceId, collectionDetails, trans);
+            for (int i = 0;i < paths.length;i++) {
+                caseDb.addImageNameJNI(objId, paths[i], i, trans);
+            }
             commitTransaction();
+            
+			addDataSourceCallbacks.onDataSourceAdded(objId);
             return objId;
         } catch (TskCoreException ex) {
             logger.log(Level.SEVERE, "Error adding image to the database", ex);
             revertTransaction();
             return -1;
-        } 
+        }
     }
     
     /**
@@ -338,6 +345,7 @@ long addFile(long parentObjId,
      * @return 0 if successful, -1 if not
      */
     private long addBatchedFilesToDb() {
+        List<Long> newObjIds = new ArrayList<>();
         try {
             beginTransaction();
             for (FileInfo fileInfo : batchedFiles) {
@@ -360,6 +368,7 @@ private long addBatchedFilesToDb() {
                         null, TskData.FileKnown.UNKNOWN,
                         fileInfo.escaped_path, fileInfo.extension, 
                         false, trans);
+                    newObjIds.add(objId);
 
                     // If we're adding the root directory for the file system, cache it
                     if (fileInfo.parentObjId == fileInfo.fsObjId) {
@@ -380,45 +389,48 @@ private long addBatchedFilesToDb() {
                     logger.log(Level.SEVERE, "Error adding file to the database - parent object ID: " + computedParentObjId
                             + ", file system object ID: " + fileInfo.fsObjId + ", name: " + fileInfo.name, ex);
                     revertTransaction();
+                    batchedFiles.clear();
                     return -1;
                 }
             }
             commitTransaction();
+            addDataSourceCallbacks.onFilesAdded(newObjIds);
         } catch (TskCoreException ex) {
             logger.log(Level.SEVERE, "Error adding batched files to database", ex);
             revertTransaction();
+            batchedFiles.clear();
             return -1;
         }
         batchedFiles.clear();
         return 0;
     }
-	
-	/**
-	 * Look up the parent object ID for a file using the cache or the database.
-	 * 
-	 * @param fileInfo The file to find the parent of
-	 * 
-	 * @return Parent object ID
-	 * 
-	 * @throws TskCoreException 
-	 */
-	private long getParentObjId(FileInfo fileInfo) throws TskCoreException {
-		// Remove the final slash from the path unless we're in the root folder
-		String parentPath = fileInfo.escaped_path;
-		if(parentPath.endsWith("/") && ! parentPath.equals("/")) {
-			parentPath =  parentPath.substring(0, parentPath.lastIndexOf('/'));
-		}
+    
+    /**
+     * Look up the parent object ID for a file using the cache or the database.
+     * 
+     * @param fileInfo The file to find the parent of
+     * 
+     * @return Parent object ID
+     * 
+     * @throws TskCoreException 
+     */
+    private long getParentObjId(FileInfo fileInfo) throws TskCoreException {
+        // Remove the final slash from the path unless we're in the root folder
+        String parentPath = fileInfo.escaped_path;
+        if(parentPath.endsWith("/") && ! parentPath.equals("/")) {
+            parentPath =  parentPath.substring(0, parentPath.lastIndexOf('/'));
+        }
 
-		// Look up the parent
-		ParentCacheKey key = new ParentCacheKey(fileInfo.fsObjId, fileInfo.parMetaAddr, fileInfo.parSeq, parentPath);
-		if (parentDirCache.containsKey(key)) {
-			return parentDirCache.get(key);
-		} else {
-			// The parent wasn't found in the cache so do a database query
-			java.io.File parentAsFile = new java.io.File(parentPath);
-			return caseDb.findParentObjIdJNI(fileInfo.parMetaAddr, fileInfo.fsObjId, parentAsFile.getPath(), parentAsFile.getName(), trans);
-		}
-	}
+        // Look up the parent
+        ParentCacheKey key = new ParentCacheKey(fileInfo.fsObjId, fileInfo.parMetaAddr, fileInfo.parSeq, parentPath);
+        if (parentDirCache.containsKey(key)) {
+            return parentDirCache.get(key);
+        } else {
+            // The parent wasn't found in the cache so do a database query
+            java.io.File parentAsFile = new java.io.File(parentPath);
+            return caseDb.findParentObjIdJNI(fileInfo.parMetaAddr, fileInfo.fsObjId, parentAsFile.getPath(), parentAsFile.getName(), trans);
+        }
+    }
     
     /**
      * Add a layout file to the database. 
diff --git a/bindings/java/src/org/sleuthkit/datamodel/SleuthkitJNI.java b/bindings/java/src/org/sleuthkit/datamodel/SleuthkitJNI.java
index 4fe44e69095ea9e9a78621ed570f2a5b3417896d..ca10fccca3e7a63440edc703f4025d48d4053a76 100644
--- a/bindings/java/src/org/sleuthkit/datamodel/SleuthkitJNI.java
+++ b/bindings/java/src/org/sleuthkit/datamodel/SleuthkitJNI.java
@@ -424,7 +424,7 @@ void free() throws TskCoreException {
 		 *                          case database.
 		 */
 		long addImageInfo(long deviceObjId, List<String> imageFilePaths, String timeZone, SleuthkitCase skCase) throws TskCoreException {
-			JniDbHelper dbHelper = new JniDbHelper(skCase);
+			JniDbHelper dbHelper = new JniDbHelper(skCase, new DefaultAddDataSourceCallbacks());
 			try {
 				long tskAutoDbPointer = initializeAddImgNat(dbHelper, timezoneLongToShort(timeZone), false, false, false);
 				runOpenAndAddImgNat(tskAutoDbPointer, UUID.randomUUID().toString(), imageFilePaths.toArray(new String[0]), imageFilePaths.size(), timeZone);				
@@ -470,7 +470,7 @@ public class AddImageProcess {
 			private volatile long tskAutoDbPointer;
 			private boolean isCanceled;
 			private final SleuthkitCase skCase;
-			private final JniDbHelper dbHelper;
+			private JniDbHelper dbHelper;
 
 			/**
 			 * Constructs an object that encapsulates a multi-step process to
@@ -493,7 +493,7 @@ private AddImageProcess(String timeZone, boolean addUnallocSpace, boolean skipFa
 				tskAutoDbPointer = 0;
 				this.isCanceled = false;
 				this.skCase = skCase;
-				this.dbHelper = new JniDbHelper(skCase);
+				
 			}
 
 			/**
@@ -515,6 +515,31 @@ private AddImageProcess(String timeZone, boolean addUnallocSpace, boolean skipFa
 			 *                          the process)
 			 */
 			public void run(String deviceId, String[] imageFilePaths, int sectorSize) throws TskCoreException, TskDataException {
+				run(deviceId, imageFilePaths, sectorSize, new DefaultAddDataSourceCallbacks());
+			}
+			
+			/**
+			 * Starts the process of adding an image to the case database.
+			 * Either AddImageProcess.commit or AddImageProcess.revert MUST be
+			 * called after calling AddImageProcess.run.
+			 *
+			 * @param deviceId       An ASCII-printable identifier for the
+			 *                       device associated with the image that
+			 *                       should be unique across multiple cases
+			 *                       (e.g., a UUID).
+			 * @param imageFilePaths Full path(s) to the image file(s).
+			 * @param sectorSize     The sector size (use '0' for autodetect).
+			 * @param addDataSourceCallbacks  The callbacks to use to send data to ingest (may do nothing).
+			 *
+			 * @throws TskCoreException if a critical error occurs within the
+			 *                          SleuthKit.
+			 * @throws TskDataException if a non-critical error occurs within
+			 *                          the SleuthKit (should be OK to continue
+			 *                          the process)
+			 */
+			public void run(String deviceId, String[] imageFilePaths, int sectorSize, 
+					AddDataSourceCallbacks addDataSourceCallbacks) throws TskCoreException, TskDataException {
+				dbHelper = new JniDbHelper(skCase, addDataSourceCallbacks);
 				getTSKReadLock();
 				try {
 					long imageHandle = 0;
@@ -536,7 +561,7 @@ public void run(String deviceId, String[] imageFilePaths, int sectorSize) throws
 				} finally {
 					releaseTSKReadLock();
 				}
-			}
+			}			
 
 			/**
 			 * Stops the process of adding the image to the case database that
@@ -597,7 +622,9 @@ public synchronized long commit() throws TskCoreException {
 						throw new TskCoreException("AddImgProcess::commit: AutoDB pointer is NULL");
 					}
 
-					dbHelper.finish();
+					if (dbHelper != null) {
+						dbHelper.finish();
+					}
 
 					// Get the image ID and delete the object in the native code
 					long id = finishAddImgNat(tskAutoDbPointer);