Complete typed SSD review runtime parity
ober
2afb60eb3313bd87598a22b3cf3a1d9dd877bdf6
--- a/templates/ssd-review-parts/GroupSuppression.ss +++ b/templates/ssd-review-parts/GroupSuppression.ss @@ -4,7 +4,8 @@ '((typed-kotlin-file "com/sfb/ssdreview/GroupSuppression.kt" (typed-library (com sfb ssdreview) - (export suppressOverlappingCandidateGroups) (type Float32) + (export suppressOverlappingCandidateGroups + removeOverlappingCandidateGroupsForTruth) (type Float32) (type Int32) (def (suppressionMinFloat32 (a : Float32) (b : Float32)) : @@ -110,4 +111,43 @@ reviewedGroup (list-ref removed i)) ignored)) - (list-size removed)))))))))) + (list-size removed)))))) + (def (removeOverlappingCandidateGroupsForTruth + (session : SsdSession) + (truthGroup : SsdGroup)) : Int32 + (let ((truthArea (rectArea (SsdGroup-bbox truthGroup)))) + (if (<= truthArea (float32 0.0)) + (int32 0) + (let ((removed + (for/fold + ((out (mutable-list-empty SsdGroup))) + ((i (in-range + (int32 0) + (list-size (SsdSession-groups session))))) + (let ((candidate + (list-ref + (SsdSession-groups session) + i))) + (if (and + (equal? + (SsdGroup-status candidate) + "candidate") + (>= (suppressionOverlapRatio + candidate + truthGroup + truthArea) + (float32 0.7))) + (begin + (mutable-list-add! out candidate) + out) + out))))) + (begin + (for/fold + ((ignored (int32 0))) + ((i (in-range (int32 0) (list-size removed)))) + (begin + (mutable-list-remove! + (SsdSession-groups session) + (list-ref removed i)) + ignored)) + (list-size removed)))))))))) --- a/templates/ssd-review-parts/LearnedFeatures.ss +++ b/templates/ssd-review-parts/LearnedFeatures.ss @@ -107,7 +107,8 @@ (json-object-put-string! out "label" - (json-object-opt-string group "label")) + (string-trim + (json-object-opt-string group "label"))) (json-object-put-string! out "box_type_id" --- a/templates/ssd-review-parts/SsdCell.ss +++ b/templates/ssd-review-parts/SsdCell.ss @@ -128,7 +128,7 @@ minH) (<= xGap (maxFloat32 - (float32 4.0) + (float32 14.0) (float32 (float32-round->int32 (* minW @@ -147,7 +147,7 @@ minW) (<= yGap (maxFloat32 - (float32 4.0) + (float32 14.0) (float32 (float32-round->int32 (* minH --- a/templates/ssd-review-parts/TruthApply.ss +++ b/templates/ssd-review-parts/TruthApply.ss @@ -26,13 +26,42 @@ (let ([parsed (ssdGroupFromTruthJson groupJson)]) (let ([group (pair-first parsed)]) (begin + (truthApplyRemoveGroupId! + (SsdSession-groups session) + (SsdGroup-id group)) + (truthApplyRemoveGroupId! + (SsdSession-suppressedGroups session) + (SsdGroup-id group)) (remapAndAddTruthCells session group (pair-second parsed) usedIds) - (suppressOverlappingCandidateGroups session group) + (removeOverlappingCandidateGroupsForTruth + session + group) (mutable-list-add! target group))))) + (def (truthApplyRemoveGroupId! + (groups : (MutableList SsdGroup)) + (id : String)) : Unit + (let ((matches + (for/fold + ((out (mutable-list-empty SsdGroup))) + ((i (in-range (int32 0) (list-size groups)))) + (let ((group (list-ref groups i))) + (if (equal? (SsdGroup-id group) id) + (begin + (mutable-list-add! out group) + out) + out))))) + (begin + (for/fold + ((ignored (int32 0))) + ((i (in-range (int32 0) (list-size matches)))) + (begin + (mutable-list-remove! groups (list-ref matches i)) + ignored)) + (begin)))) (def (applyTruthGroupArrayToSession (session : SsdSession) (groups : JSONArray) --- a/templates/ssd-review-parts/TruthStoreConstants.ss +++ b/templates/ssd-review-parts/TruthStoreConstants.ss @@ -13,23 +13,23 @@ MAX_AUTH_TOKEN_CHARS) (type Int32) (val DEFAULT_TRUTH_API_URL : String - "https://10.66.60.2:8797/api" (modifiers internal const)) + "http://127.0.0.1:8797/api" (modifiers internal const)) (val MAX_HTTP_TEXT_BYTES : Int (* (* (int 32) (int 1024)) (int 1024)) (modifiers internal const)) (val MAX_HTTP_COMPRESSED_BYTES : Int - (* (* (int 2) (int 1024)) (int 1024)) + (* (* (int 8) (int 1024)) (int 1024)) (modifiers internal const)) (val MAX_REMOTE_ZIP_BYTES : Int (* (* (int 96) (int 1024)) (int 1024)) (modifiers internal const)) (val MAX_ZIP_ENTRY_BYTES : Int - (* (* (int 4) (int 1024)) (int 1024)) + (* (* (int 32) (int 1024)) (int 1024)) (modifiers internal const)) (val MAX_ZIP_TOTAL_BYTES : Int - (* (* (int 64) (int 1024)) (int 1024)) + (* (* (int 512) (int 1024)) (int 1024)) (modifiers internal const)) - (val MAX_ZIP_ENTRIES : Int32 (int32 512) + (val MAX_ZIP_ENTRIES : Int32 (int32 8192) (modifiers internal const)) (val MAX_ZIP_DEPTH : Int32 (int32 8) (modifiers internal const)) @@ -40,11 +40,11 @@ (val RATIO_FLOOR_BYTES : Int (* (int 64) (int 1024)) (modifiers internal const)) (val MAX_STORAGE_BYTES : Int - (* (* (int 128) (int 1024)) (int 1024)) + (* (* (int 512) (int 1024)) (int 1024)) (modifiers internal const)) (val MAX_STORAGE_FILES : Int32 (int32 16384) (modifiers internal const)) - (val MAX_LEARNED_EXAMPLES : Int32 (int32 10000) + (val MAX_LEARNED_EXAMPLES : Int32 (int32 50000) (modifiers internal const)) (val MAX_JSONL_LINES : Int32 (int32 51200) (modifiers internal const)) --- a/templates/ssd-review-parts/TruthStoreJson.ss +++ b/templates/ssd-review-parts/TruthStoreJson.ss @@ -35,7 +35,11 @@ (json-object-opt-json-array truth "groups"))]) - (begin + (if (string-starts-with? + (json-object-opt-string truth "source_key") + "ssd-selftest-") + (begin) + (begin (for/fold ((ignored (int32 0))) ((i (in-range (int32 0) (json-array-length groups)))) @@ -46,15 +50,30 @@ (begin (if (nullable-null? group) (begin) - (json-array-put-json-object! - examples - (learnedExampleFromTruthGroup - truth - truthFileName - (nullable-get group)))) + (let ((value (nullable-get group))) + (let ((status + (json-object-opt-string + value "status")) + (label + (string-trim + (json-object-opt-string + value "label")))) + (if (and + (or (equal? status "reviewed") + (equal? status "approved")) + (and (not (string-blank? label)) + (not (string-ends-with? + label "?")))) + (json-array-put-json-object! + examples + (learnedExampleFromTruthGroup + truth + truthFileName + value)) + (begin))))) ignored)) ignored)) - (begin)))) + (begin))))) (def (learnedExamplesDocument (examples : JSONArray) (maxExamples : Int32)) --- a/templates/ssd-review-parts/UiTextHelpers.ss +++ b/templates/ssd-review-parts/UiTextHelpers.ss @@ -3,7 +3,7 @@ (def fragment '((typed-kotlin-file "com/sfb/ssdreview/UiTextHelpers.kt" - (kotlin-imports (java io File) (java io InputStream) + (kotlin-imports (java io File) (java io FileDescriptor) (java io InputStream) (java io OutputStream) (java lang Runnable) (java lang Thread) (java util UUID) (kotlin io FileTreeWalk) (android Manifest) (android app AlertDialog) @@ -255,7 +255,7 @@ (type Button) (type ButtonKind) (type CharSequence) (type ContentResolver) (type Context) (type DialogInterface) (type DocumentsContract) (type Editable) (type Editor) - (type EditText) (type File) (type FileTreeWalk) + (type EditText) (type File) (type FileDescriptor) (type FileTreeWalk) (type SharedPreferences) (type InputStream) (type OutputStream) (type LayoutParams) (type Cursor) (type Intent) (type Manifest) (type Uri) @@ -415,6 +415,23 @@ (Nullable ParcelFileDescriptor) (kotlin-member-call openFileDescriptor)) (extern + (parcelFileDescriptorFileRaw + (descriptor : ParcelFileDescriptor)) + : + FileDescriptor + (kotlin-member-get fileDescriptor)) + (extern + (fileDescriptorValidRaw (descriptor : FileDescriptor)) + : + Bool + (kotlin-member-call valid)) + (extern + (parcelFileDescriptorCloseRaw + (descriptor : ParcelFileDescriptor)) + : + Unit + (kotlin-member-call close)) + (extern (sha1HexRaw (input : InputStream)) : String @@ -2840,6 +2857,7 @@ "Approved on Android")]) (begin (mutable-list-add! (SsdSession-groups session) group) + (suppressOverlappingCandidateGroups session group) group)))) (def (ssdSessionApplyEditedGroup (session : SsdSession) (group : SsdGroup) (labelText : String) (resolvedName : String) @@ -2862,7 +2880,9 @@ group (editedGroupCountText countText (SsdGroup-count group))) (SsdGroup-status-set! group "reviewed") - (ssdSessionRecomputeGroup session group))) + (ssdSessionRecomputeGroup session group) + (suppressOverlappingCandidateGroups session group) + (begin))) (def (selectionIgnoredLoadingStatus) : String @@ -3456,6 +3476,9 @@ (ssdSessionApproveGroup loadedSession approvedGroup) + (suppressOverlappingCandidateGroups + loadedSession + approvedGroup) (truthStoreAppendGroupApproved store loadedSession) @@ -4067,12 +4090,15 @@ (truthStoreSeedBundledTruthAsync store (lambda ((imported : Int32)) - (if (positiveCount imported) + (if (mainActivitySelfTestEnabledRaw activity) (mainActivityRunOnUiThreadRaw activity (lambda () - (if (mainActivitySelfTestEnabledRaw activity) - (mainActivitySelfTestLogRaw activity "seed-merged") + (mainActivitySelfTestLogRaw activity "seed-merged"))) + (if (positiveCount imported) + (mainActivityRunOnUiThreadRaw + activity + (lambda () (begin (mainActivitySetStatusRaw activity @@ -4082,8 +4108,8 @@ (if (pdfUriLoaded (mainActivityPdfUriRaw activity)) (mainActivityRenderCurrentPage activity) - (begin)))))) - (begin))))) + (begin))))) + (begin)))))) (def (mainActivityHandleAreaSelected (activity : MainActivity) (view : SsdReviewView) (store : TruthStore) (rect : FloatArray) (detectedIds : (List String)) (confirmHaptic : Int32)) @@ -4994,7 +5020,30 @@ (afterTextChanged (s : (Nullable Editable))) : Unit - (begin))))) + (if (autoCompleteExactBoxTypeDisplay? + activity + boxType) + (autoCompleteDismissDropDownRaw boxType) + (begin)))))) + (def (autoCompleteExactBoxTypeDisplay? + (activity : MainActivity) + (boxType : AutoCompleteTextView)) : Bool + (let ((text + (string-lowercase + (string-trim (autoCompleteText boxType)))) + (resolved + (boxTypesResolveFromActivity + activity + (autoCompleteText boxType)))) + (if (nullable-null? resolved) + #f + (equal? + text + (string-lowercase + (string-trim + (boxTypesDisplayFromActivity + activity + (BoxType-id (nullable-get resolved))))))))) (def (editTextText (view : EditText)) : String @@ -5544,6 +5593,74 @@ index (mainActivityDpiValue)) (pdfRendererCloseRaw renderer)))) + (def (mainActivityCanOpenPdfUri + (activity : MainActivity) + (uri : Uri)) + : + Bool + (try + (let ([descriptor + (contentResolverOpenFileDescriptorRaw + (mainActivityContentResolverRaw activity) + uri + "r")]) + (if (nullable-null? descriptor) + #f + (let ([opened (nullable-get descriptor)]) + (try-finally + (fileDescriptorValidRaw + (parcelFileDescriptorFileRaw opened)) + (parcelFileDescriptorCloseRaw opened))))) + (catch (error : Exception) #f))) + (def (mainActivityClearLastPdfState + (activity : MainActivity)) + : + Unit + (begin + (mainActivityPdfUriSet + activity + (nullable-none Uri)) + (mainActivityPdfNameSet activity "") + (mainActivityPdfSha1Set activity "") + (mainActivityPageIndexSet activity (int32 0)) + (mainActivityPageCountSet activity (int32 0)) + (sharedPreferencesEditorApplyRaw + (sharedPreferencesEditorRemoveSavedViewportRaw + (sharedPreferencesEditorRemoveRaw + (sharedPreferencesEditorRemoveRaw + (sharedPreferencesEditorRemoveRaw + (sharedPreferencesEditorRemoveRaw + (sharedPreferencesEditorRemoveRaw + (sharedPreferencesEditRaw + (mainActivityGetPreferencesRaw + activity + (contextModePrivate))) + (mainActivityPrefLastPdfUriValue)) + (mainActivityPrefLastPdfNameValue)) + (mainActivityPrefLastPdfSha1Value)) + (mainActivityPrefLastPageIndexValue)) + (mainActivityPrefLastPageCountValue)))))) + (def (mainActivityStalePdfFields (uri : Uri)) : JSONObject + (let ([fields (emptyJsonObject)]) + (begin + (json-object-put-string! + fields "uri" (uriToStringRaw uri)) + fields))) + (def (mainActivityRejectStalePdf + (activity : MainActivity) + (uri : Uri)) + : + Bool + (begin + (mainActivityClearLastPdfState activity) + (mainActivitySetStatusRaw + activity + "Previous PDF is no longer available. Open an SSD PDF.") + (mainActivityLogClientEvent + activity + "ssd-last-pdf-stale" + (mainActivityStalePdfFields uri)) + #f)) (def (mainActivityRestoreLastPdf (activity : MainActivity)) : Bool @@ -5564,12 +5681,19 @@ (if (mainActivityServerUriRaw uri) (mainActivityRestoreServerPdfRaw activity uri page) - (begin - (mainActivityLoadPdf activity uri - (intentFlagGrantReadUriPermission) page - #t) - #t))) - (catch (error : Exception) #f))))))) + (if (mainActivityCanOpenPdfUri activity uri) + (begin + (mainActivityLoadPdf activity uri + (intentFlagGrantReadUriPermission) page + #t) + #t) + (mainActivityRejectStalePdf + activity uri)))) + (catch (error : Exception) + (mainActivityRejectStalePdf + activity + (uriParse + (nullableTextOrEmpty raw)))))))))) (def (sharedPreferencesEditorRemoveSavedViewportRaw (editor : Editor)) : @@ -7005,13 +7129,11 @@ (string-append (detectionTruthNoteFor existingTruth) (string-append - (if forceDetect - "" - (detectionOcrNote - (DetectionPreparation-ocrGuesses - preparation) - (list-size - (SsdSession-ocrWords session)))) + (detectionOcrNote + (DetectionPreparation-ocrGuesses + preparation) + (list-size + (SsdSession-ocrWords session))) (detectionLearnedNote learnedGuesses)))))) (mainActivityLogDetectResultRaw activity "page-load" session @@ -7445,13 +7567,11 @@ (string-append (int32->string ocrGuesses) " OCR guesses")) - (if (> ocrWordCount (int32 0)) - (string-append - ", OCR " - (string-append - (int32->string ocrWordCount) - " words")) - ""))) + (string-append + ", OCR " + (string-append + (int32->string ocrWordCount) + " words")))) (def (detectionLearnedNote (learnedGuesses : Int32)) : String --- a/templates/ssd-review-parts/main-ai-flow.ss +++ b/templates/ssd-review-parts/main-ai-flow.ss @@ -263,6 +263,12 @@ (extern (mainAiResultApk (result : SfbAiReportResult)) : (Nullable SfbAiApkArtifact) (kotlin-member-get apk)) + (extern (mainAiArtifactSha256 + (artifact : SfbAiApkArtifact)) : String + (kotlin-member-get sha256)) + (extern (mainAiArtifactUploaded + (artifact : SfbAiApkArtifact)) : Bool + (kotlin-member-get uploadedToTermux)) (extern (mainAiDownloadReady (result : SfbAiReportResult) (allowDryRun : Bool)) : Bool @@ -303,6 +309,9 @@ (extern (mainAiCacheDir (activity : MainActivity)) : File (kotlin-member-get cacheDir)) + (extern (mainAiFilesDir + (activity : MainActivity)) : File + (kotlin-member-get filesDir)) (extern (mainAiFile (parent : File) (leaf : String)) : File @@ -315,6 +324,12 @@ (kotlin-member-get parentFile)) (extern (mainAiFileIsFile (file : File)) : Bool (kotlin-member-get isFile)) + (extern (mainAiFileReadText (file : File)) : String + (kotlin-member-call readText)) + (extern (mainAiStringTakeLast + (text : String) + (count : Int32)) : String + (kotlin-member-call takeLast)) (extern (mainAiFilePath (file : File)) : String (kotlin-member-get absolutePath)) (extern (mainAiPackageManager @@ -449,7 +464,21 @@ (+ (mainAiPage activity) (int32 1)) (mainAiPageCount activity) (mainAiSelectedGroupText activity) - "")) + (mainAiRecentTelemetry activity))) + (def (mainAiRecentTelemetry + (activity : MainActivity)) : String + (try + (let ((file + (mainAiFile + (mainAiFilesDir activity) + "ssd-review-client-local.jsonl"))) + (if (mainAiFileIsFile file) + (let ((content (mainAiFileReadText file))) + (if (<= (string-length content) 262144) + content + (mainAiStringTakeLast content (int32 262144)))) + "")) + (catch (error : Exception) ""))) (def (mainAiSubmissionFields (submission : SfbAiReportSubmission) (screenshotBytes : Int32)) : JSONObject @@ -462,6 +491,35 @@ (json-object-put-int32! fields "screenshot_bytes" screenshotBytes) fields))) + (def (mainAiResultFields + (jobId : String) + (result : SfbAiReportResult) + (downloaded : (Nullable File))) : JSONObject + (let ((fields (json-object-empty)) + (artifact (mainAiResultApk result))) + (begin + (json-object-put-string! fields "job_id" jobId) + (json-object-put-string! + fields "state" (mainAiResultState result)) + (json-object-put-int32! + fields + "feedback_chars" + (int32 (string-length (mainAiResultFeedback result)))) + (json-object-put-bool! + fields + "uploaded_to_termux" + (if (nullable-null? artifact) + #f + (mainAiArtifactUploaded (nullable-get artifact)))) + (json-object-put-bool! + fields "apk_ready" (not (nullable-null? downloaded))) + (json-object-put-string! + fields + "sha256" + (if (nullable-null? artifact) + "" + (mainAiArtifactSha256 (nullable-get artifact)))) + fields))) (def (mainAiResultText (result : SfbAiReportResult) (apk : (Nullable File)) @@ -634,14 +692,20 @@ (catch (error : Exception) (nullable-none File))) (nullable-none File)))) - (mainAiRunOnUi - activity - (lambda () - (mainAiShowResult - activity - resolved - downloaded - ""))))))))) + (begin + (mainAiLogEvent + activity + "ssd-ai-report-result" + (mainAiResultFields + jobId resolved downloaded)) + (mainAiRunOnUi + activity + (lambda () + (mainAiShowResult + activity + resolved + downloaded + "")))))))))) (catch (error : Exception) (mainAiRunOnUi activity @@ -724,7 +788,7 @@ (mainAiBuilderTitle (mainAiBuilder activity) "Send Display to AI") - "The current display and app context will be sent with your description.") + "The current display and recent app telemetry will be sent with your description.") input) "Cancel" (lambda ((unused : DialogInterface) (which : Int32)) --- a/templates/ssd-review-parts/main-server-detect.ss +++ b/templates/ssd-review-parts/main-server-detect.ss @@ -80,6 +80,11 @@ (client : SfbServerClient) (name : String)) : (Nullable SfbServerPdf) (kotlin-member-call findServerPdfByName)) + (extern (mainServerDetectEnsureReachable + (client : SfbServerClient) + (allowLoopback : Bool)) : SfbServerInfo + (kotlin-member-call ensureReachableServerEndpoint)) + (type SfbServerInfo) (extern (mainServerDetectMatchedPdfId (pdf : SfbServerPdf)) : String (kotlin-member-get pdfId)) @@ -331,7 +336,9 @@ (lambda () (try (let ((client (mainServerDetectClient activity))) - (let ((pdfId + (begin + (mainServerDetectEnsureReachable client #t) + (let ((pdfId (mainServerDetectResolvePdfId activity client name))) (if (string-blank? pdfId) @@ -363,7 +370,7 @@ (lambda () (mainServerApplyLocalLabels activity token generation page uri - session)))))))))) + session))))))))))) (catch (error : Exception) (mainServerDetectRunOnUi activity @@ -407,7 +414,9 @@ (lambda () (try (let ((client (mainServerDetectClient activity))) - (let ((pdfId + (begin + (mainServerDetectEnsureReachable client #t) + (let ((pdfId (mainServerDetectResolvePdfId activity client name))) (if (string-blank? pdfId) @@ -436,7 +445,7 @@ (lambda () (mainServerDetectApply activity generation page loadedUri - session))))))))) + session)))))))))) (catch (error : Exception) (begin (mainServerDetectLogEvent --- a/templates/ssd-review-parts/main-server-lifecycle.ss +++ b/templates/ssd-review-parts/main-server-lifecycle.ss @@ -70,9 +70,10 @@ (client : SfbServerClient) (endpoint : String)) : Unit (kotlin-member-call setEndpoint)) - (extern (mainServerClientRefresh - (client : SfbServerClient)) : SfbServerInfo - (kotlin-member-call refreshServerEndpoint)) + (extern (mainServerClientEnsureReachable + (client : SfbServerClient) + (allowLoopback : Bool)) : SfbServerInfo + (kotlin-member-call ensureReachableServerEndpoint)) (extern (mainServerClientIsLoopback (client : SfbServerClient)) : Bool (kotlin-member-call endpointIsLoopback)) @@ -183,7 +184,7 @@ (try (let ((client (mainServerClientRaw activity))) (begin - (mainServerClientRefresh client) + (mainServerClientEnsureReachable client allowLoopback) (let ((endpoint (mainServerClientEndpoint client))) (begin (if (or allowLoopback --- a/templates/ssd-review-parts/main-server-pages.ss +++ b/templates/ssd-review-parts/main-server-pages.ss @@ -217,9 +217,10 @@ (extern (mainServerPageClientCatalog (client : SfbServerClient)) : SfbServerCatalog (kotlin-member-call fetchServerPdfs)) - (extern (mainServerPageClientRefresh - (client : SfbServerClient)) : SfbServerInfo - (kotlin-member-call refreshServerEndpoint)) + (extern (mainServerPageClientEnsureReachable + (client : SfbServerClient) + (allowLoopback : Bool)) : SfbServerInfo + (kotlin-member-call ensureReachableServerEndpoint)) (extern (mainServerPageClientEndpoint (client : SfbServerClient)) : String (kotlin-member-call endpointLabel)) @@ -560,7 +561,7 @@ (try (let ((client (mainServerPageClient activity))) (begin - (mainServerPageClientRefresh client) + (mainServerPageClientEnsureReachable client #f) (mainServerPageRunOnUi activity (lambda () --- a/templates/ssd-review-parts/server-client.ss +++ b/templates/ssd-review-parts/server-client.ss @@ -3,14 +3,27 @@ (def fragment '((typed-kotlin-file "com/sfb/ssdreview/SfbServerClient.kt" (kotlin-imports (android content Context) + (android net ConnectivityManager) + (android net LinkAddress) + (android net LinkProperties) + (android net Network) + (java lang Runnable) + (java lang Thread) + (java net InetAddress) (org json JSONObject)) (typed-library (com sfb ssdreview) (export SfbServerClient sfbLanEndpointTexts) (type Context) + (type ConnectivityManager) (type Exception) + (type InetAddress) (type IllegalStateException) (type Int32) + (type LinkAddress) + (type LinkProperties) + (type Network) (type JSONObject) + (type Runnable) (type SfbEndpoint) (type SfbEndpointProbe) (type SfbHttpTransport) @@ -22,6 +35,7 @@ (type SfbServerPdf) (type SfbServerRecognition) (type SfbServerSnapshot) + (type Thread) (extern (sfbClientIllegalStateException (message : String)) : IllegalStateException (kotlin-call IllegalStateException)) @@ -114,6 +128,51 @@ (kotlin-value SFB_MAX_SNAPSHOT_BYTES)) (extern (sfbClientNowMs) : Int (kotlin-call java lang System currentTimeMillis)) + (extern (sfbClientConnectivityServiceName) : String + (kotlin-value Context CONNECTIVITY_SERVICE)) + (extern (sfbClientSystemService + (context : Context) + (name : String)) : Any + (kotlin-member-call getSystemService)) + (extern (sfbClientAsConnectivityManager + (service : Any)) : ConnectivityManager + (kotlin-cast ConnectivityManager)) + (extern (sfbClientNetworks + (manager : ConnectivityManager)) : (Array Network) + (kotlin-member-get allNetworks)) + (extern (sfbClientNetworkCount + (networks : (Array Network))) : Int32 + (kotlin-member-get size)) + (extern (sfbClientNetworkAt + (networks : (Array Network)) + (index : Int32)) : Network + (kotlin-member-call get)) + (extern (sfbClientLinkProperties + (manager : ConnectivityManager) + (network : Network)) : (Nullable LinkProperties) + (kotlin-member-call getLinkProperties)) + (extern (sfbClientLinkAddresses + (properties : LinkProperties)) : (List LinkAddress) + (kotlin-member-get linkAddresses)) + (extern (sfbClientLinkAddress + (link : LinkAddress)) : InetAddress + (kotlin-member-get address)) + (extern (sfbClientAddressLoopback + (address : InetAddress)) : Bool + (kotlin-member-get isLoopbackAddress)) + (extern (sfbClientAddressHost + (address : InetAddress)) : (Nullable String) + (kotlin-member-get hostAddress)) + (extern (sfbClientThread + (runnable : Runnable) + (name : String)) : Thread + (kotlin-call Thread)) + (extern (sfbClientThreadStart (thread : Thread)) : Unit + (kotlin-member-call start)) + (extern (sfbClientThreadJoin + (thread : Thread) + (timeout : Int)) : Unit + (kotlin-member-call join)) (extern (sfbTransportCurrentEndpoint (transport : SfbHttpTransport)) : SfbEndpoint (kotlin-member-call currentEndpoint)) @@ -237,8 +296,120 @@ ":8797/api"))) same)) out)))) + (def (sfbClientPrivateIpv4 (host : String)) : Bool + (string-matches-regex? + host + "^(10\\.[0-9]+\\.[0-9]+\\.[0-9]+|192\\.168\\.[0-9]+\\.[0-9]+|172\\.(1[6-9]|2[0-9]|3[01])\\.[0-9]+\\.[0-9]+)$")) + (def (sfbClientPrefixListed + (prefixes : (List String)) + (prefix : String)) : Bool + (for/fold ((found #f)) + ((i (in-range (int32 0) (list-size prefixes)))) + (or found (equal? (list-ref prefixes i) prefix)))) + (def (sfbClientAddAddressPrefix + (out : (MutableList String)) + (address : InetAddress)) : Unit + (if (sfbClientAddressLoopback address) + (begin) + (let ((host (sfbClientAddressHost address))) + (if (nullable-null? host) + (begin) + (let ((text (nullable-get host))) + (if (sfbClientPrivateIpv4 text) + (let ((prefix + (string-replace-regex + text "\\.[0-9]+$" ""))) + (if (sfbClientPrefixListed out prefix) + (begin) + (mutable-list-add! out prefix))) + (begin))))))) + (def (sfbLocalIpv4Subnets + (context : Context)) : (MutableList String) + (let ((out (mutable-list-empty String)) + (manager + (sfbClientAsConnectivityManager + (sfbClientSystemService + context (sfbClientConnectivityServiceName))))) + (let ((networks (sfbClientNetworks manager))) + (begin + (for/fold ((same out)) + ((i (in-range + (int32 0) + (sfbClientNetworkCount networks)))) + (let ((properties + (sfbClientLinkProperties + manager (sfbClientNetworkAt networks i)))) + (if (nullable-null? properties) + same + (let ((addresses + (sfbClientLinkAddresses + (nullable-get properties)))) + (begin + (for/fold ((ignored (int32 0))) + ((j (in-range + (int32 0) + (list-size addresses)))) + (begin + (sfbClientAddAddressPrefix + out + (sfbClientLinkAddress + (list-ref addresses j))) + ignored)) + same))))) + out)))) + (record SfbDiscoveryState + ((mut winner : (Nullable SfbEndpoint)))) + (def (sfbProbeStrideUntilFound + (transport : SfbHttpTransport) + (endpoints : (List SfbEndpoint)) + (state : SfbDiscoveryState) + (index : Int32) + (stride : Int32)) : Unit + (if (or (>= index (list-size endpoints)) + (not (nullable-null? + (SfbDiscoveryState-winner state)))) + (begin) + (let ((endpoint (list-ref endpoints index))) + (if (sfbClientProbeReachable + (sfbProbeEndpoint transport endpoint)) + (SfbDiscoveryState-winner-set! + state (nullable-some endpoint)) + (sfbProbeStrideUntilFound + transport endpoints state (+ index stride) stride))))) + (def (sfbParallelFirstReachable + (transport : SfbHttpTransport) + (endpoints : (List SfbEndpoint))) : (Nullable SfbEndpoint) + (let ((state + (make-SfbDiscoveryState + (nullable-none SfbEndpoint))) + (threads (mutable-list-empty Thread)) + (workerCount + (if (< (list-size endpoints) (int32 48)) + (list-size endpoints) + (int32 48)))) + (begin + (for/fold ((same threads)) + ((i (in-range (int32 0) workerCount))) + (let ((thread + (sfbClientThread + (object Runnable + (override (run) : Unit + (sfbProbeStrideUntilFound + transport endpoints state i workerCount))) + (string-append + "sfb-server-probe-" (int32->string i))))) + (begin + (mutable-list-add! threads thread) + (sfbClientThreadStart thread) + same))) + (for/fold ((ignored (int32 0))) + ((i (in-range (int32 0) (list-size threads)))) + (begin + (sfbClientThreadJoin (list-ref threads i) (int 4000)) + ignored)) + (SfbDiscoveryState-winner state)))) (class SfbServerClient - ((context : Context) + ((context : Context (property val) (visibility private)) (endpointText : String) (bearerToken : String (property val) (visibility private))) (val transport : SfbHttpTransport @@ -265,7 +436,7 @@ (candidateUrls : (List String))) : SfbServerInfo (let ((candidates (sfbEndpointTextsToEndpoints candidateUrls bearerToken))) - (let ((best (selectFastest candidates))) + (let ((best (sfbParallelFirstReachable transport candidates))) (begin (sfbClientRequire (not (nullable-null? best)) @@ -303,10 +474,13 @@ (if (and (sfbClientProbeReachable probe) (or allowLoopback (not (sfbClientEndpointLoopback current)))) (fetchServerInfoUncached) - (throw - (sfbClientIllegalStateException - "Configured sfb-server endpoint is not reachable") - SfbServerInfo))))) + (let ((prefixes (sfbLocalIpv4Subnets context))) + (begin + (sfbClientRequire + (> (list-size prefixes) (int32 0)) + "No local IPv4 subnet is available") + (discoverServerEndpoint + (sfbLanEndpointTexts prefixes)))))))) (def (fetchServerInfoUncached) : SfbServerInfo (sfbClientInfoFromJson (sfbTransportGetJsonUncached