Integrate typed SSD AI report flow
ober
2e06423e40fd64a3ff3073b266f2f2eba1519053
new file mode 100644 --- /dev/null +++ b/templates/ssd-review-parts/main-ai-flow.ss @@ -0,0 +1,750 @@ +(import (jerboa prelude)) + +(def fragment + '((typed-kotlin-file "com/sfb/ssdreview/MainAiFlow.kt" + (kotlin-imports (android app AlertDialog) + (android app AlertDialog Builder) + (android content Context) + (android content DialogInterface) + (android content Intent) + (android content SharedPreferences) + (android content SharedPreferences Editor) + (android content pm PackageManager) + (android graphics Bitmap) + (android graphics Bitmap Config) + (android graphics Bitmap CompressFormat) + (android graphics Canvas) + (android net Uri) + (android os Build) + (android provider Settings) + (android text Editable) + (android widget EditText) + (android widget ScrollView) + (android widget TextView) + (androidx core content FileProvider) + (java io ByteArrayOutputStream) + (java io File) + (org json JSONObject)) + (typed-library (com sfb ssdreview) + (export mainAiShowIssueDialog mainAiOnResume) + (type AlertDialog) + (type Bitmap) + (type Builder) + (type ByteArrayOutputStream) + (type Canvas) + (type CompressFormat) + (type Config) + (type DialogInterface) + (type Editable) + (type Editor) + (type EditText) + (type Exception) + (type File) + (type IllegalStateException) + (type Int) + (type Int32) + (type Intent) + (type MainActivity) + (type JSONObject) + (type ScrollView) + (type SfbAiApkArtifact) + (type SfbAiPollState) + (type SfbAiReportClient) + (type SfbAiReportRequest) + (type SfbAiReportResult) + (type SfbAiReportSubmission) + (type SharedPreferences) + (type TextView) + (type Uri) + (extern (mainAiClient + (activity : MainActivity)) : SfbAiReportClient + (kotlin-member-get aiReportClient)) + (extern (mainAiPollingJob + (activity : MainActivity)) : String + (kotlin-member-get aiPollingJobId)) + (extern (mainAiPollingJobSet + (activity : MainActivity) + (jobId : String)) : Unit + (kotlin-member-set aiPollingJobId)) + (extern (mainAiReviewView + (activity : MainActivity)) : SsdReviewView + (kotlin-member-get reviewView)) + (type SsdReviewView) + (extern (mainAiPdfName + (activity : MainActivity)) : String + (kotlin-member-get pdfName)) + (extern (mainAiPage + (activity : MainActivity)) : Int32 + (kotlin-member-get pageIndex)) + (extern (mainAiPageCount + (activity : MainActivity)) : Int32 + (kotlin-member-get pageCount)) + (extern (mainAiSelectedGroup + (activity : MainActivity)) : (Nullable String) + (kotlin-member-get selectedGroupId)) + (extern (mainAiIntent + (activity : MainActivity)) : Intent + (kotlin-member-get intent)) + (extern (mainAiIntentBooleanExtra + (intent : Intent) + (key : String) + (fallback : Bool)) : Bool + (kotlin-member-call getBooleanExtra)) + (extern (mainAiAppInfo + (activity : MainActivity)) : JSONObject + (kotlin-call mainActivityAppLogInfo)) + (extern (mainAiDeviceInfo) : JSONObject + (kotlin-call mainActivityDeviceLogInfo)) + (extern (mainAiSetStatus + (activity : MainActivity) + (status : String)) : Unit + (kotlin-call mainActivitySetStatusRaw)) + (extern (mainAiLogEvent + (activity : MainActivity) + (event : String) + (fields : JSONObject)) : Unit + (kotlin-call mainActivityLogClientEvent)) + (extern (mainAiRunBackground + (name : String) + (action : (-> Unit))) : Unit + (kotlin-call runBackgroundNamed)) + (extern (mainAiRunOnUi + (activity : MainActivity) + (action : (-> Unit))) : Unit + (kotlin-member-call runOnUiThread)) + (extern (mainAiViewWidth + (view : SsdReviewView)) : Int32 + (kotlin-member-get width)) + (extern (mainAiViewHeight + (view : SsdReviewView)) : Int32 + (kotlin-member-get height)) + (extern (mainAiViewDraw + (view : SsdReviewView) + (canvas : Canvas)) : Unit + (kotlin-member-call draw)) + (extern (mainAiBitmapConfig) : Config + (kotlin-value Bitmap Config ARGB_8888)) + (extern (mainAiBitmapCreate + (width : Int32) + (height : Int32) + (config : Config)) : Bitmap + (kotlin-call Bitmap createBitmap)) + (extern (mainAiCanvas (bitmap : Bitmap)) : Canvas + (kotlin-call Canvas)) + (extern (mainAiJpeg) : CompressFormat + (kotlin-value Bitmap CompressFormat JPEG)) + (extern (mainAiOutput) : ByteArrayOutputStream + (kotlin-call ByteArrayOutputStream)) + (extern (mainAiBitmapCompress + (bitmap : Bitmap) + (format : CompressFormat) + (quality : Int32) + (output : ByteArrayOutputStream)) : Bool + (kotlin-member-call compress)) + (extern (mainAiOutputBytes + (output : ByteArrayOutputStream)) : Bytes + (kotlin-member-call toByteArray)) + (extern (mainAiBytesSize (bytes : Bytes)) : Int32 + (kotlin-member-get size)) + (extern (mainAiBitmapRecycle (bitmap : Bitmap)) : Unit + (kotlin-member-call recycle)) + (extern (mainAiIllegalState + (message : String)) : IllegalStateException + (kotlin-call IllegalStateException)) + (extern (mainAiEditText + (activity : MainActivity)) : EditText + (kotlin-call EditText)) + (extern (mainAiEditTextHintSet + (input : EditText) + (hint : String)) : Unit + (kotlin-member-set hint)) + (extern (mainAiEditTextEditable + (input : EditText)) : Editable + (kotlin-member-call getText)) + (extern (mainAiEditableText + (editable : Editable)) : String + (kotlin-member-call toString)) + (extern (mainAiBuilder + (activity : MainActivity)) : Builder + (kotlin-call AlertDialog Builder)) + (extern (mainAiBuilderTitle + (builder : Builder) + (title : String)) : Builder + (kotlin-member-call setTitle)) + (extern (mainAiBuilderMessage + (builder : Builder) + (message : String)) : Builder + (kotlin-member-call setMessage)) + (extern (mainAiBuilderEditView + (builder : Builder) + (view : EditText)) : Builder + (kotlin-member-call setView)) + (extern (mainAiBuilderScrollView + (builder : Builder) + (view : ScrollView)) : Builder + (kotlin-member-call setView)) + (extern (mainAiBuilderNegative + (builder : Builder) + (label : String) + (listener : (-> DialogInterface Int32 Unit))) : Builder + (kotlin-member-call setNegativeButton)) + (extern (mainAiBuilderPositive + (builder : Builder) + (label : String) + (listener : (-> DialogInterface Int32 Unit))) : Builder + (kotlin-member-call setPositiveButton)) + (extern (mainAiBuilderShow (builder : Builder)) : AlertDialog + (kotlin-member-call show)) + (extern (mainAiTextView + (activity : MainActivity)) : TextView + (kotlin-call TextView)) + (extern (mainAiTextViewSet + (view : TextView) + (text : String)) : Unit + (kotlin-member-call setText)) + (extern (mainAiTextViewSelectableSet + (view : TextView) + (enabled : Bool)) : Unit + (kotlin-member-call setTextIsSelectable)) + (extern (mainAiScrollView + (activity : MainActivity)) : ScrollView + (kotlin-call ScrollView)) + (extern (mainAiScrollAddText + (scroll : ScrollView) + (view : TextView)) : Unit + (kotlin-member-call addView)) + (extern (mainAiSubmit + (client : SfbAiReportClient) + (request : SfbAiReportRequest)) : SfbAiReportSubmission + (kotlin-member-call submit)) + (extern (mainAiSubmissionJobId + (submission : SfbAiReportSubmission)) : String + (kotlin-member-get jobId)) + (extern (mainAiSubmissionState + (submission : SfbAiReportSubmission)) : String + (kotlin-member-get state)) + (extern (mainAiPoll + (client : SfbAiReportClient) + (jobId : String) + (maxPolls : Int32) + (maxFailures : Int32) + (intervalMs : Int)) : SfbAiPollState + (kotlin-member-call pollUntilTerminal)) + (extern (mainAiPollResult + (state : SfbAiPollState)) : (Nullable SfbAiReportResult) + (kotlin-member-get result)) + (extern (mainAiPollPaused + (state : SfbAiPollState)) : Bool + (kotlin-member-get paused)) + (extern (mainAiResultState + (result : SfbAiReportResult)) : String + (kotlin-member-get state)) + (extern (mainAiResultFeedback + (result : SfbAiReportResult)) : String + (kotlin-member-get feedback)) + (extern (mainAiResultTranscript + (result : SfbAiReportResult)) : String + (kotlin-member-get transcript)) + (extern (mainAiResultApk + (result : SfbAiReportResult)) : (Nullable SfbAiApkArtifact) + (kotlin-member-get apk)) + (extern (mainAiDownloadReady + (result : SfbAiReportResult) + (allowDryRun : Bool)) : Bool + (kotlin-call sfbAiResultDownloadReady)) + (extern (mainAiDownloadApk + (client : SfbAiReportClient) + (artifact : SfbAiApkArtifact)) : File + (kotlin-member-call downloadApk)) + (extern (mainAiModePrivate) : Int32 + (kotlin-value Context MODE_PRIVATE)) + (extern (mainAiPreferences + (activity : MainActivity) + (name : String) + (mode : Int32)) : SharedPreferences + (kotlin-member-call getSharedPreferences)) + (extern (mainAiPreferencesGetString + (preferences : SharedPreferences) + (key : String) + (fallback : (Nullable String))) : (Nullable String) + (kotlin-member-call getString)) + (extern (mainAiPreferencesEdit + (preferences : SharedPreferences)) : Editor + (kotlin-member-call edit)) + (extern (mainAiEditorPutString + (editor : Editor) + (key : String) + (value : String)) : Editor + (kotlin-member-call putString)) + (extern (mainAiEditorRemove + (editor : Editor) + (key : String)) : Editor + (kotlin-member-call remove)) + (extern (mainAiEditorApply (editor : Editor)) : Unit + (kotlin-member-call apply)) + (extern (mainAiExceptionMessage + (error : Exception)) : (Nullable String) + (kotlin-member-get message)) + (extern (mainAiCacheDir + (activity : MainActivity)) : File + (kotlin-member-get cacheDir)) + (extern (mainAiFile + (parent : File) + (leaf : String)) : File + (kotlin-call File)) + (extern (mainAiFileFromPath (path : String)) : File + (kotlin-call File)) + (extern (mainAiCanonicalFile (file : File)) : File + (kotlin-member-get canonicalFile)) + (extern (mainAiFileParent (file : File)) : (Nullable File) + (kotlin-member-get parentFile)) + (extern (mainAiFileIsFile (file : File)) : Bool + (kotlin-member-get isFile)) + (extern (mainAiFilePath (file : File)) : String + (kotlin-member-get absolutePath)) + (extern (mainAiPackageManager + (activity : MainActivity)) : PackageManager + (kotlin-member-get packageManager)) + (type PackageManager) + (extern (mainAiCanInstall + (manager : PackageManager)) : Bool + (kotlin-member-call canRequestPackageInstalls)) + (extern (mainAiSdk) : Int32 + (kotlin-value Build VERSION SDK_INT)) + (extern (mainAiApiO) : Int32 + (kotlin-value Build VERSION_CODES O)) + (extern (mainAiPackageName + (activity : MainActivity)) : String + (kotlin-member-get packageName)) + (extern (mainAiIntentWithUri + (action : String) + (uri : Uri)) : Intent + (kotlin-call Intent)) + (extern (mainAiIntentForAction + (action : String)) : Intent + (kotlin-call Intent)) + (extern (mainAiUnknownSourcesAction) : String + (kotlin-value Settings ACTION_MANAGE_UNKNOWN_APP_SOURCES)) + (extern (mainAiViewAction) : String + (kotlin-value Intent ACTION_VIEW)) + (extern (mainAiUriParse (text : String)) : Uri + (kotlin-call Uri parse)) + (extern (mainAiFileProviderUri + (activity : MainActivity) + (authority : String) + (file : File)) : Uri + (kotlin-call FileProvider getUriForFile)) + (extern (mainAiIntentDataType + (intent : Intent) + (uri : Uri) + (mime : String)) : Intent + (kotlin-member-call setDataAndType)) + (extern (mainAiIntentAddFlags + (intent : Intent) + (flags : Int32)) : Intent + (kotlin-member-call addFlags)) + (extern (mainAiReadUriFlag) : Int32 + (kotlin-value Intent FLAG_GRANT_READ_URI_PERMISSION)) + (extern (mainAiStartActivity + (activity : MainActivity) + (intent : Intent)) : Unit + (kotlin-member-call startActivity)) + (def (mainAiSelfTest (activity : MainActivity)) : Bool + (mainAiIntentBooleanExtra + (mainAiIntent activity) + "sfb.ssdreview.selftest" + #f)) + (def (mainAiErrorText (error : Exception)) : String + (let ((message (mainAiExceptionMessage error))) + (if (nullable-null? message) "unknown" (nullable-get message)))) + (def (mainAiSelectedGroupText + (activity : MainActivity)) : String + (let ((selected (mainAiSelectedGroup activity))) + (if (nullable-null? selected) "" (nullable-get selected)))) + (def (mainAiStatePreferences + (activity : MainActivity)) : SharedPreferences + (mainAiPreferences activity "ssd_ai_state" (mainAiModePrivate))) + (def (mainAiStoreString + (activity : MainActivity) + (key : String) + (value : String)) : Unit + (mainAiEditorApply + (mainAiEditorPutString + (mainAiPreferencesEdit (mainAiStatePreferences activity)) + key + value))) + (def (mainAiRemove + (activity : MainActivity) + (key : String)) : Unit + (mainAiEditorApply + (mainAiEditorRemove + (mainAiPreferencesEdit (mainAiStatePreferences activity)) + key))) + (def (mainAiStoredString + (activity : MainActivity) + (key : String)) : String + (let ((value + (mainAiPreferencesGetString + (mainAiStatePreferences activity) + key + (nullable-none String)))) + (if (nullable-null? value) "" (string-trim (nullable-get value))))) + (def (mainAiCaptureDisplay + (activity : MainActivity)) : Bytes + (let ((view (mainAiReviewView activity))) + (let ((width (mainAiViewWidth view)) + (height (mainAiViewHeight view))) + (if (or (<= width (int32 0)) (<= height (int32 0))) + (throw (mainAiIllegalState "Display is not ready") Bytes) + (let ((bitmap + (mainAiBitmapCreate + width height (mainAiBitmapConfig)))) + (try-finally + (begin + (mainAiViewDraw view (mainAiCanvas bitmap)) + (let ((output (mainAiOutput))) + (begin + (if (mainAiBitmapCompress + bitmap (mainAiJpeg) (int32 85) output) + (begin) + (throw + (mainAiIllegalState "JPEG capture failed") + Unit)) + (let ((bytes (mainAiOutputBytes output))) + (if (<= (mainAiBytesSize bytes) + (* (* (int32 4) (int32 1024)) + (int32 1024))) + bytes + (throw + (mainAiIllegalState + "Captured display exceeds 4 MiB") + Bytes)))))) + (mainAiBitmapRecycle bitmap))))))) + (def (mainAiIssueRequest + (activity : MainActivity) + (description : String) + (screenshot : Bytes)) : SfbAiReportRequest + (new SfbAiReportRequest + description + screenshot + (mainAiSelfTest activity) + (mainAiAppInfo activity) + (mainAiDeviceInfo) + (mainAiPdfName activity) + (+ (mainAiPage activity) (int32 1)) + (mainAiPageCount activity) + (mainAiSelectedGroupText activity) + "")) + (def (mainAiSubmissionFields + (submission : SfbAiReportSubmission) + (screenshotBytes : Int32)) : JSONObject + (let ((fields (json-object-empty))) + (begin + (json-object-put-string! + fields "job_id" (mainAiSubmissionJobId submission)) + (json-object-put-string! + fields "state" (mainAiSubmissionState submission)) + (json-object-put-int32! + fields "screenshot_bytes" screenshotBytes) + fields))) + (def (mainAiResultText + (result : SfbAiReportResult) + (apk : (Nullable File)) + (downloadError : String)) : String + (let ((feedback (mainAiResultFeedback result)) + (transcript (mainAiResultTranscript result))) + (let ((details + (if (or (string-blank? transcript) + (equal? transcript feedback)) + feedback + (string-append + feedback + (string-append + "\n\nFull agent transcript:\n" + transcript))))) + (if (nullable-null? apk) + (if (string-blank? downloadError) + details + (string-append + details + (string-append + "\n\nAPK download failed: " + downloadError))) + (string-append + details + "\n\nVerified APK update downloaded to this phone."))))) + (def (mainAiInstallApk + (activity : MainActivity) + (apk : File)) : Unit + (let ((updates + (mainAiCanonicalFile + (mainAiFile (mainAiCacheDir activity) "updates"))) + (candidate (mainAiCanonicalFile apk))) + (if (and (mainAiFileIsFile candidate) + (and (not (nullable-null? (mainAiFileParent candidate))) + (equal? (nullable-get (mainAiFileParent candidate)) + updates))) + (if (and (>= (mainAiSdk) (mainAiApiO)) + (not (mainAiCanInstall + (mainAiPackageManager activity)))) + (begin + (mainAiStoreString + activity "pending_apk" (mainAiFilePath candidate)) + (mainAiStartActivity + activity + (mainAiIntentWithUri + (mainAiUnknownSourcesAction) + (mainAiUriParse + (string-append + "package:" + (mainAiPackageName activity))))) + (mainAiSetStatus + activity + "Allow installs from this app, then the update will open")) + (let ((uri + (mainAiFileProviderUri + activity + (string-append + (mainAiPackageName activity) + ".files") + candidate))) + (begin + (mainAiRemove activity "pending_apk") + (mainAiStartActivity + activity + (mainAiIntentAddFlags + (mainAiIntentDataType + (mainAiIntentForAction (mainAiViewAction)) + uri + "application/vnd.android.package-archive") + (mainAiReadUriFlag))) + (mainAiSetStatus activity "Opening verified APK update")))) + (throw + (mainAiIllegalState "Invalid APK update file") + Unit)))) + (def (mainAiShowResult + (activity : MainActivity) + (result : SfbAiReportResult) + (apk : (Nullable File)) + (downloadError : String)) : Unit + (let ((text (mainAiTextView activity)) + (scroll (mainAiScrollView activity))) + (begin + (mainAiTextViewSet + text + (mainAiResultText result apk downloadError)) + (mainAiTextViewSelectableSet text #t) + (mainAiScrollAddText scroll text) + (if (nullable-null? apk) + (begin + (mainAiBuilderShow + (mainAiBuilderNegative + (mainAiBuilderScrollView + (mainAiBuilderTitle + (mainAiBuilder activity) + "AI Repair Result") + scroll) + "Close" + (lambda ((unused : DialogInterface) (which : Int32)) + (begin)))) + (begin)) + (begin + (mainAiBuilderShow + (mainAiBuilderPositive + (mainAiBuilderNegative + (mainAiBuilderScrollView + (mainAiBuilderTitle + (mainAiBuilder activity) + "AI Repair Complete - Update Ready") + scroll) + "Close" + (lambda ((unused : DialogInterface) (which : Int32)) + (begin))) + "Install Update" + (lambda ((unused : DialogInterface) (which : Int32)) + (mainAiInstallApk activity (nullable-get apk))))) + (begin))) + (mainAiSetStatus + activity + (if (nullable-null? apk) + (string-append + "AI repair " + (string-append + (mainAiResultState result) + "; feedback received")) + "AI repair complete; APK update ready"))))) + (def (mainAiPollJob + (activity : MainActivity) + (jobId : String)) : Unit + (if (or (string-blank? jobId) + (equal? (mainAiPollingJob activity) jobId)) + (begin) + (begin + (mainAiPollingJobSet activity jobId) + (mainAiRunBackground + "sfb-ai-report-poll" + (lambda () + (try-finally + (try + (let ((state + (mainAiPoll + (mainAiClient activity) + jobId + (int32 1200) + (int32 1200) + (if (mainAiSelfTest activity) + (int 100) + (int 3000))))) + (let ((result (mainAiPollResult state))) + (if (nullable-null? result) + (mainAiRunOnUi + activity + (lambda () + (mainAiSetStatus + activity + "AI feedback polling paused; it will resume next time the app opens"))) + (let ((resolved (nullable-get result))) + (begin + (mainAiRemove activity "pending_ai_job") + (let ((downloaded + (if (mainAiDownloadReady + resolved + (mainAiSelfTest activity)) + (try + (nullable-some + (mainAiDownloadApk + (mainAiClient activity) + (nullable-get + (mainAiResultApk resolved)))) + (catch (error : Exception) + (nullable-none File))) + (nullable-none File)))) + (mainAiRunOnUi + activity + (lambda () + (mainAiShowResult + activity + resolved + downloaded + ""))))))))) + (catch (error : Exception) + (mainAiRunOnUi + activity + (lambda () + (mainAiSetStatus + activity + (string-append + "AI feedback polling failed: " + (mainAiErrorText error))))))) + (mainAiPollingJobSet activity ""))))))) + (def (mainAiSubmitIssue + (activity : MainActivity) + (description : String) + (screenshot : Bytes)) : Unit + (begin + (mainAiSetStatus activity "Sending display to AI...") + (mainAiRunBackground + "sfb-ai-report-upload" + (lambda () + (try + (let ((submission + (mainAiSubmit + (mainAiClient activity) + (mainAiIssueRequest + activity description screenshot)))) + (let ((jobId (mainAiSubmissionJobId submission))) + (begin + (mainAiStoreString activity "pending_ai_job" jobId) + (mainAiLogEvent + activity + "ssd-ai-report-submitted" + (mainAiSubmissionFields + submission + (mainAiBytesSize screenshot))) + (mainAiRunOnUi + activity + (lambda () + (mainAiSetStatus + activity + (string-append + "AI report " + (string-append + (mainAiSubmissionState submission) + (string-append + ": " + (string-append + jobId + "; waiting for full feedback"))))))) + (mainAiPollJob activity jobId)))) + (catch (error : Exception) + (mainAiRunOnUi + activity + (lambda () + (mainAiSetStatus + activity + (string-append + "AI report failed: " + (mainAiErrorText error))))))))))) + (def (mainAiShowIssueDialog + (activity : MainActivity)) : Unit + (try + (let ((screenshot (mainAiCaptureDisplay activity)) + (input (mainAiEditText activity))) + (begin + (mainAiEditTextHintSet + input + "Describe what is wrong and what you expected") + (mainAiBuilderShow + (mainAiBuilderPositive + (mainAiBuilderNegative + (mainAiBuilderEditView + (mainAiBuilderMessage + (mainAiBuilderTitle + (mainAiBuilder activity) + "Send Display to AI") + "The current display and app context will be sent with your description.") + input) + "Cancel" + (lambda ((unused : DialogInterface) (which : Int32)) + (begin))) + "Send" + (lambda ((unused : DialogInterface) (which : Int32)) + (let ((description + (string-trim + (mainAiEditableText + (mainAiEditTextEditable input))))) + (if (string-blank? description) + (mainAiSetStatus + activity + "Describe the issue before sending") + (mainAiSubmitIssue + activity description screenshot)))))) + (begin))) + (catch (error : Exception) + (mainAiSetStatus + activity + (string-append + "Display capture failed: " + (mainAiErrorText error)))))) + (def (mainAiResumePendingInstall + (activity : MainActivity)) : Unit + (let ((path (mainAiStoredString activity "pending_apk"))) + (if (string-blank? path) + (begin) + (if (or (< (mainAiSdk) (mainAiApiO)) + (mainAiCanInstall (mainAiPackageManager activity))) + (try + (mainAiInstallApk activity (mainAiFileFromPath path)) + (catch (error : Exception) + (mainAiSetStatus + activity + (string-append + "Unable to open APK update: " + (mainAiErrorText error))))) + (begin))))) + (def (mainAiOnResume (activity : MainActivity)) : Unit + (begin + (mainAiPollJob + activity + (mainAiStoredString activity "pending_ai_job")) + (mainAiResumePendingInstall activity))))))) --- a/templates/ssd-review-parts/server-ai.ss +++ b/templates/ssd-review-parts/server-ai.ss @@ -8,4 +8,5 @@ (include-fragment "ai-report.ss") (include-fragment "main-server-lifecycle.ss") (include-fragment "main-server-pages.ss") - (include-fragment "main-server-detect.ss"))) + (include-fragment "main-server-detect.ss") + (include-fragment "main-ai-flow.ss")))