Generate SSD PDF discovery from typed Jerboa

ober

32a195363e4aef5e76558efc3c1040e666ba5d45

diff --git a/templates/ssd-review.ss b/templates/ssd-review.ss
index 0b35dfb..0028aca 100644
--- a/templates/ssd-review.ss
+++ b/templates/ssd-review.ss
@@ -5986,13 +5986,13 @@
                   (activity : MainActivity)
                   (callback : (-> Unit))) : Unit
           (kotlin-member-call runOnUiThread))
-        (extern (mainActivityFindDownloadSsdPdfsRaw
+        (extern (ssdPdfFindDownloadSsdPdfsRaw
                   (activity : MainActivity)) : (List SsdPdfChoice)
-          (kotlin-member-call findDownloadSsdPdfs))
-        (extern (mainActivityFindSsdPdfsFromTreeRaw
+          (kotlin-call ssdPdfFindDownloadSsdPdfs))
+        (extern (ssdPdfFindSsdPdfsFromTreeRaw
                   (activity : MainActivity)
                   (uri : Uri)) : (List SsdPdfChoice)
-          (kotlin-member-call findSsdPdfsFromTree))
+          (kotlin-call ssdPdfFindSsdPdfsFromTree))
         (extern (sendClientLogBlockingRaw
                   (payload : JSONObject)
                   (readTimeoutMillis : Int32)) : Unit
@@ -7992,6 +7992,13 @@
                (payload : JSONObject)
                (readTimeoutMillis : Int32)) : Unit
           (sendClientLogBlockingRaw payload readTimeoutMillis))
+        (def (mainActivityFindDownloadSsdPdfsRaw
+               (activity : MainActivity)) : (List SsdPdfChoice)
+          (ssdPdfFindDownloadSsdPdfsRaw activity))
+        (def (mainActivityFindSsdPdfsFromTreeRaw
+               (activity : MainActivity)
+               (uri : Uri)) : (List SsdPdfChoice)
+          (ssdPdfFindSsdPdfsFromTreeRaw activity uri))
         (def (mainActivityLogDetectResultRaw
                (activity : MainActivity)
                (stage : String)
@@ -12733,104 +12740,280 @@
        "        mainActivityHandleReadStoragePermissionResult(this, requestCode, REQ_READ_STORAGE, REQ_OPEN_PDF, REQ_OPEN_SSD_PDF_TREE, PREF_SSD_PDF_TREE_URI, PREF_LAST_PDF_URI, Build.VERSION.SDK_INT, Build.VERSION_CODES.R, Build.VERSION_CODES.M, Manifest.permission.READ_EXTERNAL_STORAGE, PackageManager.PERMISSION_GRANTED, Settings.ACTION_MANAGE_APP_ALL_FILES_ACCESS_PERMISSION, Settings.ACTION_MANAGE_ALL_FILES_ACCESS_PERMISSION)"
        "    }"
        ""
-       "    internal fun findSsdPdfsFromTree(treeUri: Uri): List<SsdPdfChoice> {"
-       "        val maybeRoot = DocumentFile.fromTreeUri(this, treeUri)"
-       "        if (!documentFilePresent(maybeRoot)) return emptyList()"
-       "        val root = checkNotNull(maybeRoot)"
-       "        return findSsdPdfs(root)"
-       "    }"
-       ""
-       "    internal fun findDownloadSsdPdfs(): List<SsdPdfChoice> {"
-       "        val choices = mutableListOf<SsdPdfChoice>()"
-       "        choices.addAll(findDownloadSsdPdfsViaMediaStore())"
-       "        choices.addAll(findDownloadSsdPdfsViaFiles())"
-       "        return ssdPdfUniqueSortedLimitedChoices(choices, MAX_SSD_PDF_CHOICES)"
-       "    }"
-       ""
-       "    private fun findDownloadSsdPdfsViaMediaStore(): List<SsdPdfChoice> {"
-       "        if (sdkBefore(Build.VERSION.SDK_INT, Build.VERSION_CODES.Q)) return emptyList()"
-       "        val choices = mutableListOf<SsdPdfChoice>()"
-       "        val projection = arrayOf("
-       "            MediaStore.Downloads._ID,"
-       "            MediaStore.MediaColumns.DISPLAY_NAME,"
-       "            MediaStore.MediaColumns.RELATIVE_PATH,"
-       "            MediaStore.MediaColumns.MIME_TYPE"
-       "        )"
-       "        val selection = mediaStorePdfSelection(MediaStore.MediaColumns.MIME_TYPE, MediaStore.MediaColumns.DISPLAY_NAME)"
-       "        val selectionArgs = arrayOf(\"application/pdf\", \"%.pdf\")"
-       "        val downloadCursor = contentResolver.query("
-       "            MediaStore.Downloads.EXTERNAL_CONTENT_URI,"
-       "            projection,"
-       "            selection,"
-       "            selectionArgs,"
-       "            mediaStoreDisplayNameSort(MediaStore.MediaColumns.DISPLAY_NAME)"
-       "        )"
-       "        if (cursorPresent(downloadCursor)) checkNotNull(downloadCursor).use { cursor ->"
-       "            val idCol = cursor.getColumnIndexOrThrow(MediaStore.Downloads._ID)"
-       "            val nameCol = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.DISPLAY_NAME)"
-       "            val pathCol = cursor.getColumnIndexOrThrow(MediaStore.MediaColumns.RELATIVE_PATH)"
-       "            collectSsdPdfChoicesFromMediaCursor("
-       "                cursor,"
-       "                choices,"
-       "                idCol,"
-       "                nameCol,"
-       "                pathCol,"
-       "                MediaStore.Downloads.EXTERNAL_CONTENT_URI,"
-       "                MAX_SSD_PDF_CHOICES"
-       "            )"
-       "        }"
-       "        return choices"
-       "    }"
-       ""
-       "    private fun findDownloadSsdPdfsViaFiles(): List<SsdPdfChoice> {"
-       "        val dir = Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOWNLOADS)"
-       "        if (fileIsNotDirectory(dir)) return emptyList()"
-       "        val choices = mutableListOf<SsdPdfChoice>()"
-       "        try {"
-       "            collectSsdPdfChoicesFromDownloadFiles("
-       "                dir,"
-       "                choices,"
-       "                MAX_SSD_PDF_SCAN_DEPTH,"
-       "                MAX_SSD_PDF_CHOICES"
-       "            )"
-       "        } catch (_: Exception) {"
-       "        }"
-       "        return choices"
-       "    }"
-       ""
-       "    private fun findSsdPdfs(root: DocumentFile): List<SsdPdfChoice> {"
-       "        val choices = mutableListOf<SsdPdfChoice>()"
-       "        collectSsdPdfs(root, \"\", choices, 0)"
-       "        return ssdPdfUniqueSortedLimitedChoices(choices, MAX_SSD_PDF_CHOICES)"
-       "    }"
-       ""
-       "    private fun collectSsdPdfs("
-       "        dir: DocumentFile,"
-       "        prefix: String,"
-       "        choices: MutableList<SsdPdfChoice>,"
-       "        depth: Int"
-       "    ) {"
-       "        if (shouldStopSsdPdfTreeScan(depth, choices.size, MAX_SSD_PDF_CHOICES, MAX_SSD_PDF_SCAN_DEPTH)) return"
-       "        forEachDocumentFile("
-       "            dir.listFiles().sortedWith(compareBy<DocumentFile>({ ssdPdfDocumentNotDirectory(it) }, { nullableLowercaseOrEmpty(it.name) }))"
-       "        ) { child ->"
-       "            if (!shouldStopSsdPdfTreeScan(depth, choices.size, MAX_SSD_PDF_CHOICES, MAX_SSD_PDF_SCAN_DEPTH)) {"
-       "                val rawName = child.name"
-       "                if (nullableTextPresent(rawName)) {"
-       "                val name = nullableTextOrEmpty(rawName)"
-       "                val label = ssdPdfTreeLabel(prefix, name)"
-       "                if (ssdPdfDocumentIsDirectory(child)) {"
-       "                    collectSsdPdfs(child, label, choices, depth + 1)"
-       "                } else if (shouldIncludeSsdPdfDocument(child, name, choices.size, MAX_SSD_PDF_CHOICES)) {"
-       "                    choices.add(SsdPdfChoice(label, child.uri))"
-       "                }"
-       "                }"
-       "            }"
-       "            }"
-       "    }"
-	       ""
 	       "}"
        ))
+    (typed-kotlin-file "com/sfb/ssdreview/SsdPdfDiscovery.kt"
+      (kotlin-imports (android content ContentResolver)
+                      (android database Cursor)
+                      (android net Uri)
+                      (android os Build)
+                      (android os Environment)
+                      (android provider MediaStore)
+                      (androidx documentfile provider DocumentFile)
+                      (java io File))
+      (typed-library (com sfb ssdreview)
+        (export ssdPdfFindSsdPdfsFromTree ssdPdfFindDownloadSsdPdfs)
+        (type ContentResolver)
+        (type Cursor)
+        (type DocumentFile)
+        (type Exception)
+        (type File)
+        (type Int)
+        (type Int32)
+        (type MainActivity)
+        (type SsdPdfChoice)
+        (type Uri)
+        (extern (ssdPdfMaxScanDepthValue) : Int32
+          (kotlin-value MAX_SSD_PDF_SCAN_DEPTH))
+        (extern (ssdPdfMaxChoicesValue) : Int32
+          (kotlin-value MAX_SSD_PDF_CHOICES))
+        (extern (ssdPdfBuildSdkIntValue) : Int32
+          (kotlin-value Build VERSION SDK_INT))
+        (extern (ssdPdfBuildVersionCodesQValue) : Int32
+          (kotlin-value Build VERSION_CODES Q))
+        (extern (ssdPdfEnvironmentDirectoryDownloadsValue) : String
+          (kotlin-value Environment DIRECTORY_DOWNLOADS))
+        (extern (ssdPdfEnvironmentDownloadsDirectory
+                  (kind : String)) : File
+          (kotlin-call Environment getExternalStoragePublicDirectory))
+        (extern (ssdPdfDocumentFileFromTreeUri
+                  (activity : MainActivity)
+                  (uri : Uri)) : (Nullable DocumentFile)
+          (kotlin-call DocumentFile fromTreeUri))
+        (extern (ssdPdfDocumentFileListFiles
+                  (file : DocumentFile)) : (Array DocumentFile)
+          (kotlin-member-call listFiles))
+        (extern (ssdPdfDocumentFileName
+                  (file : DocumentFile)) : (Nullable String)
+          (kotlin-member-get name))
+        (extern (ssdPdfDocumentFileUri
+                  (file : DocumentFile)) : Uri
+          (kotlin-member-get uri))
+        (extern (ssdPdfMainActivityContentResolver
+                  (activity : MainActivity)) : ContentResolver
+          (kotlin-member-get contentResolver))
+        (extern (ssdPdfMediaStoreDownloadsIdColumn) : String
+          (kotlin-value MediaStore Downloads _ID))
+        (extern (ssdPdfMediaStoreDisplayNameColumn) : String
+          (kotlin-value MediaStore MediaColumns DISPLAY_NAME))
+        (extern (ssdPdfMediaStoreRelativePathColumn) : String
+          (kotlin-value MediaStore MediaColumns RELATIVE_PATH))
+        (extern (ssdPdfMediaStoreMimeTypeColumn) : String
+          (kotlin-value MediaStore MediaColumns MIME_TYPE))
+        (extern (ssdPdfMediaStoreDownloadsExternalContentUri) : Uri
+          (kotlin-value MediaStore Downloads EXTERNAL_CONTENT_URI))
+        (extern (ssdPdfStringListToArray
+                  (items : (List String))) : (Array String)
+          (kotlin-member-call toTypedArray))
+        (extern (ssdPdfContentResolverQuery
+                  (resolver : ContentResolver)
+                  (uri : Uri)
+                  (projection : (Nullable (Array String)))
+                  (selection : (Nullable String))
+                  (selectionArgs : (Nullable (Array String)))
+                  (sortOrder : (Nullable String))) : (Nullable Cursor)
+          (kotlin-member-call query))
+        (extern (ssdPdfCursorGetColumnIndexOrThrow
+                  (cursor : Cursor)
+                  (name : String)) : Int32
+          (kotlin-member-call getColumnIndexOrThrow))
+        (extern (ssdPdfCursorClose
+                  (cursor : Cursor)) : Unit
+          (kotlin-member-call close))
+        (def (ssdPdfDiscoveryCursorPresent
+               (cursor : (Nullable Cursor))) : Bool
+          (not (nullable-null? cursor)))
+        (def (ssdPdfDiscoveryProjection) : (Array String)
+          (ssdPdfStringListToArray
+            (list-of String
+              (ssdPdfMediaStoreDownloadsIdColumn)
+              (ssdPdfMediaStoreDisplayNameColumn)
+              (ssdPdfMediaStoreRelativePathColumn)
+              (ssdPdfMediaStoreMimeTypeColumn))))
+        (def (ssdPdfDiscoverySelectionArgs) : (Array String)
+          (ssdPdfStringListToArray
+            (list-of String "application/pdf" "%.pdf")))
+        (def (ssdPdfDocumentSortName
+               (file : DocumentFile)) : String
+          (string-lowercase
+            (nullableTextOrEmpty
+              (ssdPdfDocumentFileName file))))
+        (def (ssdPdfDocumentBefore?
+               (candidate : DocumentFile)
+               (existing : DocumentFile)) : Bool
+          (let ((candidateFile? (ssdPdfDocumentNotDirectory candidate))
+                (existingFile? (ssdPdfDocumentNotDirectory existing)))
+            (if (not (equal? candidateFile? existingFile?))
+              (not candidateFile?)
+              (< (string-compare-to
+                   (ssdPdfDocumentSortName candidate)
+                   (ssdPdfDocumentSortName existing))
+                 (int32 0)))))
+        (def (ssdPdfDocumentInsertIndex
+               (sorted : (MutableList DocumentFile))
+               (file : DocumentFile)) : Int32
+          (for/fold ((found (int32 -1)))
+                    ((i (in-range (int32 0) (list-size sorted))))
+            (if (>= found (int32 0))
+              found
+              (if (ssdPdfDocumentBefore? file (list-ref sorted i))
+                i
+                found))))
+        (def (ssdPdfDocumentInsertSorted
+               (sorted : (MutableList DocumentFile))
+               (file : DocumentFile)) : (MutableList DocumentFile)
+          (let ((insertAt (ssdPdfDocumentInsertIndex sorted file)))
+            (if (< insertAt (int32 0))
+              (begin
+                (mutable-list-add! sorted file)
+                sorted)
+              (let ((out (mutable-list-empty DocumentFile)))
+                (begin
+                  (for/fold ((ignored (int32 0)))
+                            ((i (in-range (int32 0) (list-size sorted))))
+                    (begin
+                      (if (= i insertAt)
+                        (begin
+                          (mutable-list-add! out file)
+                          (mutable-list-add! out (list-ref sorted i)))
+                        (mutable-list-add! out (list-ref sorted i)))
+                      ignored))
+                  out)))))
+        (def (ssdPdfSortedDocumentChildren
+               (dir : DocumentFile)) : (MutableList DocumentFile)
+          (let ((files (ssdPdfDocumentFileListFiles dir)))
+            (for/fold ((sorted (mutable-list-empty DocumentFile)))
+                      ((i (in-range (int32 0) (list-size files))))
+              (ssdPdfDocumentInsertSorted sorted (list-ref files i)))))
+        (def (ssdPdfCollectTree
+               (dir : DocumentFile)
+               (prefix : String)
+               (choices : (MutableList SsdPdfChoice))
+               (depth : Int32)) : Unit
+          (if (shouldStopSsdPdfTreeScan
+                depth
+                (list-size choices)
+                (ssdPdfMaxChoicesValue)
+                (ssdPdfMaxScanDepthValue))
+            (begin)
+            (forEachDocumentFile
+              (ssdPdfSortedDocumentChildren dir)
+              (lambda ((child : DocumentFile))
+                (if (shouldStopSsdPdfTreeScan
+                      depth
+                      (list-size choices)
+                      (ssdPdfMaxChoicesValue)
+                      (ssdPdfMaxScanDepthValue))
+                  (begin)
+                  (let ((rawName (ssdPdfDocumentFileName child)))
+                    (if (nullableTextPresent rawName)
+                      (let ((name (nullableTextOrEmpty rawName)))
+                        (let ((label (ssdPdfTreeLabel prefix name)))
+                          (if (ssdPdfDocumentIsDirectory child)
+                            (ssdPdfCollectTree
+                              child
+                              label
+                              choices
+                              (+ depth (int32 1)))
+                            (if (shouldIncludeSsdPdfDocument
+                                  child
+                                  name
+                                  (list-size choices)
+                                  (ssdPdfMaxChoicesValue))
+                              (mutable-list-add!
+                                choices
+                                (make-SsdPdfChoice
+                                  label
+                                  (ssdPdfDocumentFileUri child)))
+                              (begin)))))
+                      (begin))))))))
+        (def (ssdPdfFindSsdPdfs
+               (root : DocumentFile)) : (MutableList SsdPdfChoice)
+          (let ((choices (mutable-list-empty SsdPdfChoice)))
+            (begin
+              (ssdPdfCollectTree root "" choices (int32 0))
+              (ssdPdfUniqueSortedLimitedChoices
+                choices
+                (ssdPdfMaxChoicesValue)))))
+        (def (ssdPdfFindSsdPdfsFromTree
+               (activity : MainActivity)
+               (treeUri : Uri)) : (List SsdPdfChoice)
+          (let ((maybeRoot
+                  (ssdPdfDocumentFileFromTreeUri activity treeUri)))
+            (if (documentFilePresent maybeRoot)
+              (ssdPdfFindSsdPdfs (nullable-get maybeRoot))
+              (mutable-list-empty SsdPdfChoice))))
+        (def (ssdPdfFindDownloadSsdPdfsViaMediaStore
+               (activity : MainActivity)) : (MutableList SsdPdfChoice)
+          (if (sdkBefore
+                (ssdPdfBuildSdkIntValue)
+                (ssdPdfBuildVersionCodesQValue))
+            (mutable-list-empty SsdPdfChoice)
+            (let ((choices (mutable-list-empty SsdPdfChoice)))
+              (let ((downloadCursor
+                      (ssdPdfContentResolverQuery
+                        (ssdPdfMainActivityContentResolver activity)
+                        (ssdPdfMediaStoreDownloadsExternalContentUri)
+                        (nullable-some (ssdPdfDiscoveryProjection))
+                        (nullable-some
+                          (mediaStorePdfSelection
+                            (ssdPdfMediaStoreMimeTypeColumn)
+                            (ssdPdfMediaStoreDisplayNameColumn)))
+                        (nullable-some (ssdPdfDiscoverySelectionArgs))
+                        (nullable-some
+                          (mediaStoreDisplayNameSort
+                            (ssdPdfMediaStoreDisplayNameColumn))))))
+                (begin
+                  (if (ssdPdfDiscoveryCursorPresent downloadCursor)
+                    (let ((cursor (nullable-get downloadCursor)))
+                      (try-finally
+                        (collectSsdPdfChoicesFromMediaCursor
+                          cursor
+                          choices
+                          (ssdPdfCursorGetColumnIndexOrThrow
+                            cursor
+                            (ssdPdfMediaStoreDownloadsIdColumn))
+                          (ssdPdfCursorGetColumnIndexOrThrow
+                            cursor
+                            (ssdPdfMediaStoreDisplayNameColumn))
+                          (ssdPdfCursorGetColumnIndexOrThrow
+                            cursor
+                            (ssdPdfMediaStoreRelativePathColumn))
+                          (ssdPdfMediaStoreDownloadsExternalContentUri)
+                          (ssdPdfMaxChoicesValue))
+                        (ssdPdfCursorClose cursor)))
+                    (begin))
+                  choices)))))
+        (def (ssdPdfFindDownloadSsdPdfsViaFiles) : (MutableList SsdPdfChoice)
+          (let ((dir
+                  (ssdPdfEnvironmentDownloadsDirectory
+                    (ssdPdfEnvironmentDirectoryDownloadsValue))))
+            (if (fileIsNotDirectory dir)
+              (mutable-list-empty SsdPdfChoice)
+              (let ((choices (mutable-list-empty SsdPdfChoice)))
+                (begin
+                  (try
+                    (collectSsdPdfChoicesFromDownloadFiles
+                      dir
+                      choices
+                      (ssdPdfMaxScanDepthValue)
+                      (ssdPdfMaxChoicesValue))
+                    (catch (error : Exception)
+                      (begin)))
+                  choices)))))
+        (def (ssdPdfFindDownloadSsdPdfs
+               (activity : MainActivity)) : (List SsdPdfChoice)
+          (let ((choices (mutable-list-empty SsdPdfChoice)))
+            (begin
+              (mutable-list-add-all!
+                choices
+                (ssdPdfFindDownloadSsdPdfsViaMediaStore activity))
+              (mutable-list-add-all!
+                choices
+                (ssdPdfFindDownloadSsdPdfsViaFiles))
+              (ssdPdfUniqueSortedLimitedChoices
+                choices
+                (ssdPdfMaxChoicesValue)))))))
     (typed-kotlin-file "com/sfb/ssdreview/MainActivityConstants.kt"
       (typed-library (com sfb ssdreview)
         (export DPI