A complete static reversal of MICO 4.2026.04.23.1145, the eyecon caller-ID and spam-blocker app. The e-auth credential scheme reversed end to end so any request it makes can be forged by hand, 50 endpoints across 36 hosts, the complete clientId issuance chain, 21 database tables, 96 activities, and five ARM native libraries with three reversed including a recovered XOR string decryptor and a cipher seeded from the wall clock. Seven parts covering the attack surface, the backend, the data layer, every feature flow, and every first-party package in the APK.

MICO / com.eyecon.global: what a caller-ID app actually sends
576 mins
115139 words
Loading views

This is a full reversal of MICO, the Android caller-ID and spam-blocking app published by eyecon as com.eyecon.global, version 4.2026.04.23.1145. Everything below is derived from the shipped APK at eyecon-app.latest.apk, 65,604,949 bytes, 54,878 classes across six DEX files.

The analysis is static. None of it was observed running. There was no device capture and no traffic capture, and no server-side work of any kind. Nothing here claims what an endpoint actually returns. Where a claim is inferred from bytecode rather than read directly out of it, it says which one it is.

A clarification belongs here, at the top, because “caller ID app” invites a specific reading and gets it wrong. MICO is a commercial product doing a commercial job: it identifies who is calling and it blocks the calls you do not want. It ships advertising SDKs, which is ordinary for the category. There is no command-and-control channel, no dropper, no privilege escalation, no persistence beyond standard Android components, and no dynamic code loading beyond a bundled Meta Audience Network DEX. It does not record calls.

What makes the app worth reversing is narrower and more specific than the marketing suggests. The request authentication scheme is recoverable end to end, and once it is, any request the app can make can be reproduced by hand with curl. The clientId that anchors the whole identity chain is issued by one unauthenticated endpoint. The Facebook application secret sits in plaintext resources. Two of the bundled ByteDance libraries implement their own cryptography, and one of them seeds a permutation from the wall clock.

Three of the five native libraries are reversed here. Two are not, and I would rather say so plainly than let the count imply more than it does. libbuffer_pgl.so and libfile_lock_pgl.so are ByteDance Pangle libraries that appear in the APK, are never mentioned in the source documentation, and were never opened. Part VI says what that leaves unanswered.

How to read thish2

The document runs in seven parts. Each part opens with a short framing section written to give you the shape of what follows, and then reproduces the underlying analysis in full and unedited.

partcoverssource
Iscope, target identity, obfuscation map, method, tooling stateworkspace README
IImanifest, components, permissions, network configMANIFEST-AND-SECURITY.md
IIIe-auth, endpoints, HTTP stack, device fingerprint, secretsBACKEND-API.md
IVdatabases, tables, preferences, the clientId chainDATA-LAYER.md
V29 feature modules and nine reconstructed flowsFEATURE-AND-FLOWS.md
VInative libraries, ARM binaries, Pangle provenancethree native documents
VIIevery first-party package, cluster by clusterseven cluster documents

One convention. Class names in code listings are JADX’s obfuscated output with original names restored where the JADX INFO: renamed from comment survives, so you can grep either form.

Part I - Scope, target, methodh2

Before any findings, the ground rules: what was analysed, what tooling did the work, how much of the tree survived decompilation, and where the coverage ran out. This part also carries the deliverables list and the obfuscation-to-identity map, which is the lookup you will want most when reading Part VII.

Two numbers frame everything after this. Only 5,771 of the 38,575 decompiled Java files are first-party, which is 15.3 percent. The remaining 85 percent is advertising and analytics: Google AdMob alone accounts for 6,439 classes, Facebook Audience Network 3,152, Fyber FairBid plus Inneractive 1,148. The analysis budget went to the 15 percent, and the reason is that the other 85 percent is a solved problem that other people have already published.

The decompiler itself lost 392 classes outright, 162 of them hard failures at the file level. Every one of those is AndroidX or Compose internals, so the loss is real but it is loss in territory nobody needed. 31,911 files decompiled with no warnings at all.

Four packages resisted classification entirely: p293fh, gj, g8, and yi. They are catalogued in the C6 appendices with what is known about them, which is close to nothing.

MICO (com.eyecon.global) — Full Decompilation & Reconstructionh3

Generated from C:\Users\btc_e\Downloads\eyecon-app.latest.apk Method: JADX 1.5.6 CLI (--deobf --show-bad-code --comments-level debug) + targeted native inspection.


Deliverablesh4

PathSizeContents
src/sources/38,575 .java filesComplete decompiled source tree (JADX)
src/resources/6,800 filesDecoded resources.arsc, layouts, manifests, assets
docs/BACKEND-API.md83 KB, 1049 linesEvery outbound HTTP call, headers, params, auth algorithms
docs/DATA-LAYER.md65 KB, 1110 lines8 databases, 21 tables, ~250 preference keys, storage layout
docs/MANIFEST-AND-SECURITY.md72 KB, 741 lines96 activities, 27 services, 27 receivers, 9 providers, 78 permissions, security surface
docs/FEATURE-AND-FLOWS.md164 KB, 2008 lines29 modules catalogued, 9 end-to-end flows reconstructed, obfuscation→identity map
docs/cluster-C1-core-platform.md172 KB, 2354 lines16 pkgs / 350 files — foundation: constants, HTTP core, prefs, remote config
docs/cluster-C2-registration-billing.md155 KB, 2574 lines50 pkgs / 962 files — registration, CLI auth, Play Billing, JS bridges
docs/cluster-C3-dialer-call-sms.md118 KB, 1121 lines42 pkgs / 843 files — dialer, spam, AI SMS assistant, TTM
docs/cluster-C4-main-contacts.md120 KB, 1464 lines27 pkgs / 489 files — contact sync, photos, main screen, reminders
docs/cluster-C5-media-social-extra.md204 KB, 2186 lines14 pkgs / 229 files — Toki voice, backup, photos, social, ads
docs/cluster-C6a-unclassified.md59 KB, 706 lines193 pkgs / 2426 files — 177 third-party, 9 first-party, 16 deep entries
docs/cluster-C6b-unclassified.md60 KB, 637 lines193 pkgs / 472 files — tail-end stubs and dead code

Total footprint: 44,575 files / 314 MB.


Target identityh4

package com.eyecon.global
versionCode 749
versionName 4.2026.04.23.1145
minSdk 23 targetSdk 35 compileSdk 36
size 65,604,949 bytes
DEX 6 files (classes.dex … classes6.dex) + assets/audience_network.dex (5,017,908 B)
classes 54,878 across 8,291 packages
ABIs arm64-v8a, armeabi-v7a, x86, x86_64
processes main, :notification_process, :restart_app

Corrections to BACKEND-FINDINGS.mdh4

The first pass left open questions that the deeper pass resolved:

  1. CID origin — RESOLVED. clientId (the e-auth credential) is the join.jsp response body. It is server-issued, not a device identifier. See FEATURE-AND-FLOWS.md §A.
  2. The AI SMS assistant does NOT use voice2text. It posts to numberlookup.instabridge.com over cleartext HTTP. voice2text.eyecon-app.com is the separate Talk-To-Me voicemail-to-text service. The first pass conflated them.
  3. e-auth-k is rotatable. PgdtSBeR0MumR7fO is the compiled-in default, but Remote Config key circus_key can reflectively overwrite the static final field at runtime (p032b5.AbstractC3464e.m3668s()), and that Remote Config is fetched over cleartext HTTP.
  4. EyeconRoomDB is dead code. The entity class is an image utility, the DAO is empty, it is never instantiated. MICO uses raw SQL exclusively.
  5. App SharedPreferences is not Android SharedPreferences — it is a custom implementation backed by a shared_preference_v2 table inside contactdb.
  6. Native libraries are not app protection. libpglarmor.so / libtobEmbedPagEncrypt.so / libnms.so / libbuffer_pgl.so are ByteDance Pangle ad-SDK components. Nothing in native code strengthens the API auth. (The first pass suspected a packer; that was wrong.)

Obfuscation → identity maph4

Recovered from JADX renamed from comments during the flow reconstruction:

ObfuscatedIdentity
a7constants + SQLiteOpenHelper
w6HTTP core (NetworkHelper), Server facade, API work queue
f7custom SharedPreferences, emulator detector, ServerMySettings
q7registration callbacks, validation, error enums
x7, w9spam model + spam DB
x5, woAI SMS assistant (Retrofit + Moshi)
e7AsyncCallbackBase
jdjoin.jsp client
r5cli_auth / checkcode
x6durable request queue
yl, wl, as.u2Toki UDP transport / session / TalkiServer
f5Google Drive backup
g5, j5Play Billing + PaidPremium/FreePremium
k7dead SyncAdapter

Two classes look like third-party code but are app code: com.facebook.appevents.AbstractC9039h and androidx.fragment.app.RunnableC2598c.


Top findings by severityh4

Criticalh5
  • Facebook app secret shipped in the client — verified in res/values/strings.xml: facebook_app_id = 1716660955257637, facebook_client_token = afa3f38c55f24c73b177afc962969b15, and fct = the pair as app_id|app_secret. Facebook documents the app secret as server-only. Severity depends on the app’s Graph permissions and whether any Eyecon endpoint trusts app-level auth — both need the Meta app console.
  • Cleartext unauthenticated remote control plane — ~400 A/B flags plus store_url, search_url, links_script, big_url_script fetched over plain HTTP from cloudfront-cdn-all.eyecon-app.com. Enables JS injection, cards-host repointing (leaking cli+cid), and audio redirection. (This replaces the retracted “rc_key rotation” claim.)
Highh5
  • AI SMS assistant is unauthenticated and cleartext — x5.p.m38847d POSTs up to 50 messages from inbox and sent, plus contact name, number and display name, to numberlookup.instabridge.com/respond-sms over plain HTTP. A MITM can therefore make MICO send attacker-chosen SMS from the user’s phone.
  • Premium entitlement is 100% client-side — no Google Play purchase verification exists; the only confirmation is a client-authored mysettings POST. Purchase tokens and a page-chosen offerToken are handed to remote web content.
  • StoreActivity leaks the bearer token — remote-config store_url with no scheme, host or pinning validation, loaded into a JS WebView exposing clientId (the e-auth bearer), GAID, MSISDN and an unfiltered SharedPreferences reader. WebviewPaywallActivity implements exactly the missing validation, so the fix is known internally and applied in one place only.
  • MyPhoneReceiver is exported with no permission and honours CALL_SCREENING_NEW_CALL and INTENT_ACTION_START_CALL_WHATSAPP, taking state and incoming_number verbatim from the broadcast — any installed app can drive the caller-ID screen for an arbitrary number.
  • Unencrypted Opus-over-UDP voice to ptt.eyecon-app.com; push-to-talk via ACTION_DOWN/ACTION_UP, except the notification-answer path starts the mic with no release.
  • Unencrypted contactdb to Google Drive (full SMS bodies, notification text, voicemail transcripts), restored by a renameTo swap.
  • PhotosTrackerWorker silently harvests Facebook avatars of every Facebook-linked contact every 3 days, via a hidden people-search in an invisible JS WebView with a spoofed Pixel 2 / Chrome 79 User-Agent.
  • EyeconAnalyticsWorker can ZIP and upload the entire call log; disabled by default but remotely flippable, with a from_date rewind for re-upload.
  • Contact-photo “encryption”: Blowfish/ECB keyed "Ewu" + nextInt(999) + "sTQHGK" → 999 possible keys, key stored plaintext in prefs as cipher_key, with ObjectInputStream.readObject() reached twice over decryptable file bytes.
Mediumh5
  • PII in URL query strings: GET cantalk.jsp?recvcli=…&message=<SMS body>; the first-party spam report sends name + caller-id as GET params. IMEI/MAC/android_id/GAID fingerprint on join.jsp, cli_auth/gettransport, addcontacts.jsp.
  • Singular (e1) harvests GAID via reflection and discards isLimitAdTrackingEnabled() — Limit-Ad-Tracking is read then thrown away. It also enumerates all location providers and reverse-geocodes to a country code; coarse location alone suffices.
  • Amplitude Session Replay (p000) records caller-ID card screens via rrweb.
  • eo.b.m31717e calls clearApplicationUserData() on DB corruption and System.exit(0) on any prefs read error, destroying clientId and forcing re-registration.
  • Moloco banner WebView: JS enabled + evaluateJavascript("beginVideo(" + <VAST data> …), with creative URLs passed to an unschematised external ACTION_VIEW intent.
  • PhoneAccount is a dead stub — getAuthToken throws UnsupportedOperationException.
  • 401 + E-Mark triggers a destructive client-side wipe of auth state, reachable via the same cleartext path. eyecon://browser?url= intent redirection; exported PhoneNumberValidationActivity.onNewIntent extras injection.
Explicitly NOT findings / NOT presenth5
  • No call recording exists. No MediaRecorder, no VOICE_CALL/DOWNLINK/UPLINK, no MediaProjection. The only AudioRecord app-wide is an AMR voice-note. The Documents/Eyecon/RecordedCalls directory is index/scan/migrate/promote/ACTION_SEND only.
  • SMS broadcast receivers are permission-guarded and never call abortBroadcast.
  • All other exported services/receivers are permission-guarded.
  • No mutable PendingIntent; no certificate-pinning bypass; backup is correctly disabled.
  • The pgl native libraries are ByteDance Pangle ad SDK, not a packer or app protector.
  • e-auth-c (m38421e) is a trivial checksum, not a challenge-response.
  • No CRITICAL permission-granting defect.
Explicitly NOT findingsh5

SMS broadcast receivers are permission-guarded and never call abortBroadcast; all other exported services/receivers are permission-guarded; no mutable PendingIntent; no certificate pinning bypass; backup is correctly disabled; there is no CRITICAL permission-granting defect.


Provider / tooling stateh4

ToolState
JADX MCPLive. Used for interactive reads and naming of the CLI export.
JADX CLI 1.5.6Used for the bulk export. Found at C:\RE\RE\02-Decompilers\jadx-gui-1.5.6-win\lib\jadx-gui-1.5.6-all.jar
Ghidra 12.1.2Installed and running at C:\RE\RE\01-Disassemblers\ghidra_12.1.2_PUBLIC_20260605\ghidra_12.1.2_PUBLIC. GhidraMCP extension extracted to ~\.ghidra\.ghidra_12.1.2_PUBLIC\Extensions\GhidraMCP\. Requires a Ghidra restart + a binary to be loaded before the bridge on <8080> answers.
Ghidra bridgeConfigured in ~\.config\opencode\opencode.jsonc → ghidramcp\bridge_mcp_ghidra.py, target http://127.0.0.1:8080/. Currently refused.
IDA Pro 9.4Installed at C:\Program Files\IDA Professional 9.4. ida-pro-mcp 2.0.0 Python package present. Plugin installed to %APPDATA%\Hex-Rays\IDA Pro\plugins\ida_mcp.py + ida_mcp\. Requires an IDA restart and an open binary.
IDA MCP registrationWritten to ~\.config\opencode\opencode.json as type: remote, http://127.0.0.1:13337/mcp.
REA MCPRegistered in ~\.config\opencode\opencode.jsonc; tools present but rea CLI Android inspection is blocked (REA_JADX_MCP_JAR unset).

Note: opencode reads both opencode.json and opencode.jsonc in ~\.config\opencode\. ida-pro-mcp wrote to the .json; REA wrote to the .jsonc. Both are active.


Residual unknownsh4

Static analysis only. Nothing here observed execution.

  1. Response schemas for every endpoint — only request shapes are recoverable statically.
  2. Whether e-auth-k is validated server-side at all.
  3. E-Mark / CID 401 semantics — needs runtime or server-side evidence.
  4. 392 classes (1.2%) failed decompilation; contents unknown.
  5. The phone-call audio capture call site was not located; only Toki’s AudioRecord exists in app code.
  6. PhoneAccount.SyncService.onPerformSync is empty — the contacts account is registered and unused.
  7. The GhidraMCP extension declares ghidraVersion=11.3.2 against Ghidra 12.1.2; it may fail to load.

Part II - Attack surfaceh2

What the app declares to the platform. This is the cheapest part of any Android reversal to read and often the most revealing, because the manifest states intentions that the code then softens.

Three things dominate. The app requests android:usesCleartextTraffic="true" globally, and pairs that with a network security config that permits cleartext for specific hosts. It declares 96 activities, of which a substantial number are reachable only through deep links. And it ships a <queries> block naming roughly 30 messaging apps, which is a platform-level statement that the user wants to know which messengers are installed.

The cleartext configuration matters more than it first appears, because it interacts with the 401 handling in Part III. A client-side auth wipe is triggered by a response condition that an on-path attacker can manufacture over an unencrypted channel. That chain is spelled out there.

MICO / com.eyecon.global - Manifest & Security Surface Referenceh3

FieldValue
Packagecom.eyecon.global
versionCode / versionName749 / 4.2026.04.23.1145
minSdk / targetSdk / compileSdk23 / 35 / 36
installLocationauto (SD-card install permitted)
Application classcom.eyecon.global.Others.MyApplication
Manifestsrc/resources/AndroidManifest.xml (1669 lines)
Component totals96 activities, 4 activity-aliases, 27 services, 27 receivers, 9 providers
Permission entries78 (77 uses-permission + 1 uses-permission-sdk-23), plus 3 self-declared <permission>
Processes3 total - com.eyecon.global (main) + 2 remote (:notification_process, :restart_app)
uses-feature3 (all required="false")
<queries>54 <package> + 26 <intent> = 80 visibility declarations
meta-data58 total (22 on <application>, 36 on components)
Backupandroid:allowBackup="false" and android:fullBackupContent="false" - no dataExtractionRules
NetworkusesCleartextTraffic="true" + @xml/network_security_config

1. Manifest componentsh4

1.1 Processesh5
ProcessDeclared onPurpose
com.eyecon.global (main)defaultEverything else. MyApplication.attachBaseContext sets f12170o = processName.equals("com.eyecon.global") (MyApplication.java:383); the whole 401/E-Mark wipe path and DB init are gated on this flag.
:notification_processIdPlus.NotificationReader.NotificationReaderServiceNotificationListenerService - isolated so the WhatsApp/Telegram notification scraper cannot crash the caller-ID process.
:restart_appOthers.Activities.RestartAppActivityTrampoline activity that kills the app process and restarts it (System.out.println("killProcess RestartAppActivity, pid = ..."), RestartAppActivity.java:28).

MyApplication.onCreate (MyApplication.java:471-474) returns immediately for non-main processes, so DB / analytics / crash handling only initialise in the main process.

1.2 Activities (96)h5

Legend for exported: true = android:exported="true" (or effectively exported via a browsable/intent filter); default = no attribute -> defaults to false because the component declares no intent-filter.

App activitiesh6
ActivityexportedlaunchModetaskAffinity / processIntent filters (action -> data)Notes
MainScreen.MainActivitytruesingleTop-MAIN(LAUNCHER,DEFAULT,BROWSABLE); com.android.phone.action.RECENT_CALLS(TAB); VIEW/eyecon, VIEW/tl-1d6737ca; DIAL/tel; VIEW+DIAL/tel; DIAL + mime vnd.android.cursor.item/phone, .../person; DIAL/voicemail; VIEW + mime vnd.android.cursor.dir/calls; CALL_BUTTON; com.android.phone.action.TOUCH_DIALER(TAB); SEARCH; CALL(LAUNCHER); CALL/tel; VIEW + eyecon.notification.action(LAUNCHER); SEND + text/plain; SEND + video/*20 filters. Primary deeplink handler; deep-link router with ~20 host branches. Portrait; adjustPan.
Backup.BackupActivitydefault---
Call.HistoryLogActivitydefault---
Others.Activities.RestartAppActivitydefault-:restart_app-separate process
Registration.RegistrationActivitydefaultsingleTask--4354 lines; sends verification SMS
Registration.PhoneNumberValidationActivitytruesingleTask-VIEW/eyecon_number_validation (BROWSABLE)onNewIntent trusts external extras - see F-02 (M)
MoreMenuAndSettings.MyProfileActivitydefault---
Contacts.NewContactActivitytrue--INSERT_OR_EDIT/APP_CONTACTS + mime contact/vcard; INSERT + mime cursor.dir/person|contact|raw_contactContacts-app integration
Photos.PhotoPicker.PhotoPickerActivitydefault---camera capture + crop; FileProvider output URI
CanTalk.CanTalkActivitydefaultsingleInstance.CanTalkActivity, excludeFromRecents, noHistory-
Others.Activities.DummyActivitytrue-.eyecon, excludeFromRecentsVIEW/eyecon_request (BROWSABLE)Also reachable by explicit intent with arbitrary action + extras - see M-04
MoreMenuAndSettings.SettingActivitytruesingleTask-VIEW/eyecon_settings (BROWSABLE)
MoreMenuAndSettings.AboutActivitydefault-.AboutActivity-
MoreMenuAndSettings.LicensesActivitydefault-.AboutActivity-
Others.Activities.TestActivitydefaultsingleTask--Reads cli extra
AfterCall.CompactAfterCallActivitydefaultsingleInstance.AfterCallActivity, excludeFromRecents-
AfterCall.BaseAfterCallActivitydefaultsingleInstance.AfterCallActivity, excludeFromRecents, noHistory, showWhenLocked + turnScreenOn-registers 3 dynamic receivers
AfterCall.AfterCallWithNoAutoFinishdefaultsingleInstance.AfterCallActivity, excludeFromRecents, showWhenLocked + turnScreenOn-
AfterCall.DefaultAfterCallActivitydefaultsingleInstance.AfterCallActivity, excludeFromRecents-
Others.Activities.FragmentsActivitydefault---
Billing.Store.StoreActivitytrue--VIEW/eyecon_store (no category)JS-enabled WebView + 2 JS bridges, remote-config URL - see H-01
Billing.Premium.PremiumPurchasingDefaultActivitydefaultsingleTask--
Billing.Premium.PremiumPurchasingPaywallUiActivitydefaultsingleTask--
Billing.Premium.GoldPremiumPurchasingActivitydefaultsingleTask--
DefaultDialer.CallActivitydefault-.CallActivityTask, showWhenLocked + turnScreenOn + showForAllUsers + autoRemoveFromRecents-in-call UI
MainScreen.Communication.EyeSearchActivitydefault---
Contacts.ContactsChooserActivitydefault---
Billing.Premium.GetFreePremiumActivitydefaultsingleTask--
Billing.Premium.FreePremiumUserActivitytruesingleTask-EYECON_PREMIUM_UPDATE (BROWSABLE)
Billing.Premium.PremiumUserActivitydefaultsingleTask--
Sms.SmsWindowActivitydefault-.SmsWindowActivity-
ContactReminder.ContactReminderActivitydefault-.ContactReminderActivity-
IdPlus.NotificationReader.NotificationReaderWindowActivitydefault-.NotificationReaderActivity-renders scraped notification text
IdPlus.Telegram.TelegramWindowActivitydefault-.TelegramWindowActivity, excludeFromRecents-
Call.TalkToMe.p204UI.TtmWindowActivityfalsesingleInstance.NotificationReaderActivity, excludeFromRecents, noHistory-
Toki.TokiActivitydefaultsingleTop.toki-
Toki.TokiContactsChooserActivitydefaultsingleTop.toki-
Permissions.ExtendedPermissionPromptActivitydefault---
Themes.CustomThemePreviewActivitydefault---
Call.TalkToMe.p204UI.TtmTutorialActivitydefault---
AfterCall.SplashInterstitialActivityfalse-.AfterCallActivity-
Registration.Onboarding.OnboardingActivitydefault---
Registration.Onboarding.FakeCallActivitydefault---
Ads.Google.FullscreenAds.FullscreenNativeAdsActivitydefault---
DefaultDialer.assistant.OnboardingActivitydefault---
Billing.Premium.WebViewPaywall.WebviewPaywallActivitydefault---JS bridge Eyecon - see M-06
com.yalantis.ucrop.UCropActivitydefault---image crop
com.facebook.FacebookActivitydefault---
com.facebook.CustomTabActivitytrue--VIEW/@string/fb_login_protocol_scheme = fb1716660955257637; VIEW/fbconnect host cct.com.eyecon.globalFB SDK OAuth callback
com.facebook.CustomTabMainActivitydefault---
Third-party / SDK activitiesh6
ActivityexportedNotes
tech.crackle.cracklertbsdk.vast.ShowFullScreenVideotrueno filter -> explicit intent only. Ad-SDK “show me an ad” surface (L-05)
tech.crackle.cracklertbsdk.ShowFullScreenStatictruesame
tech.crackle.core_sdk.ads.CrackleFullScreenActivitytruesame
com.amazon.aps.ads.activity.ApsInterstitialActivitytruesame
com.amazon.device.ads.DTBInterstitialActivitytruesame
com.google.android.gms.ads.AdActivityfalsemultiprocess="true"
com.google.android.gms.ads.NotificationHandlerActivityfalsesingleTask
com.bytedance.sdk.openadsdk.activity.* (13 activities)default (false)no filters
com.moloco.sdk.xenoss...* (4 activities)false
com.inmobi.ads.rendering.InMobiAdActivitydefault (false)
com.vungle.ads.internal.p226ui.VungleActivitydefault (false)
com.fyber.* (7 activities)default (false)
com.mbridge.msdk.* (4 activities)default/false
com.facebook.ads.AudienceNetworkActivityfalse
com.android.billingclient.api.ProxyBillingActivity(V2)false
androidx.credentials.playservices.HiddenActivityfalse
com.google.android.gms.auth.api.signin.internal.SignInHubActivityfalse
com.google.android.gms.common.api.GoogleApiActivityfalse
com.google.android.play.core.common.PlayCoreDialogWrapperActivityfalse
com.fyber.fairbid.sdk.testsuite.views.TestSuiteActivitydefault (false)debug surface of the ad SDK
activity-aliases (4) - all exported="true" but enabled="false"h6
AliastargetIcon
com.eyecon.global.pinkMainScreen.MainActivityic_launcher_white_icon_pink_bg
com.eyecon.global.blueMainScreen.MainActivityic_launcher_white_icon_blue_bg
com.eyecon.global.greenMainScreen.MainActivityic_launcher_white_icon_green_bg
com.eyecon.global.purpleOthers.Activities.TestActivityic_launcher_white_icon_purple_bg

All four are disabled in the shipped build (branding-variant leftovers) - not reachable at runtime.

1.3 Services (27)h5
Serviceexportedpermission guardprocess / FGS typeIntent filterNotes
Call.CallServicedefault(false)-main / specialUse-Foreground caller-ID service. Restarter chain: onDestroy -> eyecon.CallServiceRestart -> CallServiceRestarterReceiver. Subtype: “Used to tunnel incoming calls from a broadcast-receiver and determine if the caller ID should be shown to the user.”
IdPlus.NotificationReader.NotificationReaderServicetrueBIND_NOTIFICATION_LISTENER_SERVICE:notification_processandroid.service.notification.NotificationListenerServicemeta-data disabled_filter_types=silent. Reads WhatsApp/Telegram notification title+text
Others.Tasks.ForegroundLauncherServicedefault(false)-main / specialUse-Subtype: “A workaround service that prevent crashes while trying to start other services”
Toki.TokiServicedefault(false)-main / specialUse-
DefaultDialer.CallStateServicetrueBIND_INCALL_SERVICEmain / phoneCallandroid.telecom.InCallServicemeta-data IN_CALL_SERVICE_UI=true, INCLUDE_SELF_MANAGED_CALLS=true, IN_CALL_SERVICE_RINGING=false
DefaultDialer.EyeconCallScreeningServicetrueBIND_SCREENING_SERVICEmainandroid.telecom.CallScreeningServiceReaches MyPhoneReceiver via RunnableC17334k case 7
Others.Tasks.JobsServicetrueBIND_JOB_SERVICEmain-only JobScheduler can bind
Push.PushServicefalse-maincom.huawei.push.action.MESSAGING_EVENT, com.google.firebase.MESSAGING_EVENT
PhoneAccount.SyncServicetruenonemainandroid.content.SyncAdapterStandard pattern. supportsUploading=false, userVisible=true, contentAuthority=com.android.contacts
PhoneAccount.EyeconAccountAuthServicetruenonemainandroid.accounts.AccountAuthenticatorStandard pattern (system binds)
Sms.SmsJobServicetrueBIND_JOB_SERVICEmain-
SocialMediaShare.ShareMediaJobtrueBIND_JOB_SERVICEmain-
com.google.firebase.components.ComponentDiscoveryServicefalse-directBootAware-20 ComponentRegistrar meta-data entries (Messaging, Crashlytics, Perf, RemoteConfig, Sessions, Installations, ABT, DataTransport, InAppMessaging, AnalyticsConnector)
com.google.firebase.messaging.FirebaseMessagingServicefalse-directBootAwarecom.google.firebase.MESSAGING_EVENT (priority -500)
com.google.android.gms.auth.api.signin.RevocationBoundServicetrue...signin.permission.REVOCATION_NOTIFICATION--visibleToInstantApps
com.google.android.gms.measurement.AppMeasurementService / AppMeasurementJobService / AppMeasurementReceiverfalse(JobService)--
com.google.android.gms.ads.AdServicefalse---
com.google.firebase.sessions.SessionLifecycleServicefalse---
com.google.android.datatransport.runtime.backends.TransportBackendDiscoveryfalse---meta-data backend:...CctBackendFactory
...jobscheduling.JobInfoSchedulerServicefalseBIND_JOB_SERVICE--
androidx.work.impl.background.systemjob.SystemJobServicetrueBIND_JOB_SERVICE--
androidx.work.impl.background.systemalarm.SystemAlarmServicefalse---enabled=@bool/enable_system_alarm_service_default
androidx.work.impl.foreground.SystemForegroundServicefalse---
androidx.room.MultiInstanceInvalidationServicefalse-directBootAware-
androidx.credentials.playservices.CredentialProviderMetadataHolderfalse---
com.bytedance.sdk.openadsdk.multipro.aidl.BinderPoolServicedefault(false)---
1.4 Receivers (27)h5
Receiverexportedpermission guardpriorityIntent filter
IdPlus.NotificationReader.NotificationBroadcastReceiverfalse--eyecon.action_notification_posted
Others.Receivers.BootStartReceivetrue-2147483647BOOT_COMPLETED
Others.Receivers.OnPackageReplaceReceivertrue-2147483647ACTION_PACKAGE_REPLACED
Others.Receivers.OnDataClearedReceivertrue-2147483647ACTION_PACKAGE_DATA_CLEARED
Call.MyPhoneReceivertruenone2147483647com.eyecon.global.action.INTENT_ACTION_START_CALL_WHATSAPP, PHONE_STATE, NEW_OUTGOING_CALL, com.eyecon.global.action.CALL_SCREENING_NEW_CALL - 2 of these 4 actions are unprotected app-defined strings (H-02)
Sms.MySmsReceivertrueBROADCAST_SMS2147483647android.provider.Telephony.SMS_RECEIVED - system-only sender; no abortBroadcast()
DefaultDialer.assistant.MessageAssistantBroadcastReceivertrueBROADCAST_SMS-SMS_RECEIVED - extracts body + sender, serialises to JSON, enqueues MessageAssistantReplyWorker (network required)
Call.CallServiceRestarterReceiverfalse--eyecon.CallServiceRestart (label RestartServiceWhenStopped)
Others.Receivers.MainProcessReceiverfalse--EYECON_ACTION_GET_CS, ...RECORDING_SUCCESS, ...EXCEPTION, ...REPORT_EVENT
Registration.InstallReferrerReceivertrueandroid.permission.INSTALL_PACKAGES (sender must be Play Store / system-signed)-com.android.vending.INSTALL_REFERRER
Analytics.RemoteConfig.RemoteConfigDebugTestReceivertruenone-eyecon_remote_config_test - body is a no-op (result of equalsIgnoreCase is discarded)
Registration.SMSRetrieverBroadcasttruenone-com.google.android.gms.auth.api.phone.SMS_RETRIEVED, com.huawei.hms.auth.api.phone.SMS_RETRIEVED - re-broadcasts the extras as app-local INTENT_ACTION_AUTH_SMS_RECEIVED
com.facebook.CurrentAccessTokenExpirationBroadcastReceiverfalse--com.facebook.sdk.ACTION_CURRENT_ACCESS_TOKEN_CHANGED
com.facebook.C5849x7a527f5cfalse--...ACTION_CURRENT_AUTHENTICATION_TOKEN_CHANGED
com.google.firebase.iid.FirebaseInstanceIdReceivertruecom.google.android.c2dm.permission.SEND-com.google.android.c2dm.intent.RECEIVE; FINISHED_AFTER_HANDLED=true
com.google.android.gms.measurement.AppMeasurementReceiverfalse---
androidx.work.* (7 receivers)falseDiagnosticsReceiver has DUMP-battery/storage/network constraint proxies, RescheduleReceiver, ForceStopRunnable.BroadcastReceiver
androidx.work.impl.diagnostics.DiagnosticsReceivertrueandroid.permission.DUMP-androidx.work.diagnostics.REQUEST_DIAGNOSTICS
androidx.profileinstaller.ProfileInstallReceivertrueandroid.permission.DUMP-4 androidx.profileinstaller.action.*
com.google.android.datatransport...AlarmManagerSchedulerBroadcastReceiverfalse---
com.mbridge.msdk.foundation.same.broadcast.NetWorkChangeReceivertrue--android.net.conn.CONNECTIVITY_CHANGE (protected broadcast - system-only)
1.5 Providers (9)h5
ProviderexportedauthoritygrantUriPermissionsinitOrdermeta-data
com.eyecon.global.Others.Objects.EyeconInitProviderfalsecom.eyecon.global.EyeconInitProvider-101init-order provider that initialises the Facebook SDK (m10352a)
androidx.core.content.FileProviderfalse@string/file_provider = com.eyecon.global.fileprovidertrue-android.support.FILE_PROVIDER_PATHS -> @xml/file_paths
com.facebook.FacebookContentProvidertruecom.facebook.app.FacebookContentProvider1716660955257637--Required by the FB SDK because this app uses multiple processes
com.google.android.gms.ads.MobileAdsInitProviderfalsecom.eyecon.global.mobileadsinitprovider-100-
androidx.startup.InitializationProviderfalsecom.eyecon.global.androidx-startup--WorkManager, EmojiCompat, ProcessLifecycle, ProfileInstaller, OkHttp Platform initializers
com.fyber.fairbid.internal.UncaughtExceptionHandlerContentProviderfalsecom.eyecon.globalfalse101authority == the app’s own package name
com.vungle.ads.VungleProviderfalsecom.eyecon.global.vungle-provider-102-
com.facebook.ads.AudienceNetworkContentProviderfalsecom.eyecon.global.AudienceNetworkContentProvider---
com.squareup.picasso.PicassoProviderfalsecom.eyecon.global.com.squareup.picasso---

Orphan resource: res/xml/image_share_filepaths.xml (files-path name="image_provider_images" path="image_provider/") exists and is referenced only from R.xml.image_share_filepaths (R.java:12701). No provider in the manifest points at it - dead config.

1.6 <meta-data>h5
On <application> (22)h6
NameValueNote
com.google.android.gms.ads.APPLICATION_ID@string/admob_app_id -> ca-app-pub-2959190743815944~6092014645AdMob app id (public)
com.google.android.gms.ads.flag.OPTIMIZE_INITIALIZATIONtrue
com.google.android.gms.ads.flag.OPTIMIZE_AD_LOADINGtrue
com.google.android.gms.ads.flag.rtb_enabledtrueReal-time bidding on
com.google.android.gms.ads.INTEGRATION_MANAGERwebview
com.google.android.geo.API_KEYAIzaSyD1LLixZGTwP6i3Ts8AzX4jxdb9zuaikKoGoogle Maps key, hardcoded in the manifest; must be package+SHA-1 restricted server-side or it is an open key for quota abuse
com.google.android.gms.version@integer/google_play_services_version
com.google.android.gms.analytics.globalConfigResource@xml/global_trackerga_logLevel=verbose, ga_dryRun=false, ga_dispatchPeriod=1800, ga_autoActivityTracking=true
com.facebook.sdk.ClientToken@string/facebook_client_token -> afa3f38c55f24c73b177afc962969b15FB app id 1716660955257637
com.facebook.sdk.AutoLogAppEventsEnabledfalseFB auto-app-events off
com.facebook.sdk.AdvertiserIDCollectionEnabledfalseFB advertiser-ID collection off
com.facebook.sdk.AutoInitEnabledfalsemanual FB init from EyeconInitProvider
crackle_key@string/crackle_key -> fai=1:917722272265:android:af67...fak=AIzaSyBxXQfYa0khnAZ2-NiK3WQV-4JbMWCw6ls~~~fpi=eyecon---eyecon-caller-id~~~s5=417687~~~s6=234363Ad-SDK keys incl. a second Google API key
firebase_performance_logcat_enabledfalse
com.google.firebase.messaging.default_notification_icon@drawable/ic_eyecon_icon_shape
com.google.firebase.messaging.default_notification_color@color/app_color
com.google.android.play.billingclient.version7.1.1
com.bytedance.sdk.pangle.version7.6.0.3Pangle ad SDK
com.android.dynamic.apk.fused.modulesbase
com.android.stamp.sourcehttps://play.google.com/store
com.android.stamp.typeSTAMP_TYPE_STANDALONE_APK
com.android.vending.splits@xml/splits0
On components (36)h6
Ownermeta-data
NotificationReaderServiceandroid.service.notification.disabled_filter_types = silent
CallStateServiceandroid.telecom.IN_CALL_SERVICE_UI=true, android.telecom.INCLUDE_SELF_MANAGED_CALLS=true, android.telecom.IN_CALL_SERVICE_RINGING=false
FileProviderandroid.support.FILE_PROVIDER_PATHS -> @xml/file_paths
SyncServiceandroid.content.SyncAdapter -> @xml/eyecon_account_sync_adapter; android.provider.CONTACTS_STRUCTURE -> @xml/eyecon_contacts_account
EyeconAccountAuthServiceandroid.accounts.AccountAuthenticator -> @xml/eyecon_contacts_account_auth
ComponentDiscoveryService20 x com.google.firebase.components:<registrar>
TransportBackendDiscoverybackend:com.google.android.datatransport.cct.CctBackendFactory = cct
CredentialProviderMetadataHolderandroidx.credentials.CREDENTIAL_PROVIDER_KEY
InitializationProvider5 x androidx.startup initializers
FirebaseInstanceIdReceivercom.google.android.gms.cloudmessaging.FINISHED_AFTER_HANDLED=true
<property> (foreground-service subtypes)h6
ComponentPROPERTY_SPECIAL_USE_FGS_SUBTYPE
Call.CallService”Used to tunnel incoming calls from a broadcast-receiver and determine if the caller ID should be shown to the user.”
Others.Tasks.ForegroundLauncherService”A workaround service that prevent crashes while trying to start other services”
13 x com.bytedance.sdk.openadsdk.activity.TT*android.window.PROPERTY_COMPAT_ALLOW_RESTRICTED_RESIZABILITY=true
1.7 Self-declared <permission> (3)h5
NameprotectionLevelNotes
android.permission.INTERACT_ACROSS_USERSsignatureRe-declaration of a framework permission name. A platform permission cannot be redefined by a third-party app; this is a no-op and grants nothing. Dead code / leftover.
com.eyecon.global.permission.C2D_MESSAGEsignatureUsed by PushService’s FCM intent filter. Correctly signature-guarded.
com.eyecon.global.DYNAMIC_RECEIVER_NOT_EXPORTED_PERMISSIONsignatureStandard androidx DynamicReceiver self-permission. The app also requests it.
1.8 <queries> - package visibilityh5
Messaging / social apps (detected via getInstalledApplications)h6

The detection primitive is AbstractC0062l0.m334C1(pkg) -> m333B1 (p008a7/AbstractC0062l0.java:123-163), which calls PackageManager.getInstalledApplications(128) and caches enabled package names in a HashSet (f324i). Without QUERY_ALL_PACKAGES (which the app does not request) this returns a filtered list on API 30+, so each package must be declared in <queries> - and all of these are.

CategoryPackagesConsumer
Messaging (enum-driven)com.whatsapp, com.whatsapp.w4b, org.telegram.messenger, com.viber.voip, jp.naver.line.android, com.kakao.talk, com.tencent.mm (WeChat), com.tencent.mobileqq (QQ), com.facebook.orca (Messenger), com.google.android.talk (Hangouts), com.google.android.apps.messaging, com.snapchat.android, com.skype.raider, com.twitter.android, kik.android, com.oovoo, com.imo.android.imoim, com.nimbuzz, com.path.paperboy, com.yahoo.mobile.client.android.im, com.sgiggle.production (Tango), com.sec.chaton, com.turkcell.bip, com.azarlive.android, com.sina.weibo, com.bbmp516r5.EnumC19861h0 (social-action enum; ctor line 108 calls m334C1("com.whatsapp") / ("com.whatsapp.w4b")); p511r0.C19813a; p034b7.C3502i; p516r5.C19867k0
Social networkscom.instagram.android, com.facebook.katana, com.linkedin.android, com.vkontakte.androidsame enum; social-profile scraping in C3502i / C19867k0
Competitor caller-ID appscom.webascender.callerid (Truecaller), com.truecaller, com.callapp.contacts, mobi.drupe.app (Druper), com.contapps.android, com.bsb.hike, com.sgiggle.productionRegistrationActivity.java:2872; migration/offers
Browsers / mapscom.google.android.browser, com.waze, com.google.android.apps.maps, com.google.android.apps.mapslite, com.sygic.auradefault-dialer / map hand-off
Telecomcom.android.server.telecomCallScreeningService / default-dialer role checks
OEM security / autostart managerscom.miui.securitycenter, com.huawei.systemmanager, com.oppo.safe, com.coloros.safecenter, com.iqoo.secure, com.vivo.permissionmanager, com.letv.android.letvsafe, com.asus.mobilemanager, com.evenwell.powersaving.g3p720zq.AbstractC22312g - 21 setClassName(...) calls to OEM autostart/permission screens (lines 162-210, 1274-1322)
Emulatorscom.google.android.launcher.layouts.genymotion, com.bluestacks, com.bignox.appFacebook SDK (p284f7.C16489q, lines 60-62) - not app code. Same class holds IMEI/serial blacklist arrays and goldfish/genyd checks
MIUI / iQOO deep-link targetsintents miui.intent.action.APP_PERM_EDITOR, com.iqoo.secure.PERMISSION_MANAGER, com.iqoo.secure.BGSTARTUPMANAGERAbstractC22312g
<intent> queries (26)h6
QueryConsumer / purpose
SEND + message/rfc822; SENDTO + mailtoCompose email (AbstractC0062l0.m336E1)
INSERT + content schemeContacts insertion
INSERT + vnd.android.cursor.dir/eventCalendar event creation
VIEW + geoNavigation hand-off
VIEW + sms; DIAL + tel:Messaging / dialer hand-off
telecom.action.CHANGE_DEFAULT_DIALERDefault-dialer role request
settings.APPLICATION_DETAILS_SETTINGS, settings.REQUEST_IGNORE_BATTERY_OPTIMIZATIONS (+ package: scheme)Battery-optimisation exemption prompts
VIEW + https / http (BROWSABLE)Custom-Tabs / in-app browser
VIEW + marketPlay Store rating
androidx.browser.customtabs.CustomTabsService, android.support.customtabs.action.CustomTabsServiceCustom Tabs
com.android.vending.billing.InAppBillingService.BIND; ...play.billingtestcompanion.BillingOverrideService.BINDPlay Billing / licence testing
com.singular.preinstall.READ_PERMISSION_SINGULARSingular pre-install attribution
com.digitalturbine.ignite.cl.IgniteRemoteServiceDigital Turbine Ignite ad SDK
MAIN, VIEW (bare)generic

2. Resources of interesth4

2.1 res/xml/network_security_config.xmlh5
<network-security-config>
<base-config cleartextTrafficPermitted="true">
<trust-anchors><certificates src="system"/></trust-anchors>
</base-config>
<debug-overrides cleartextTrafficPermitted="true">
<trust-anchors><certificates src="user"/></trust-anchors>
</debug-overrides>
<domain-config cleartextTrafficPermitted="true">
<domain includeSubdomains="true">127.0.0.1</domain>
</domain-config>
</network-security-config>
  • Cleartext is permitted globally (base-config), reinforced by android:usesCleartextTraffic="true" on <application>. Nothing is scoped down to a single legacy host.
  • No certificate pinning anywhere.
  • debug-overrides trusts user-installed CAs - correct Android behaviour, only active for debuggable builds / ro.debuggable. This is a release APK, so it is inert. Not a finding.
  • domain-config for 127.0.0.1 is vestigial.
2.2 Backup configurationh5

android:allowBackup="false" and android:fullBackupContent="false". No android:dataExtractionRules (introduced API 31). With allowBackup="false" both adb backup and cloud/D2D transfer are disabled. This is correct and is a positive finding.

2.3 Provider path configsh5

res/xml/file_paths.xml (referenced by the FileProvider):

<external-path name="external" path="."/>
<external-files-path name="external_files" path="."/>
<cache-path name="cache" path="."/>
<external-cache-path name="external_cache" path="."/>
<files-path name="files" path="."/>

All five roots are path=".". Combined with android:grantUriPermissions="true", any file under shared external storage, external files, cache, external cache, or internal files can be handed out as a content://com.eyecon.global.fileprovider/... URI. The provider is exported="false", so only this app can mint URIs - and the single observed call site writes a File.createTempFile(..., getFilesDir()) camera output (PhotoPickerActivity.java:775 + :1012). Exploitability therefore depends on some other code path handing a URI to a third-party app; none was found. Hardening issue, not an exploitable bug (L-06).

res/xml/image_share_filepaths.xml - orphan, not referenced by any provider.

2.4 Account / sync-adapter XMLh5
  • eyecon_contacts_account_auth.xml - accountType="com.eyecon.global", label/icon from the app.
  • eyecon_account_sync_adapter.xml - accountType="com.eyecon.global", contentAuthority="com.android.contacts", userVisible="true", supportsUploading="false", allowParallelSyncs="false", isAlwaysSyncable="true".
  • eyecon_contacts_account.xml - full ContactsAccountType EditSchema (name/photo/phone/email/nickname/im/postal/organization/website/sip/note/event/relationship DataKinds).
2.5 res/xml/ad_services_config.xmlh5
<ad-services-config>
<attribution allowAllToAccess="true"/>
<custom-audiences allowAllToAccess="true"/>
<topics allowAllToAccess="true"/>
</ad-services-config>

All three Privacy Sandbox surfaces fully opened. Combined with ACCESS_ADSERVICES_ATTRIBUTION/AD_ID/CUSTOM_AUDIENCE/TOPICS and com.google.android.gms.permission.AD_ID this is the maximum ad-tracking posture.

2.6 Otherh5
  • res/xml/global_tracker.xml - legacy Google Analytics for Firebase config; ga_logLevel=verbose, ga_dryRun=false, ga_autoActivityTracking=true, dispatch every 1800 s.
  • res/xml/splits0.xml - Play feature-split (APK set) metadata.

3. Security / privacy surfaceh4

3.1 Permissions that grant access to personal data, and who consumes themh5
PermissionLevelConsumer (file)Data actually touched
READ_SMSdangerousMainActivity:500, MainActivity:2499, SmsJobService:59, SmsFragment:117/135/180, RegistrationActivity:2002SMS inbox for the in-app SMS window
RECEIVE_SMSdangerousSmsFragment:137-
SEND_SMSdangerousRegistrationActivity:816/2002, PhoneNumberValidationActivity:523/848, MainActivity:1671/2495sends the verification SMS
READ_CONTACTS / WRITE_CONTACTSdangerousDBContacts:2033; AccountWithDataSet:37 (ContactsContract.RawContacts.CONTENT_URI); SyncAdapter writes into com.android.contactsfull contact graph read + write
READ_CALL_LOGdangerousDBContacts:280/1964/2033, EyeconAnalyticsWorker:250, RegistrationActivity:3923, PhoneNumberValidationActivity:1242call history
WRITE_CALL_LOGdangerousRegistrationActivity:3923, PhoneNumberValidationActivity:1242writes call-log entries
READ_PHONE_STATE / READ_PHONE_NUMBERSdangerousfingerprint builder p620w6.C21280hIMEI, phone number, SIM count
GET_ACCOUNTSdangerous (deprecated)- (no direct reference found)declared, apparently unused
AUTHENTICATE_ACCOUNTS maxSdkVersion=22deprecated-inert on minSdk 23
CALL_PHONEdangerousdialer actions (C3502i, C19867k0)places calls
ANSWER_PHONE_CALLSdangerousCallStateService / CallActivityanswers incoming calls
PROCESS_OUTGOING_CALLSdeprecatedMyPhoneReceiver NEW_OUTGOING_CALLmonitors outgoing dialling
MANAGE_OWN_CALLS (+ uses-permission-sdk-23)normalCallStateService, EyeconCallScreeningServiceeffective only when the app is the default dialer
RECORD_AUDIOdangerousRecordingsFragment:180/205/347/450 (user-facing recordings list); p599v7.RunnableC20908f:508 -> new AudioRecord(1 /* VOICE_CALL */, 8000, 16, 2, 16000) + AmrEncoderTalk-To-Me records the call audio stream, AMR-encodes it and uploads to voice2text.eyecon-app.com. Companion playback loop in p633wl.RunnableC21441d
CAMERAdangerousPhotoPickerActivity:427/1008, CallActivity:2684/2701contact-photo capture
ACCESS_FINE_LOCATION / ACCESS_COARSE_LOCATIONdangerousno LocationManager / FusedLocationProvider reference found in com.eyeconlikely ad-SDK only (InMobi / Pangle / Fyber location targeting)
POST_NOTIFICATIONSruntimenotification channels caller_id_status, caller_id_v2 (CallService:134/153)
ACTIVITY_RECOGNITIONdangerousno reference found in com.eyeconad-SDK
READ_APP_BADGE + 17 vendor badge permissionsvendorme.leolin.shortcutbadger.impl.*launcher badge counts
WRITE_EXTERNAL_STORAGE / READ_EXTERNAL_STORAGEdeprecatedRecordingsFragment:185/206/437see 3.4 / M-05
BIND_NOTIFICATION_LISTENER_SERVICE (service guard)signatureNotificationReaderServicenotification content of all apps
SYSTEM_ALERT_WINDOWappopAbstractC22312g:612 -> android.settings.action.MANAGE_OVERLAY_PERMISSION”draw over notifications” caller-ID popup
DISABLE_KEYGUARDdeprecated/normalAbstractC0046d0.m191J (KeyguardManager.requestDismissKeyguard)

Headline privacy items

  1. NotificationListenerService scrapes WhatsApp/Telegram notification title (= phone number) and text (= message body) - p252ds/RunnableC16004e.java:442-518. Gated by user-granted Notification Access plus the readNotificationTelegramEnable remote flag / IS_TELEGRAM_WINDOW_ENABLE_BY_USER pref.
  2. MessageAssistantBroadcastReceiver forwards full incoming SMS bodies to MessageAssistantReplyWorker, which requires network connectivity.
  3. AudioRecord(VOICE_CALL, ...) records live call audio for Talk-To-Me and uploads it to voice2text.eyecon-app.com.
  4. Device fingerprint sent with every API call (p620w6.C21280h): device_id (ANDROID_ID), adv_id, imei, mc = MAC address of wlan0, installer, SIM count, locale, timezone, manufacturer/model.
3.2 Exported components reachable by other appsh5
ComponentWhat an external app can do
MainScreen.MainActivity20 filters incl. BROWSABLE. Send/launch with attacker-chosen eyecon:// URI. Router branches include browser (intent redirection, M-03), url (opens link / dismiss_url), menifa?phone_number=, showdialog, support_email, open_must_permissions, record_calls, show_themes_dialog, contest_*, tel. Also accepts android.intent.action.SEND with android.intent.extra.TEXT (MainActivity<625>), parsed for tiktok.com / facebook.com / instagram.com URLs.
Others.Activities.DummyActivityeyecon_request:// (host must be caller_id_role for the deeplink path) or an explicit intent with any action - see M-04.
MoreMenuAndSettings.SettingActivityeyecon_settings://
Registration.PhoneNumberValidationActivityeyecon_number_validation://; onNewIntent reads state / phoneNumber / smscode from any sender - see M-02
Billing.Store.StoreActivityeyecon_store://?source=...[&product_id=..&item_id=..] -> JS bridge - see H-01
Billing.Premium.FreePremiumUserActivityEYECON_PREMIUM_UPDATE
Contacts.NewContactActivityContacts-app INSERT / INSERT_OR_EDIT intents
com.facebook.CustomTabActivityfb1716660955257637:// and fbconnect://cct.com.eyecon.global - the only legitimate purpose is the FB OAuth redirect
Others.Receivers.MyPhoneReceiverUnprotected custom actions -> H-02
Analytics.RemoteConfig.RemoteConfigDebugTestReceiverBroadcast eyecon_remote_config_test - no-op body
Registration.SMSRetrieverBroadcastBroadcast either SMS_RETRIEVED action - forwards the Status extras onward
Registration.InstallReferrerReceiverGuarded by sender-side INSTALL_PACKAGES -> Play Store only
Sms.MySmsReceiver, ...assistant.MessageAssistantBroadcastReceiverGuarded by BROADCAST_SMS -> system only
tech.crackle.*ShowFullScreen*, tech.crackle.core_sdk.ads.CrackleFullScreenActivity, com.amazon.aps.ads.activity.ApsInterstitialActivity, com.amazon.device.ads.DTBInterstitialActivityexported="true", no filter, no permission -> any installed app can startActivity them by explicit intent to force an ad impression (revenue theft). L-05
com.facebook.FacebookContentProviderexported="true", no permission -> any app can query() its installreferrer / bridge tables
Exported services with guardsNotificationReaderService, CallStateService, EyeconCallScreeningService (BIND_* signature/system), JobsService, SmsJobService, ShareMediaJob, SystemJobService (BIND_JOB_SERVICE), RevocationBoundService (GMS perm), DiagnosticsReceiver / ProfileInstallReceiver (DUMP) - all safe
PhoneAccount.SyncService / EyeconAccountAuthServiceexported="true", no permission, but only bindable by the system SyncManager / AccountManager - standard and not exploitable
3.3 Dynamic receiver registrationh5

ContextCompat.registerReceiver flag constants: 2 = RECEIVER_EXPORTED, 4 = RECEIVER_NOT_EXPORTED.

SiteFilterFlagsAssessment
AfterCall/BaseAfterCallActivity:984android.intent.action.SCREEN_OFF2Protected broadcast (system-only) -> not exploitable
AfterCall/BaseAfterCallActivity:1000android.intent.action.CLOSE_SYSTEM_DIALOGS2Protected broadcast -> not exploitable
DefaultDialer/CallActivity:2483android.bluetooth.devicepicker.action.DEVICE_SELECTED2Unprotected. Any app can spoof a “BT device selected” event into the dialer’s device picker (L-04)
Registration/RegistrationActivity:837-838ACTION_SMS_SENT_EYECON, ACTION_SMS_DELIVERED_EYECON2Unprotected app-defined actions (L-03)
Registration/PhoneNumberValidationActivity:541-542same two2same
CallStateService:973, CallStateService:1833, RegistrationActivity:4174, PhoneNumberValidationActivity:1344, TelegramWindowActivity:301, SpamAndBlockFragment:149, SmsFragment:247, BaseAfterCallActivity:556various EYECON_*, INTENT_*, EYECON_ACTION_CALL_TO_FLASH_FOUND4 (NOT_EXPORTED)Correct
Others.MyApplication:508overrides ContextWrapper.registerReceiver(receiver, filter) in a try/catch and swallows all throwables-Hides ReceiverLeaked / SecurityException failures at the framework boundary

Internal broadcasts are package-scoped by convention: AbstractC0062l0.m343L1(ctx, intent, src, true) calls intent.setPackage(getPackageName()) before sendBroadcast (p008a7/AbstractC0062l0.java:267-273). Good hygiene where used.

3.4 WebView usageh5
SiteJSJS bridgesURL sourceOrigin enforcement
Billing/Store/StoreActivity:217-226onaddJavascriptInterface(new C16132d(), "Eyecon") and addJavascriptInterface(C17757i, "EyeconStore"); setAcceptThirdPartyCookies(true); dom storage onAbstractC15804d.m31091l("store_url", false) -> remote config, default "" (p236d5/AbstractC15804d.java:295)none. C5846m does not override shouldOverrideUrlLoading -> the WebView follows any link, and the bridges are bound to the WebView, not to an origin
Billing/Premium/WebViewPaywall/WebviewPaywallActivity:266-273on"Eyecon" (C16132d) and "Android" (C5838e)paywall model from backendyes - WebviewPaywallActivity.java:282-287: requires https and host ==/endsWith(".eyecon-app.com"), else SecurityException("Untrusted paywall URL: ...") + finish()
ad SDKs (InMobi, Pangle, Moloco, Mbridge, Fyber Inneractive, Crackle)---third-party; present but out of scope

EyeWebViewClient base class (p284f7/AbstractC16490r) only handles onRenderProcessGone. It does not override onReceivedSslError, so the platform default (SslErrorHandler.cancel()) applies - there is no handler.proceed() anywhere in the app. TLS errors are correctly refused.

Bridge capabilities:

  • Eyecon = p262e7.C16132d - getStringSP(key, default) / getBooleanSP / getIntSP / getFloatSP / getLongSP (arbitrary read of the app’s default SharedPreferences file, via MyApplication.m10340b -> new SharedPreferencesC16497y(context)), getAdvertisingId(), getPublicId(), getCid(), getMyName(), reportEvent(name, json), getVersionCode() / getVersionName(), getScreenWidth/Height, getLanguageCode, getCountryISO.
  • EyeconStore = p379k5.C17757i - purchase(...), downloadItem(...), itemSelected(...), getProductsInfo(...), getPurchasedProducts(cb), getDownloadItemsState(cb), getSelectedItems(cb), getUiTextForSubPeriod(key), getPeriodIntoDays, onError, close.
  • Android = C5838e - paywall-specific callbacks.
3.5 PendingIntent / implicit-intent / intent-redirection audith5
  • Every PendingIntent.get* in com.eyecon.global routes through AbstractC0046d0.m185D(flags) (p008a7/AbstractC0046d0.java:70-72), which ORs in a constant on API >= 31. Callers pass 134217728 (FLAG_UPDATE_CURRENT) or 1073741824 (FLAG_ONE_SHOT); no caller passes FLAG_MUTABLE. Sites: CallService:133,156,289, CallStateService:1049,1062,1090,1104,1105,1265,1291,1305,1306, TokiService:53, SmsJobService:127, ForegroundLauncherService:43, RegistrationActivity:828-829, PhoneNumberValidationActivity:535-536, p008a7/AbstractC0077t:175,450, p008a7/AbstractC0060k0:119 (pin-shortcut). NOTE ON CONFIDENCE: JADX printed the ORed constant’s symbolic name as AccessibilityEventCompat.TYPE_VIEW_TARGETED_BY_SCROLL (0x40000000) rather than PendingIntent.FLAG_IMMUTABLE (0x04000000). FLAG_UPDATE_CURRENT | FLAG_ONE_SHOT with neither mutability flag would throw IllegalArgumentException on API 31+, and this is a working targetSdk-35 app, so the constant is almost certainly the immutability flag. See section 6.
  • Empty-intent PendingIntents: CallService:156 and ForegroundLauncherService:43 use PendingIntent.getActivity(ctx, 777, new Intent(), ...) as the notification contentIntent - tapping does nothing. Harmless but sloppy (L-11).
  • Intent redirection: MainActivity.java:836 - the host.equals("browser") branch builds new Intent(ACTION_VIEW, Uri.parse(URLDecoder.decode(data2.getQueryParameter("url"), "UTF-8"))) and then intent5.putExtras(bundleM32028w) (the whole incoming extras bundle), then startActivity(intent5). No scheme allow-list. See M-03.
  • Implicit sendBroadcast helper AbstractC0062l0.m343L1 is package-scoped when the boolean is true; all call sites examined pass true.
3.6 Logging of sensitive datah5
  • All app logging funnels through p410lo.C18416a.m34588y(tag, msg, args...), which is gated on C18416a.f46357c, initialised false and set from the remote-config flag print_logs_in_release at p008a7/RunnableC0074r0.java:264 and p672y5/C21867v.java:862. So verbose logging - including the log lines that contain phone numbers, SMS-derived data, product ids, referral data and full request URLs - is off by default but can be switched on in production by the server (L-01).
  • C18416a.m34565a(str) appends to getFilesDir()/eye_log.txt, rotating at 5 MiB (keeps the last 50 + tail lines). Internal file only; no world-readable exposure found.
  • Only two android.util.Log imports remain in com.eyecon, and both are Log.isLoggable("ShortcutBadge", 3) - no actual writes.
  • Ungated System.out.println -> logcat in release: Registration/InstallReferrerReceiver.java:38,42,62,89,91,94,99,107,110,118 dumps every key and value of the install-referrer bundle plus the raw referrer string; Others/Activities/RestartAppActivity.java:28 prints the pid. Also several System.out.println in p005a4/RunnableC0031c, p008a7/AbstractC0046d0:80, p599v7/RunnableC20908f:496 (L-02).
3.7 The 401 / E-Mark state-wipe pathh5

Fully traced:

C21273a.m38420d(conn, url) p620w6/C21273a.java:112-126
if (responseCode != 401) return responseCode;
if ( !(url.startsWith("https://api.eyecon-app.com/app/") || url.contains("eyecon-app.com"))
|| isEmpty(conn.getHeaderField("E-Mark")) ) return TYPE_DURATION; // 0
if (!MyApplication.f12170o) return 401; // main process only
AbstractC9039h.m10641U(conn.getHeaderField("CID"), "401 1 from url = " + url);

AbstractC9039h.m10641U (com/facebook/appevents/AbstractC9039h.java:592-636) -> unAuthorizedUser:

  1. Bail-outs: already in RegistrationActivity, real phone number known, or SP_unAuthorizedUser_isCleared already true (one-shot latch).

  2. Sets SP_unAuthorizedUser_isCleared = true, clear_data_by_eyecon = true.

  3. C16945i.m32773e(DBContacts.f11708K, new RunnableC19874r(4)) -> wipe (p516r5/RunnableC19874r.java:99-118):

    SQLiteDatabase db = C0041b.m156t().m160D(500L);
    db.delete("history", null, null);
    db.delete("contacts", null, null);
    db.delete("social", null, null); // jadx rendered the literal via NotificationCompat.CATEGORY_SOCIAL
    db.delete("fresh_pics", null, null);
    db.delete("block_list", null, null);
    db.delete("recorded_notes", null, null);
  4. Stops the foreground notification (EYECON, STOP_FOREGROUND_NOTIFICATION -> CallService).

  5. Logs analytics ("User unauthorized").

  6. If an activity is up, re-registers after 1 s; otherwise kills the process.

m38420d is invoked from the GET path (C21273a:198), the GZIP POST path (:259) and :357. Consequences in M-01.

3.8 Other observationsh5
  • installLocation="auto" - SD-card install permitted.
  • android:profileable android:shell="true" - simpleperf / ART profiling by shell only. Normal.
  • <uses-library android:name="org.apache.http.legacy" required="false"/> plus androidx.window.extensions / androidx.window.sidecar - legacy HTTP client available.
  • android:largeHeap="true", android:hardwareAccelerated="true", android:extractNativeLibs="false", supportsRtl="false".
  • Notification channels created: caller_id_status, caller_id_v2.
  • MyApplication installs itself as the process-wide UncaughtExceptionHandler (MyApplication.java:469-470) and forwards to Firebase Crashlytics.
  • assets/audience_network.dex (5 MB) is a dynamically loaded Meta Audience Network DEX, separate from the six main DEX files.

4. Ranked findingsh4

Severity is the realistic worst-case impact for a device that already has an untrusted app installed, unless stated otherwise.

CRITICALh5

None.

HIGHh5

H-01 - Cleartext remote config can control the URL loaded into a JS-bridged WebView; the bridge reads arbitrary SharedPreferences

  • Severity: High
  • Evidence:
    • res/xml/network_security_config.xml - base-config cleartextTrafficPermitted="true", no pinning; AndroidManifest.xml:272 usesCleartextTraffic="true".
    • p236d5/AbstractC15804d.java:300 - map.put("eyecon_remote_config_url", "http://cloudfront-cdn-all.eyecon-app.com/configs/ab-test.txt") (plain HTTP), fetched by p005a4/RunnableC0031c.java:463 via C21273a.m38423g. Response is parsed as JSON (C18416a.m34586w(str).m14606i()) with only an HTTP status-code check (:465) - no signature, MAC, nonce or version binding.
    • Billing/Store/StoreActivity.java:213 reads store_url from that config; :218-226 enables JS, sets setAcceptThirdPartyCookies(true) and attaches the Eyecon and EyeconStore bridges. C5846m (the WebViewClient) does not override shouldOverrideUrlLoading, so the WebView will navigate anywhere while keeping the bridges.
    • p262e7/C16132d.getStringSP(String, String) -> p358j5/AbstractC17503r.m33607k -> MyApplication.m10348j() -> the app’s default SharedPreferences. Arbitrary key read from any page loaded in that WebView, plus getAdvertisingId(), getPublicId(), getCid(), getMyName(), reportEvent(), and EyeconStore.purchase(product_id, ...).
  • Impact: a network MITM (or anyone who can poison the CDN path / DNS) rewrites one JSON blob and obtains an arbitrary-URL WebView with a JS bridge that can read the app’s private preferences, exfiltrate stable device identifiers, and drive the purchase flow. Requires a network position (or CDN compromise), which is why this is High and not Critical.
  • Mitigating: WebviewPaywallActivity.java:282-287 shows the team already knows how to do an origin allow-list - it is just not applied to the store WebView.

H-02 - Caller-ID UI can be triggered by any app with an arbitrary phone number (spoofed incoming-call screen)

  • Severity: High (caller-ID spoofing / vishing primitive inside a trusted, brand-recognised UI; also drives real side effects - CallService start, caller-ID DB writes, reporting calls, and the WhatsApp-call path)
  • Evidence:
    • AndroidManifest.xml:391-401 - com.eyecon.global.Call.MyPhoneReceiver is android:exported="true" with no android:permission, priority 2147483647, and listens for com.eyecon.global.action.INTENT_ACTION_START_CALL_WHATSAPP and com.eyecon.global.action.CALL_SCREENING_NEW_CALL - two unprotected, app-defined actions alongside the protected PHONE_STATE / NEW_OUTGOING_CALL.
    • com/eyecon/global/Call/MyPhoneReceiver.java:48-53 accepts any of the four and forwards the raw Bundle to RunnableC17334k case 7.
    • im/RunnableC17334k.java:868-924 - action is coerced to PHONE_STATE, state + incoming_number are read straight from the broadcast extras, incoming_number is normalised and CallService is started.
    • The identical payload shape is what the trusted IdPlus/NotificationReader/NotificationReaderService.m10223a uses (NotificationReaderService.java:38-46), so nothing distinguishes an attacker broadcast from a real one.
  • Attack: adb shell am broadcast -a com.eyecon.global.action.INTENT_ACTION_START_CALL_WHATSAPP --es state RINGING --es incoming_number "+14155550123" -n com.eyecon.global/.Call.MyPhoneReceiver - no permission needed. Also an NPE (DoS) if state is omitted while incoming_number is present (string3.equals(...) at line 906).
  • Mitigating: a local attacker app could in principle draw its own activity, but it could not present the screen with MICO’s identity, icons and styling, nor reuse MICO’s backend enrichment of the number.
MEDIUMh5

M-01 - Server-header-triggered local data wipe, reachable via the cleartext config channel

  • Severity: Medium
  • Evidence: p620w6/C21273a.java:112-126 (see 3.7) + p516r5/RunnableC19874r.java:99-118 (wipes history, contacts, social, fresh_pics, block_list, recorded_notes).
  • The gate is a response header (E-Mark) on a 401. The URL test is a substring match (str.contains("eyecon-app.com")), and the HTTP client sets setInstanceFollowRedirects(true) (C21273a:191). The remote-config fetch at http://cloudfront-cdn-all.eyecon-app.com/configs/ab-test.txt uses exactly this code path.
  • Impact: a network MITM who can answer that cleartext request with HTTP/1.1 401 + E-Mark: 1 forces a full local data wipe and a forced re-registration. No credential or PII is disclosed; the blast radius is the app’s own SQLite state, which is re-fetchable.
  • The one-shot latch (SP_unAuthorizedUser_isCleared) limits repeat abuse per install.

M-02 - Exported PhoneNumberValidationActivity.onNewIntent trusts arbitrary caller extras

  • Severity: Medium
  • Evidence: AndroidManifest.xml:580-593 (exported="true", launchMode="singleTask"). Registration/PhoneNumberValidationActivity.java:1273-1282 reads state, phoneNumber and smscode from any sender and calls m10473L0(phone, code) (:429-459), which (a) fills and locks the four code fields with attacker-chosen digits, (b) sets sendSmsEnded = true so the genuine SMS flow is suppressed, and (c) submits the code to the backend via C21280h.m38435h(..., EnumC19662h1.SO_SMS, true).
  • Preconditions: the activity must be on the code-entry step (ViewFlipper.getDisplayedChild() == 1); singleTask means an external startActivity reuses the existing task and routes through onNewIntent.
  • Also a crash primitive: a smscode shorter than 4 characters (or absent) hits str2.charAt(0..3) unguarded -> uncaught NPE / StringIndexOutOfBoundsException on the UI thread.
  • The phone number itself comes from SharedPreferences (tempAuthenticatedCli), not the intent, so this is an integrity/DoS issue rather than account takeover.

M-03 - eyecon://browser?url= is an unvalidated intent-redirection sink

  • Severity: Medium

  • Evidence: MainScreen/MainActivity.java:833-842, reachable from the BROWSABLE eyecon scheme filter (AndroidManifest.xml:445-450):

    if (host.equals("browser")) {
    Intent i5 = new Intent(ACTION_VIEW, Uri.parse(URLDecoder.decode(data2.getQueryParameter("url"), "UTF-8")));
    i5.putExtras(bundleM32028w); // the entire incoming extras bundle is forwarded
    startActivity(i5);
    }

    No scheme allow-list, and the incoming extras are copied verbatim. Classic intent-redirection: an intent://...#Intent;...;end payload is passed straight to the system resolver, so MICO acts as an open launcher for arbitrary exported components on the device with attacker-chosen extras.

  • Impact is device-dependent (depends on which target app has a dangerous exported component), which is why this is Medium rather than High.

M-04 - Exported DummyActivity accepts arbitrary action + extras via explicit intent and renders attacker content in-app

  • Severity: Medium
  • Evidence: AndroidManifest.xml:661-675 (exported="true", taskAffinity=".eyecon", excludeFromRecents="true"). Others/Activities/DummyActivity.java:87-194 switches on getIntent().getAction() and, for action EYECON.INTENT_ACTION_SHOW_NOTE, parses the caller-supplied note extra as JSON (C18416a.m34586w(string6).m14606i(), line 150) and displays it in the app’s dialog with caller-supplied cli / name / source; EYECON.INTENT_ACTION_THANK_YOU_DIALOG calls AbstractC21601b.m38812e(...) (an API call) with caller-supplied cis / cli / src; EYECON.INTENT_ACTION_SHOW_WARNING_FACEBOOK shows a Facebook warning for a caller-supplied fid; EYECON.INTENT_ACTION_UNLOCK_KEYGUARD* reaches KeyguardManager.requestDismissKeyguard.
  • The custom-scheme path itself is tightly restricted (scheme must be eyecon_request, and only host=caller_id_role does anything), but exported="true" means a local app can bypass the filter entirely with an explicit intent.
  • requestDismissKeyguard on a secure keyguard still requires credential entry, so this is UI spoofing / unwanted navigation, not a lock bypass.

M-05 - Call recordings and voice notes written to world-readable shared storage

  • Severity: Medium
  • Evidence: Others/MyApplication.java:467 - f12172q = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOCUMENTS), "Eyecon"). p260e5/AbstractC16098a.java:97 - new File(MyApplication.f12172q, "RecordedNotes") / "RecordedCalls"; p008a7/RunnableC0070p0.java:123-124; p420m7/C18517d.java:47 - new File(MyApplication.f12172q, "ContactsPhotos"). AudioRecording/RecordingsFragment.java:152 also uses Environment.getExternalStorageDirectory() as the recordings root.
  • On API 23-28 (and API 29 with requestLegacyExternalStorage="true" + WRITE_EXTERNAL_STORAGE) Documents/Eyecon/ is readable and writable by every app. Sensitive call audio and personal notes in shared storage. Blocked by scoped storage from API 30, so this is an old-device exposure.

M-06 - Recording of the live call audio stream (Talk-To-Me)

  • Severity: Medium (privacy, not a code-execution bug)
  • Evidence: p599v7/RunnableC20908f.java:507-512 - AmrEncoder.init(0); new AudioRecord(1 /* MediaRecorder.AudioSource.VOICE_CALL */, 8000, 16, 2, 16000); audioRecord.startRecording();, with Process.setThreadPriority(-19). Streamed AMR frames are POSTed to voice2text.eyecon-app.com (see BACKEND-FINDINGS.md). p633wl/RunnableC21441d is the companion player loop.
  • Requires RECORD_AUDIO, an opt-in product, and - on modern Android - the default-dialer role. Rated Medium because the capability (third-party capture of a telephony audio source) is exceptionally sensitive and the recording path runs at elevated thread priority.
LOW / INFORMATIONALh5

L-01 - Logging can be remotely re-enabled in release builds. C18416a.f46357c defaults false and is set from the remote-config flag print_logs_in_release (p008a7/RunnableC0074r0.java:264, p672y5/C21867v.java:862). When on, m34588y writes log lines that include phone numbers, SMS-derived data, referral strings, product ids and full request URLs, to getFilesDir()/eye_log.txt and logcat. Not exploitable by a third party without READ_LOGS, but a server-side misconfiguration re-enables verbose PII logging in production.

L-02 - Ungated System.out.println of the install-referrer bundle. Registration/InstallReferrerReceiver.java:38,42,62,89-118 prints every key/value of the referrer intent and the raw referrer string to logcat unconditionally. Attribution data only (no credentials), but an unconditional write to a world-readable-on-old-Android buffer.

L-03 - Spoofable SMS send-result broadcasts. RegistrationActivity:837-838 and PhoneNumberValidationActivity:541-542 register ACTION_SMS_SENT_EYECON / ACTION_SMS_DELIVERED_EYECON with RECEIVER_EXPORTED (flag 2) although both are app-private strings. Any app can forge a “verification SMS sent” result.

L-04 - Spoofable Bluetooth-device-picked broadcast. DefaultDialer/CallActivity:2483 registers android.bluetooth.devicepicker.action.DEVICE_SELECTED with RECEIVER_EXPORTED. Unprotected action; a local app can inject a fake selected device into the dialer’s BT picker.

L-05 - Ad-SDK activities exported with no guard. tech.crackle.cracklertbsdk.vast.ShowFullScreenVideo, tech.crackle.cracklertbsdk.ShowFullScreenStatic, tech.crackle.core_sdk.ads.CrackleFullScreenActivity, com.amazon.aps.ads.activity.ApsInterstitialActivity, com.amazon.device.ads.DTBInterstitialActivity are all android:exported="true" with no filter and no permission. Any installed app can startActivity them explicitly to force an ad impression (revenue theft / nuisance). Third-party-SDK defect, not app code.

L-06 - Over-broad FileProvider path roots. res/xml/file_paths.xml maps all five roots with path="." and the provider has grantUriPermissions="true". Only one call site was found and it uses an internal files/ temp file, so nothing is currently exposed; latent hardening problem.

L-07 - com.facebook.FacebookContentProvider exported with no permission. Authority com.facebook.app.FacebookContentProvider1716660955257637; any app can query the FB SDK’s installreferrer / bridge tables. Standard FB SDK behaviour (needed for multi-process), low impact.

L-08 - Dead / vestigial declarations. res/xml/image_share_filepaths.xml (orphan), res/xml/splits0.xml (Play splits for a standalone APK - see com.android.stamp.type=STAMP_TYPE_STANDALONE_APK), the <permission android:name="android.permission.INTERACT_ACROSS_USERS" android:protectionLevel="signature"/> re-declaration of a framework permission (a no-op that grants nothing), the <uses-permission android:name="BILL"/> request for an undeclared permission, and androidx.startup.InitializationProvider meta-data for classes not in the APK (e.g. androidx.p725lifecycle.ProcessLifecycleInitializer). None exploitable.

L-09 - Privacy Sandbox fully opened + RTB on. res/xml/ad_services_config.xml sets allowAllToAccess="true" for attribution, custom audiences and topics; com.google.android.gms.ads.flag.rtb_enabled=true; ACCESS_ADSERVICES_* x4 and AD_ID requested. Maximum ad-tracking posture - a policy/privacy statement, not a bug.

L-10 - Unrestricted Google Maps / Crackle API keys. com.google.android.geo.API_KEY = AIzaSyD1LLixZGTwP6i3Ts8AzX4jxdb9zuaikKo in the manifest, plus a second Google key inside crackle_key. These must be restricted server-side (package + SHA-1 + API); nothing in the artifact proves they are.

L-11 - Empty-intent PendingIntents used as notification content intents. CallService.java:156, ForegroundLauncherService.java:43, TokiService.java:53 - PendingIntent.getActivity(ctx, 777, new Intent(), ...). Tapping those notifications does nothing. Cosmetic.

Checked and explicitly NOT a findingh5
  • Sms.MySmsReceiver and DefaultDialer.assistant.MessageAssistantBroadcastReceiver - both guarded by android.permission.BROADCAST_SMS (system-only sender) and neither calls abortBroadcast(). No SMS interception.
  • NotificationReaderService, CallStateService, EyeconCallScreeningService, JobsService, SmsJobService, ShareMediaJob, SystemJobService, DiagnosticsReceiver, ProfileInstallReceiver, FirebaseInstanceIdReceiver, RevocationBoundService - all exported but protected by signature/system permissions. Standard Android patterns, not exploitable.
  • PhoneAccount.SyncService / EyeconAccountAuthService exported with no permission - only the system SyncManager / AccountManager can bind them. Standard.
  • com.mbridge...NetWorkChangeReceiver exported for CONNECTIVITY_CHANGE - protected broadcast, system-only sender.
  • SCREEN_OFF / CLOSE_SYSTEM_DIALOGS registered with RECEIVER_EXPORTED - both protected broadcasts.
  • AndroidManifest.xml:224-226 - declaring android.permission.INTERACT_ACROSS_USERS does not let the app use it; platform permissions cannot be redefined by third-party apps. No escalation.
  • debug-overrides with <certificates src="user"/> - correct Android default, inert in a release build.
  • onReceivedSslError is not overridden anywhere in app code, so the platform default (cancel) applies; there is no handler.proceed().
  • android:allowBackup="false" + android:fullBackupContent="false" - adb / cloud / D2D backup disabled.
  • No FLAG_MUTABLE PendingIntent, no QUERY_ALL_PACKAGES, no root/emulator detection in app code (the Genymotion/BlueStacks queries come from the Facebook SDK).
  • <queries> does not include QUERY_ALL_PACKAGES; all package-visibility is declared per-package.

5. Permissions, grouped (78 entries)h4

Dangerous / runtime (personal data)h5
PermissionmaxSdkNote
ACCESS_FINE_LOCATION-no app-code consumer found
ACCESS_COARSE_LOCATION-no app-code consumer found
CAMERA-PhotoPickerActivity, CallActivity
READ_CONTACTS-DBContacts, AccountWithDataSet
WRITE_CONTACTS-SyncAdapter writes into com.android.contacts
READ_PHONE_STATE-IMEI / phone number / SIM count fingerprint
READ_PHONE_NUMBERS-
CALL_PHONE-
ANSWER_PHONE_CALLS-CallStateService / CallActivity
PROCESS_OUTGOING_CALLS-deprecated; MyPhoneReceiver
RECEIVE_SMS-
SEND_SMS-registration SMS
READ_SMS-in-app SMS window
READ_CALL_LOG-DBContacts, EyeconAnalyticsWorker
WRITE_CALL_LOG-
GET_ACCOUNTS-deprecated; no consumer found
AUTHENTICATE_ACCOUNTS22inert (minSdk 23)
RECORD_AUDIO-recordings + Talk-To-Me call capture
READ_EXTERNAL_STORAGE-deprecated on API 33
WRITE_EXTERNAL_STORAGE-deprecated on API 33
POST_NOTIFICATIONS-runtime on API 33+
BLUETOOTH_CONNECT-runtime on API 31+
com.google.android.gms.permission.ACTIVITY_RECOGNITION-no app-code consumer found
USE_FULL_SCREEN_INTENT-incoming-call full-screen intent
Normal / specialh5

INTERNET - ACCESS_NETWORK_STATE - ACCESS_WIFI_STATE - CHANGE_NETWORK_STATE - WAKE_LOCK - VIBRATE - MODIFY_AUDIO_SETTINGS - RECEIVE_BOOT_COMPLETED - REORDER_TASKS - KILL_BACKGROUND_PROCESSES - REQUEST_IGNORE_BATTERY_OPTIMIZATIONS - MANAGE_OWN_CALLS (+ uses-permission-sdk-23 duplicate) - ACCESS_NOTIFICATION_POLICY - BLUETOOTH (deprecated) - BLUETOOTH_ADMIN (deprecated) - SYSTEM_ALERT_WINDOW (appop) - WRITE_SETTINGS (appop) - DISABLE_KEYGUARD (deprecated) - FOREGROUND_SERVICE - FOREGROUND_SERVICE_PHONE_CALL - FOREGROUND_SERVICE_SPECIAL_USE - com.android.launcher.permission.INSTALL_SHORTCUT

Billing / install-referrer / ads / pushh5

com.android.vending.BILLING - com.android.vending.CHECK_LICENSE - BIND_GET_INSTALL_REFERRER_SERVICE - com.google.android.finsky.permission.BIND_GET_INSTALL_REFERRER_SERVICE - com.google.android.gms.permission.AD_ID - ACCESS_ADSERVICES_ATTRIBUTION - ACCESS_ADSERVICES_AD_ID - ACCESS_ADSERVICES_CUSTOM_AUDIENCE - ACCESS_ADSERVICES_TOPICS - com.google.android.c2dm.permission.RECEIVE - com.eyecon.global.permission.C2D_MESSAGE (self, signature)

OEM / launcher badges (16)h5

android.permission.READ_APP_BADGE - com.sec.android.provider.badge.permission.READ - com.sec.android.provider.badge.permission.WRITE - com.sonyericsson.home.permission.BROADCAST_BADGE - com.sonymobile.home.permission.PROVIDER_INSERT_BADGE - com.htc.launcher.permission.READ_SETTINGS - com.htc.launcher.permission.UPDATE_SHORTCUT - com.anddoes.launcher.permission.UPDATE_COUNT - com.majeur.launcher.permission.UPDATE_BADGE - com.huawei.android.launcher.permission.CHANGE_BADGE - com.huawei.android.launcher.permission.READ_SETTINGS - com.huawei.android.launcher.permission.WRITE_SETTINGS - com.oppo.launcher.permission.READ_SETTINGS - com.oppo.launcher.permission.WRITE_SETTINGS - me.everything.badger.permission.BADGE_COUNT_READ - me.everything.badger.permission.BADGE_COUNT_WRITE

OEM vendorh5

oppo.permission.OPPO_COMPONENT_SAFE - com.huawei.permission.external_app_settings.USE_COMPONENT

Selfh5

com.eyecon.global.DYNAMIC_RECEIVER_NOT_EXPORTED_PERMISSION

Requested but undeclaredh5

BILL - requested but never declared anywhere in the manifest; a no-op.

<uses-feature> (all required="false")h5
Featurerequired
android.hardware.telephonyfalse
android.hardware.camerafalse
android.hardware.camera.frontfalse
<uses-library>h5

org.apache.http.legacy (false) - androidx.window.extensions (false) - androidx.window.sidecar (false)


6. Confidence / unknownsh4

High confidence (directly read in source)

  • Every component, filter, permission, permission-guard, exported flag, process, launchMode, taskAffinity, provider authority and meta-data value in sections 1 and 5 - all extracted programmatically from AndroidManifest.xml via XML parsing, not summarised from memory.
  • network_security_config.xml, file_paths.xml, ad_services_config.xml, eyecon_account_sync_adapter.xml, eyecon_contacts_account_auth.xml, the MyApplication process gate, the JS-bridge classes C16132d / C17757i, the 401 / E-Mark chain, the DB wipe table list, RemoteConfigDebugTestReceiver being a no-op, MySmsReceiver not aborting the broadcast, m343L1 package-scoping, the print_logs_in_release gating, and the AudioRecord(VOICE_CALL) call.
  • BACKEND-FINDINGS.md claims about the e-auth scheme, the endpoint list and the fingerprint fields were re-verified where they intersect this document’s scope (E-Mark, C21273a.m38420d, C21280h).

Medium confidence

  • PendingIntent immutability. AbstractC0046d0.m185D is the single choke point for every PendingIntent in app code, and no caller passes FLAG_MUTABLE. But JADX printed the ORed constant as AccessibilityEventCompat.TYPE_VIEW_TARGETED_BY_SCROLL (0x40000000) rather than PendingIntent.FLAG_IMMUTABLE (0x04000000). The immutability conclusion is inferred: FLAG_UPDATE_CURRENT | FLAG_ONE_SHOT with neither mutability flag would throw IllegalArgumentException on API 31+, and this is a working targetSdk-35 app. A DEX-level read of the constant pool would confirm it.
  • eyecon://browser reachability. MainActivity’s intent router is heavily decompiled and JADX inverted several conditions around MainActivity.java:694-712. The host-dispatch chain (showdialog … browser … menifa … url … tel) is unambiguous, and every branch that matters is guarded only by host.equals(...). I am confident the branch is reachable from an eyecon://browser?url=... intent; I could not prove it by execution.
  • JADX constant substitution. Several integer literals were resolved to unrelated framework constants (e.g. RunnableC19874r:104 renders the "social" table name as NotificationCompat.CATEGORY_SOCIAL; C5846m.f11594d = 4 is a field-initialiser artefact of a constructor parameter). Table names and enum discriminators in 3.7 should be treated as approximate where quoted from JADX.

Low confidence / unresolved

  1. store_url reachability at runtime. The shipped default is the empty string (AbstractC15804d.java:295); the actual value comes from the remote config, which was not fetched. If the remote store_url is empty, StoreActivity would throw on Uri.parse("").getHost().replace(...) (StoreActivity.java:215) before the WebView is ever configured. The bridge exposure in H-01 is real iff store_url is populated, which the codebase strongly implies (theme/ringtone purchase flows, premium_products_ids_list, EyeconStore.getSelectedItems).
  2. E-Mark semantics. The server-side meaning is unknown (see BACKEND-FINDINGS.md section 9). M-01 is rated on the client-side trigger conditions, which are fully readable; the attacker-chosen value the server expects may be validated in a way not visible here.
  3. FragmentTransaction.TRANSIT_FRAGMENT_MATCH_ACTIVITY_OPEN and similar appear as enum-constant substitutes for integer IDs in EnumC19861h0 (e.g. SMS(1048576, ...), CALL(4101, ...)). The values look right but were not cross-checked against the DEX.
  4. Third-party SDK internals. Ad/analytics/attribution SDK behaviour (InMobi, Pangle, Mbridge, Fyber, Crackle, Vungle, Singular, Amplitude, DataTransport) was not audited beyond its manifest footprint. Some declared permissions may be used only by those SDKs.
  5. No dynamic analysis. Nothing here observes execution. No traffic was captured, no intent was actually broadcast, no WebView was loaded. All conclusions are static.
  6. Which uses-permission entries are actually exercised was determined by grepping com.eyecon for permission strings. Permissions consumed only inside SDK code (e.g. ACCESS_FINE_LOCATION for ad targeting) are marked “no app-code consumer found”, not “unused”.
  7. NotificationListenerService coverage. The full set of packages whose notifications are read could not be enumerated exhaustively from the static path (packageName.contains("telegram"), contains("com.whatsapp")); a .contains() test could match more packages than intended.

Generated from the JADX export at C:\Users\btc_e\Downloads\mico-analysis\src. Sources read-only; this document is the only file written.

Part III - Backendh2

Fifty endpoints across 36 hosts, all reachable, all reconstructable. The credential scheme is called e-auth v1 and it is fully reversible: a shared secret, an m38421e checksum, and a timestamp. With those three, a request can be forged by hand.

The single highest-value finding in this part is the clientId chain, which is covered in Part IV because it straddles backend and storage. In short: the value is read from com.facebook.appevents.AbstractC9039h.m10662s(), written by two sites in q7, persisted to contactdb.shared_preference_v2 under key clientId, and issued by join.jsp. It cannot be minted offline. That last constraint is what turns an otherwise ordinary identifier into the app’s trust anchor.

A second string is worth pulling out of this part rather than leaving it in a table. The Facebook application ID 1716660955257637 and its client token afa3f38c55f24c73b177afc962969b15 are both present in strings.xml in plaintext, along with the combined fct value. Verified independently, not inferred from the SDK.

MICO (com.eyecon.global) - Backend API Referenceh3

Target: com.eyecon.global (Eyecon / MICO caller-ID & spam blocker), version 4.2026.04.23.1145, versionCode 749. Source tree analysed: C:\Users\btc_e\Downloads\mico-analysis\src\sources (+ ..\resources). Method: static reading of the JADX decompilation only. No traffic was generated and no server was contacted.

How to read this documenth5
  • Observation = literally visible in the decompiled code (a literal, a call site, a constant).
  • Inference = my reasoned conclusion drawn from those observations.
  • Each endpoint entry gives: exact URL, HTTP method, every request parameter and where its value comes from, the issuing class/method, whether it is fire-and-forget or has a callback, and how the response is consumed.
  • ProGuard renaming: p008a7 = original a7, p620w6 = w6, p284f7 = f7, p494q7 = q7, p647x6 = x6, p420m7 = m7, p673y6 = y6, p466oq = oq, p315h7 = h7. com.facebook.appevents.* and androidx.fragment.app.RunnableC2598c are application code deliberately packed into third-party-looking package names. ir.AbstractC17448b is really kotlin.jvm.internal.Intrinsics. p420m7.C18533l / p466oq.C19231e0 are OkHttp. bm.C3645j1 is OkHttp Request.Builder.

0. Executive summaryh4

ItemValue
Primary API base URLhttps://api.eyecon-app.com/app/ (const p008a7.AbstractC0039a.f168L1, hard-coded)
HTTP corep620w6.C21273a (orig. w6.a, TAG NetworkHelper)
Server facadep620w6.C21280h (orig. w6.h) - caches base URL + uploadpic.jsp + join.jsp
Queued-request storeSQLite table table_api_requests (p647x6.*), drained by com.eyecon.global.Network.ApiRequests.ApiRequestWorker
Auth scheme4 headers e-auth-v, e-auth, e-auth-c, e-auth-k; applied only when the URL starts with the API base or contains eyecon-app.com
Static keyPgdtSBeR0MumR7fO (C21273a.rc_key), remotely overridable via Remote Config key circus_key using reflection
Distinct Eyecon API paths50 (43 on api.eyecon-app.com/app/, 7 on voice2text.eyecon-app.com)
Distinct non-Eyecon app-initiated destinations12
Distinct hostnames36
Cleartext functional transmissions3 (cloudfront-cdn-all.eyecon-app.com config fetch, numberlookup.instabridge.com SMS assistant, join.eyecon-app.com Singular branded domain)
Cleartext allowed globallyYes - android:usesCleartextTraffic="true" and res/xml/network_security_config.xml <base-config cleartextTrafficPermitted="true">; debug builds additionally trust user CAs
Top findings (severity ordered)h5
  1. CRITICAL - hard-coded, remotely overridable auth secret. rc_key = "PgdtSBeR0MumR7fO" ships in the APK and is the compiled-in default of Remote Config key circus_key. p032b5.AbstractC3464e.m3668s() reads the remote value and reflectively overwrites the private static final field C21273a.rc_key. Control of the Remote Config project therefore changes the e-auth-k credential presented by every installed client, with no app update.
  2. HIGH - SMS transcript, contact name and phone number over cleartext HTTP. POST http://numberlookup.instabridge.com/respond-sms (Retrofit/Moshi: p636wo.C21474f, p646x5.InterfaceC21626a) with {contactName, number, messages:[{role,message}], locale, messagesInSession, userName}.
  3. HIGH - full call-log exfiltration endpoint. EyeconAnalyticsWorker zips data.json ({cli, fake_cli, call_logs:[{phone, date, duration_in_seconds, type, name}]}) and multipart-POSTs it to the Remote-Config-controlled analytics_worker_url. Off by default ("disabled_by_remote") but one remote flip away; it also carries a from_date rewind that can re-upload history.
  4. MEDIUM - device fingerprint including IMEI and Wi-Fi MAC address sent on join.jsp, cli_auth/gettransport, addcontacts.jsp and others (section 7).
  5. MEDIUM - contact names sent to Google Translate (GET translate.google.com/translate_a/single?...&q=MY NAME IS <name>).
  6. MEDIUM - Eyecon’s own config/A-B JSON is fetched over cleartext HTTP (http://cloudfront-cdn-all.eyecon-app.com/configs/ab-test.txt), and that JSON populates the very map that holds circus_key and every endpoint URL.
  7. MEDIUM - all query parameters (CLIs, IMEI, MAC, contact lists, names) travel in the URL, including for POSTs issued through the main helper (empty body, params in query string).
  8. LOW/MEDIUM - numerous hard-coded third-party credentials, including a usable PaperQuotes bearer token and a LocationIQ key (section 8).

1. Host inventoryh4

HostTLSPurposeSource
api.eyecon-app.comhttpsPrimary API (/app/*.jsp, /app/<rest>) + caller-ID photo endpoint /app/picAbstractC0039a.f168L1
voice2text.eyecon-app.comhttps”Talk-To-Me” (TTM) / voice-to-text servicep620w6.RunnableC21274b, RunnableC21276d, p572u4.RunnableC20597o, as.RunnableC3316o
ptt.eyecon-app.complain UDP, no TLSToki / push-to-talk signalling and media (C3349u2 / C21952k DatagramSocket)p703z7.C22122v0:419, p633wl.C21450m
eyecon-app.comhttpsViral invite landing pages https://eyecon-app.com/vrs/evid-<id>p358j5.C17485b:79, C17487c:64
www.eyecon-app.comhttpsMarketing site / ToS / privacymany UI classes
faq.eyecon-app.comhttpsPer-manufacturer troubleshooting WebViewsp008a7.AbstractC0042b0.m181d0
apps.eyecon-app.comhttpscall-contest/a/report-calls (remote-config value, no call site found)AbstractC15804d:220
cards.eyecon-app.comhttps”Dynamic area”/cards host (remote config dynamic_area_url; no direct call site found)AbstractC15804d:122
cloudfront-cdn-all.eyecon-app.comhttp (cleartext)Eyecon A/B + remote-config JSON (/configs/ab-test.txt)AbstractC15804d:300
join.eyecon-app.comhttp (cleartext)Singular SDK branded domain / attribution deep-link domainp671y4.C21845b:72
api.paperquotes.comhttpsAfter-call quotes (/apiv1/quotes/)p284f7.C16476j0
us1.locationiq.com / eu1.locationiq.comhttpsReverse geocode of caller areap379k5.RunnableC17750b:583
numberlookup.instabridge.comhttp (cleartext)AI SMS-assistant backend (/respond-sms)p636wo.C21474f:295, p646x5.InterfaceC21626a
workers.degoo.comhttpsIP -> lat/long feature gate (/location)p008a7.RunnableC0064m0:112
translate.google.comhttpsContact-name normalisationp344ic.RunnableC17219d1:590
www.google.com, google.com, google.co.inhttpsConnectivity test; reverse-image-search WebView (search?hl=en&udm=2&q=)AbstractC15804d:120, p236d5.AbstractC0060k0, p420m7.C18533l
www.facebook.com, m.facebook.com, graph.facebook.comhttpsProfile/photo scraping, Facebook media downloadp344ic, p303g5, p541s5, PhotosTrackerWorker
www.instagram.com, instagram.com, www.tiktok.comhttpsProfile/media scraping, TikTok oEmbedp599v7.C20917o, p621w7.C21295o:216
www.linkedin.com, mobile.twitter.com, new.vk.comhttpsShare-sheet text onlyUI
the-news-desk.com, s3.amazonaws.com (embed.footylight.com)httpsAd-tag <script> injected into WebViewsAbstractC15804d:195-197
hb.crackle.co.inhttpsCrackle / Mobitech mediation (/user-module/v4/getAppIdAdUnitsMap)p614w0.AbstractC21232a:370
docs.google.comhttpsWelcome-back survey form, ToS / privacyAbstractC15804d:208, UI
api2.amplitude.com, api.eu.amplitude.com, static.amplitude.comhttpsAmplitude SDKp284f7.C16440x:639
firebase-settings.crashlytics.com, firebaseinappmessaging.googleapis.com, firebaseremoteconfigrealtime.googleapis.com, www.googleapis.com, console.firebase.google.comhttpsFirebase Crashlytics / FCM / Remote ConfigFirebase SDK
<analytics_worker_url>, <server_report_url>, <store_download_url>remote-config controlledDynamic upload/download endpoints (section 5)AbstractC15804d

Third-party ad-network hosts (Google Ads/AdMob, Meta Audience Network, InMobi, Mintegral, Vungle, Pangle/ByteDance, Fyber FairBid, Inneractive, AppLovin, Adjust, Singular, AdColony, Digital Turbine, Amazon Ads) are present but constitute vendor SDK traffic and are out of scope except where the app hard-codes identifiers (section 8).


2. The HTTP core - p620w6.C21273a (orig. w6.a, TAG NetworkHelper)h4

2.1 Request primitivesh5
Method (obfuscated)TransportSimplified signature
m38417a(C19231e0, boolean)OkHttp(okhttp3.Request, addAuth) -> okhttp3.Response, shared singleton client
m38423g(...)HttpURLConnection(ArrayList<C21872a> params, String url, String method, int[] codeOut, boolean jsonAccept, String endPointLabel, HashMap headerValues, boolean addAutoHeaders, boolean gzip, HashMap headerResult, String rid) -> String
m38422f(...)HttpURLConnectionthin wrapper over m38423g with rid = ""
m38424h(ArrayList, String url, int[] codeOut, String label, String rid)HttpURLConnection + GZIPOutputStreamgzip POST
m38425i(int[] codeOut, boolean addAuth, String body, String dataName, String url, String label, String contentType, String method, String rid)HttpURLConnectionraw-body POST/GET
p420m7.C18540p.m34732a / m34736eHttpURLConnection + BitmapFactoryphoto/image fetch with manual redirect handling
2.2 URL / query construction - p410lo.C18416a.m34580q(ArrayList)h5

Observation. Query strings are produced with OkHttp’s HttpUrl.Builder:

HttpUrl.Builder b = new HttpUrl.Builder(); // p420m7.C18533l
b.scheme("https"); b.host("example.com");
for (C21872a p : params) b.addQueryParameter(p.name, p.value);
return b.build().encodedQuery();

Consequences (inference, high confidence):

  • Parameter names and values are percent-encoded with OkHttp’s canonical query-component set.
  • Every parameter of every request issued through m38423g ends up in the URL query string, regardless of HTTP method; such POSTs carry an empty body.
  • A null value is emitted as a bare name with no =.
  • Parameter order equals insertion order into the ArrayList.
2.3 Headers set by m38423gh5

Observation (C21273a.m38423g, lines 171-198):

HeaderValueCondition
User-AgentMozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36always
acceptapplication/json when jsonAccept == true, else */*always
accept-charsetUTF-8always
content-typeapplication/x-www-form-urlencoded; charset=utf-8always, even for GET
Accept-Encodinggziponly when gzip == true
ridper-request UUIDonly when the rid argument is non-empty (section 2.6)
e-auth-v, e-auth, e-auth-c, e-auth-ksee section 3only when addAutoHeaders == true and the URL starts with https://api.eyecon-app.com/app/ or contains eyecon-app.com
caller-suppliedfrom the headerValues HashMapwhen non-null

HttpURLConnection.setInstanceFollowRedirects(true) is also set here.

2.4 Timeoutsh5
PrimitiveConnectReadWriteCall
m38423g / m38422f60 000 ms60 000 msn/an/a
m38424h (gzip POST)60 000 ms (set twice)60 000 msn/an/a
m38425i (raw body)60 000 ms60 000 msn/an/a
m38417a (OkHttp, C19225b0 builder defaults)10 000 ms10 000 ms10 000 ms60 000 ms
C21280h.m38441n (uploadpic.jsp)none set (platform default)nonenonenone
EyeconAnalyticsWorker (OkHttp)client defaultsclient defaultsn/an/a

retryOnConnectionFailure is left at the OkHttp default (true).

2.5 Response reading, gzip, redirectsh5
  • m38426j(conn, gzip, headerResultMap) reads the body into a 1 KiB buffer that grows by 1024 bytes per fill and returns a UTF-8 String.
  • Gzip is unwrapped only when gzip == true and the Content-Encoding response header equals "gzip".
  • All response headers are copied into the caller’s HashMap headerResult through m38418b (first value per name). This is how join.jsp reads pi, sd, is_new, and how getupdatedcontactsinfo.jsp reads sms.
  • The photo loader (C18540p.m34738g) sets setInstanceFollowRedirects(false) and follows 3xx manually: maximum 5 hops, scheme must be http/https, otherwise SecurityException("illegal URL redirect"). It special-cases Eyecon-hosted photos redirecting off eyecon-app.com/app/ and records the redirect so the caller can fall back.
2.6 rid (request id)h5

Observation. p647x6.AbstractC21650c generates UUID.randomUUID().toString() per queued request and passes it as the rid header. Direct calls (m38422f, or m38423g with "") send no rid. The queued serialisation stores the same value as uuid.


3. Authentication / header logich4

3.1 The four headersh5

Observation - C21273a.m38427k(HttpURLConnection) and C21273a.m38417a(req, true):

String cid = AbstractC9039h.m10662s(); // SharedPreferences "clientId"
if (isEmpty(cid)) throw new RuntimeException("Error missing CID");
conn.setRequestProperty("e-auth-v", "e1");
conn.setRequestProperty("e-auth", cid);
conn.setRequestProperty("e-auth-c", m38421e(cid));
conn.setRequestProperty("e-auth-k", rc_key);
HeaderValueHow computed
e-auth-vconstant "e1" (auth protocol version)literal
e-auththe account CIDAbstractC9039h.m10662s() -> SharedPreferences key "clientId", default ""
e-auth-cchecksum of the CIDC21273a.m38421e(cid) - see algorithm below
e-auth-kstatic keyprivate static final String rc_key = "PgdtSBeR0MumR7fO"

Exact e-auth-c algorithm (observation-faithful pseudo-code):

int n = 0;
for (String seg : cid.split("-")) { // Java split() drops trailing empty strings
char[] c = seg.toCharArray();
if (c.length >= 1) n += Character.getNumericValue(c[0]);
}
return String.valueOf(n);

This is a non-cryptographic checksum, not a MAC: it is trivially computable from the CID and reveals only the sum of the first characters of each dash-separated segment. Note that Character.getNumericValue returns -1 for a first character that is not a decimal or hex digit, so the value can be negative.

3.2 When the headers are appliedh5

Two independent gates, both observations:

  1. Host gate: url.startsWith("https://api.eyecon-app.com/app/") || url.contains("eyecon-app.com"). Consequence: the auth headers are also sent to voice2text.eyecon-app.com, ptt.eyecon-app.com, faq.eyecon-app.com, cloudfront-cdn-all.eyecon-app.com, apps.eyecon-app.com, join.eyecon-app.com, cards.eyecon-app.com - that is, any host whose name contains eyecon-app.com.
  2. Per-call gate: addAutoHeaders, the 8th positional argument of m38423g (and the addAutoHeaders JSON field of a queued request). The C21653f / C21652e / C21651d constructors force it to true. Several call sites deliberately pass false: LocationIQ, degoo, getfreshpicsjson.jsp, all virality/*, eyecon_remote_config_url, social_auth/use_cli_validation, and join.jsp (no CID yet).

m38417a(request, true) adds the same four headers to the OkHttp request. m38417a(request, false) (quotes API, Instagram/Facebook scraping, translate.google, the OkHttp downloader, Facebook media fetch) adds none.

3.3 Remote override of e-auth-k - high severityh5

Observation, p032b5.AbstractC3464e.m3668s():

String k = AbstractC15804d.m31091l("circus_key", false); // Remote Config value
Field f = C21273a.m38419c().getClass().getDeclaredField("rc_key");
f.setAccessible(true);
f.set(C21273a.m38419c(), k);

and the compiled-in default is p236d5.AbstractC15804d:103 -> map.put("circus_key", "PgdtSBeR0MumR7fO").

  • Observation: the e-auth-k value is whatever Remote Config currently says, with the APK literal as fallback.
  • Inference (high confidence): whoever can modify the Remote Config project changes the credential that every installed client presents to api.eyecon-app.com, with no app update required. Whether the reflective write succeeds against a static final field is ART-version dependent; the code clearly intends it to.
  • Observation: the Eyecon-specific config channel itself (eyecon_remote_config_url) is cleartext HTTP, and its JSON is merged into the same config map by C15803c.m31073a(...). Whether circus_key is also settable over cleartext depends on a server-side allow-list that static analysis cannot see.
3.4 Other credential headersh5
HeaderValueWhere
Authorization: Token <quotes_api_id>Token e2eeb1aa9f32eb07fa04595a0c457ecb6fadb772p284f7.C16476j0:102
Content-Type: application/json-same
Content-Type: multipart/form-data;boundary=*****hand-built body, field msisdnC21280h.m38441n
Content-Type: application/getStoreItemstore item downloadp379k5.C17754f:89
3.5 401 / E-Mark / CID pathh5

Observation (C21273a.m38420d, plus AbstractC9039h.m10641U):

int code = conn.getResponseCode();
if (code != 401) return code;
if (!(url.startsWith(base) || url.contains("eyecon-app.com")) || isEmpty(conn.getHeaderField("E-Mark")))
return 700; // androidx TYPE_DURATION == 700
if (!MyApplication.f12170o) return 401; // f12170o == "am I the main process?"
AbstractC9039h.m10641U(conn.getHeaderField("cid"), "401 1 from url = " + url);
return 401;

AbstractC9039h.m10641U(cid, reason) - “unAuthorizedUser” - is a destructive handler:

  1. bails out if RegistrationActivity.f12898k1 != null (a join is already in flight) or if !m10665v() (not an Eyecon user);
  2. bails out if the cid header equals the static AbstractC9039h.f13178e;
  3. bails out if SP flag SP_unAuthorizedUser_isCleared is already true (one-shot);
  4. otherwise sets SP_unAuthorizedUser_isCleared = true and clear_data_by_eyecon = true, posts EYECON, STOP_FOREGROUND_NOTIFICATION to stop CallService, fires the analytics event "User unauthorized", and finishes/kills the app.

Additional observation: the handler logs the raw CID and URL to logcat via C18416a.m34588y("Authenticator", "unAuthorizedUser reason = %s", reason).

Inference: a stolen or replayed CID therefore causes the victim’s client to wipe its own authenticated state - the server can remotely de-register a device by returning 401 with an E-Mark header.


4. Endpoint catalogue - Eyecon functional APIh4

Parameter-name note: cv (value vc_749_vn_4.2026.04.23.1145_a) is the build stamp; in the decompiled code it is spelled ApsMetricsDataMap.APSMETRICS_FIELD_APSVERSION, whose value is "cv". Unless stated otherwise, all endpoints below: Auth = yes, params in the URL query string, fire-and-forget unless a callback is named.

4.1 Registration, authentication, CLI validationh5
join.jsp - register an account (CID) for a phone numberh6
  • URL / method: GET https://api.eyecon-app.com/app/join.jsp?<params>&cv=vc_749_vn_4.2026.04.23.1145_a
  • Issuer: p365jd.RunnableC17614z case 1. URL cached in C21280h.f56467c.
  • Auth headers: addAutoHeaders = false - the client has no CID yet.
  • Callback: yes, p262e7.AbstractC16130b; C21280h.m38437j(...) maps the server result onto p494q7.EnumC19653e1.
ParamValue source
clip399l7.C18312b.m34437h().m34444e(rawCli) - E.164-normalised CLI
usernameliteral "New_User_Eyecon"
devicenamep008a7.AbstractC0062l0.m357l1() -> "<MANUFACTURER> <MODEL>" (de-duplicated via m347b1)
user_langAbstractC0062l0.m356k1() - device language, iw->he, in->id, ji->yi, default "en"
os"Android " + Build.VERSION.RELEASE + " ; " + Build.VERSION.SDK_INT
public_idAbstractC9039h.m10625E() - SP SP_UUID (previous public id, may be empty)
adv_idp448o4.InterfaceC19056i.f48663a.m35361a() - Google Advertising ID (cached in Eyecon.SP_KEY_ADVID)
mcp008a7.AbstractC0042b0.m175X() - uppercase hex MAC of wlan0
viral_idSP SP_KEY_INVITER_ID
cli_cccaller-supplied country code of the CLI
svronly when re-joining: C19644b1.f50539o.toString()
transportonly if non-null: mo_sms / so_sms / so_call / so_flash / huawei / fb / social_login
generated_cliSP SP_REJOIN_OLD_GENERATED_CLI, only if non-null
imeip418m5.C18502p.m34691f() - up to 3 IMEIs across slots, de-duplicated, comma-joined
os_idp008a7.AbstractC0042b0.m172U() - Settings.Secure "android_id"
cvbuild stamp
  • Response consumption: headers, not body. map.get("pi") becomes the new SP_UUID (public id); missing -> Exception("Join, missing public id") + failure. map.get("sd") == "0" -> isDeviceChanged. map.get("is_new") == "1" -> isJoin. The body is passed to the callback verbatim and to p268eh.AbstractC16257t1.m31616A(...).
  • Error mapping (C21280h.m38437j -> EnumC19653e1): non-200 -> HTTP_RESPOND_CODE(i); -10 -> CLIENT_RETURN_WITH_NO_ERROR; HTTP 200 with an empty body -> JOIN_RESPOND_CODE_200_WITHOUT_CID.
cli_auth/gettransport - start phone-number validationh6
  • URL / method: GET https://api.eyecon-app.com/app/cli_auth/gettransport?<params>
  • Issuer: p365jd.RunnableC17614z case default. Auth: addAutoHeaders = z14 (is_already_social_auth), hence false on a first-time validation. Callback: yes, C19647c1.
ParamValue source
clinormalised CLI
reg_idFirebase Cloud Messaging registration token
is_already_social_authboolean string
entire fingerprint set from C21280h.m38430c (section 7)
transportoptional EnumC19662h1 name
last_erroroptional error string
  • Response (JSON) keys read: error_status (int -> EnumC19653e1), subcode (0 = all retries failed, 1 = country not supported, 2 = deleteme over limit, 3 = banned by traffic analyzer), transport, and details{} with send_to, timeout, body, code (for MO_SMS) or pattern (for SO_FLASH).
  • Notable: the SERVER_BANNED_BY_TRAFFIC_ANALYZER code demonstrates that server-side traffic-anomaly banning exists.
cli_auth/checkcode - submit the validation codeh6
  • URL / method: GET https://api.eyecon-app.com/app/cli_auth/checkcode?<params>
  • Issuer: p516r5.RunnableC19875s case 1. Callback: yes, AbstractC16130b.
ParamValue source
clinormalised CLI
codecode entered by the user
transportEnumC19662h1 name
is_already_social_authboolean string
cvbuild stamp
  • Response: plain integer text. 0 + HTTP 200 -> success -> continues into the p308gj.C16770d join flow. Anything else -> C21280h.m38437j.
  • Retry: attempted twice - on any Throwable the app sleeps 5000 ms (AbstractC0077t.m439X0(5000L)) then retries once (RunnableC19875s:137-139). The same pattern is used by social_auth/validatetoken.
social_auth/use_cli_validation - “join with real number” after social loginh6
  • URL / method: GET https://api.eyecon-app.com/app/social_auth/use_cli_validation?<params>
  • Issuer: p607vk.RunnableC21082e0 case 23.
  • Observation: the internal label passed is "join.jsp" although the path is social_auth/use_cli_validation; the label only affects logging and DB naming.
  • Params: device_id (android_id), cli (normalised), advid (Advertising ID), cv.
  • Callback: yes (C19677n); the body is forwarded, then AbstractC16257t1.m31616A(false, null, false, true).
social_auth/validatetoken - exchange an OAuth token for a CLIh6
  • URL / method: GET https://api.eyecon-app.com/app/social_auth/validatetoken?<params> (jsonAccept = true)
  • Issuer: p607vk.RunnableC21093i case 25. Callback: yes (C15802b).
ParamValue source
device_idandroid_id
sim_ccAbstractC0062l0.m369x1() - TelephonyManager.getSimCountryIso() upper-cased
user_ccAbstractC0062l0.m371z1() - SP userCountryISO, defaults to the SIM country
adv_idGoogle Advertising ID
providere.g. facebook, google
tokenprovider access token
cvbuild stamp
  • Response: JSON that must contain a non-empty cli; otherwise p284f7.C16479l("validatetoken api return with missing info, json = ..."). A real key is read and discarded (AbstractC16468f0.m32025t("real", null, json)).
  • Retry: once, after a 5 s sleep, on Throwable.
  • Security note: C18416a.m34588y("Server", "validateTokenReg provider = %s, token = %s", provider, token) writes the raw provider access token to logcat unconditionally.
deleteme.jsp - account deletion / right-to-erasureh6
  • URL / method: GET https://api.eyecon-app.com/app/deleteme.jsp?cv=...
  • Issuer: p598v6.RunnableC20874a case default, from AboutActivity’s “Delete account” flow.
  • Callback: yes (C18716e). The body is ignored; only the HTTP status matters.
4.2 Contactsh5
addcontacts.jsp - bulk contact upload (gzip POST)h6
  • URL / method: POST https://api.eyecon-app.com/app/addcontacts.jsp
  • Issuer: p620w6.C21280h.m38428a(String contacts, boolean init) -> new C21651d(url, "POST", init, 2, "addcontacts.jsp") -> C21649b.m38854c(...) -> row inserted into table_api_requests -> executed by ApiRequestWorker.
  • Transport: C21273a.m38424h. The query string produced by C18416a.m34580q(params) is UTF-8 encoded, GZIP-compressed, and written as the request body, with Content-Type: application/x-www-form-urlencoded and Content-Encoding: gzip. The URL itself carries no query string.
ParamValue source
contactscli@z@name records; @a@ separates fields, @z@ separates records - the same encoding as removecontacts.jsp (p541s5.RunnableC20065s:92-96)
cli_ccTelephonyManager.getSimCountryIso().toLowerCase()
init"1" / "0" first-upload flag
possible_langsAbstractC0062l0.m356k1().toLowerCase()
cvbuild stamp
  • Response: read then discarded (the StringBuilder in m38424h is never returned) - fire-and-forget at payload level; only the HTTP status feeds the queue bookkeeping.
  • On success: DBContacts.m10115r(); if isCameFromReg, DBContacts.m10143p(false, "updatePostRequest", true, false, null) re-syncs.
  • Follow-up: may also issue getcontactslang.jsp.
  • Caveat: C21280h.m38428a has no call site in the decompiled tree (section 11). The payload format is inferred from the matching removecontacts.jsp builder (inference, medium-high confidence).
removecontacts.jsp - bulk contact removalh6
  • URL / method: POST https://api.eyecon-app.com/app/removecontacts.jsp?contacts=...&cv=... (params in the URL, empty body)
  • Issuer: p541s5.RunnableC20065s case default -> new C21653f(params, url, "POST", "removecontacts.jsp") -> queued.
  • Params: contacts = cli@a@name@z@cli@a@name... built from each contact’s cli_in_server + name; cv.
  • Response: ignored.
getupdatedcontactsinfo.jsp - poll for server-side contact updatesh6
  • URL / method: GET https://api.eyecon-app.com/app/getupdatedcontactsinfo.jsp?cv=..., gzip = false, addAutoHeaders = true, jsonAccept = false
  • Issuer: p607vk.RunnableC21093i case 26 (private m38227a), driven from DBContacts.
  • Params: cv only.
  • Response header consumed: sms. C21280h.m38432e checks Integer.parseInt(map.get("sms")) > 0; on a positive value it re-enqueues RunnableC21276d(..., 5) (fetch new external SMS ids), sets SP sp_new_external_sms_in_server = true and calls back. NumberFormatException is swallowed.
  • Response body format: a @z@-separated list of @a@-separated records; per record: [0] = cli_in_server, [1] = is_viewer, [2] = os_type, optional [3] = number of social pairs then repeated (social_type, social_id) pairs, then is_sub, then is_can_talk_enable, then vn. Values are written into the social table (DBContacts.f164K0/f167L0/f176O0/f216f/f179P0, f255s, f273y, f276z).
getfreshpicsjson.jsp - fetch “fresh” caller-ID photo URLsh6
  • URL / method: GET https://api.eyecon-app.com/app/getfreshpicsjson.jsp?init=...&cv=...&trigger=...
  • Issuer: androidx.fragment.app.RunnableC2598c (case 7 / default), invoked from DBContacts.m10143p(...) and p541s5.RunnableC3686q0.
  • Callback: a Runnable (RunnableC20055i) plus an output ArrayList.
ParamValue
init"1" / "0"
cvbuild stamp
triggercaller-supplied sync-trigger string
  • Response (JSON array), keys read: cli (String -> cli_in_server), type (int -> pic_status_in_server), big_pic (String, optional -> big_pic_url), small_pic (String, optional -> small_pic_url), tag (String, optional -> fresh_pic_tag).
  • 401 handling: on HTTP 401 an empty row carrying an "error 401" marker is appended to the result list and the callback still runs.
getnames.jsp - reverse lookup of caller name / spam verdicth6
  • URL / method: GET https://api.eyecon-app.com/app/getnames.jsp?cli=...&lang=...&is_callerid=...&is_ic=...&cv=...&requestApi=...&source=...[&is_search=true]
  • Issuer: RunnableC2598c case 6 (caller-ID path, jsonAccept = true) and p599v7.RunnableC20908f case 5 (photo-picker path, source = PhotoPicker).
  • Callback: yes - p620w6.InterfaceC21279g.mo34917g(String[] names, C21657d result, int errorCode).
ParamValue source (caller-ID path)
cliC18312b.m34437h().m34444e(cli) (normalised)
lang"<contactLang or 'en'>" + "," + "<deviceLang>" when the device language is not already contained; otherwise just contactLang
is_callerid"1"
is_icboolean string of the caller (is_ic flag)
cvbuild stamp
requestApi"okHttp" or "URLconnection" - chosen by C21280h.f56469e, which is new Random().nextBoolean() evaluated once per process start
sourcecaller-supplied feature tag, e.g. PhotoPicker
is_search"true" only on the photo-picker path
  • Response (JSON array), keys read: name (String), spam_type (String, compared against "spam"), suspicious_spam (Boolean). C21280h.m38429b maps the name to "" when it is blank / "spam" / "maybe spam".
  • Client-side caching: before hitting the network the app consults p648x7.C21670q and p554t5.C20337g caches; if a cached name exists it is returned with errorCode = 0 and no request is made.
  • Failure modes mapped to errorCode: 2 = not an Eyecon user / skipped; 2 = no HTTP network or an exception; 2 = “trying to use cache” miss path or exception; 1 = non-200 HTTP or an empty body; -1 = a cache entry is invalidated when the HTTP code is 404 or 200.
notifyaddcontact.jsp - notify the server of a newly added contacth6
  • URL / method: GET https://api.eyecon-app.com/app/notifyaddcontact.jsp?notifycli=<cli>&cv=...
  • Issuer: as.RunnableC3316o case default. Fire-and-forget: result array int[] is initialised to {0} and never read; the call is wrapped in a try/catch that only prints a stack trace.
  • Guard: only when notifycli is non-empty and AbstractC9039h.m10665v() (is an Eyecon user).
getcontactslang.jsp - negotiate the contact-name languageh6
  • URL / method: GET https://api.eyecon-app.com/app/getcontactslang.jsp?cv=...
  • Issuer: p620w6.C21280h.m38428a (post-addcontacts.jsp).
  • Trigger condition: SP contactLangNeedUpdate, or the upload contains > 20 records, or (records * 100) / contactsInDb > 5 (more than 5 % of the address book is being re-sent).
  • Response: a bare 2-letter language code, validated against [a-zA-Z]{2}; stored in SP contactLang, clears contactLangNeedUpdate, and if it differs from the currently selected quotes language a quotes reload is scheduled.
  • Error handling: any exception is caught and discarded.
picapproval.jsp - user approval / rejection of a proposed contact photoh6
  • URL / method: GET https://api.eyecon-app.com/app/picapproval.jsp?piccli=...&tag=...&isapproved=...&cv=...
  • Issuer: p572u4.RunnableC20597o case 5, via C21280h.m38431d(...).
  • Params:
ParamValue source
picclicomma-joined list of CLIs from the caller’s ArrayList
tagthe photo tag, with &access_token=<FB token> stripped from it (str.replace("&access_token=" + PhotosTrackerWorker.m10433e(), ""))
isapproved"yes" / "no"
cvbuild stamp
  • Transport difference (observation): when isapproved == "yes" the call is executed immediately and synchronously (m38423g, result returned through strArr[0]); when "no" it is queued into table_api_requests via C21649b.m38854c(new C21653f(...)).
  • Guard: if the CLI list is empty, the tag is blank, or the user is not an Eyecon user, strArr[0] = "" and nothing is sent.
getoptionalpics.jsp - “survey” alternative photos for a contacth6
  • URL / method: GET https://api.eyecon-app.com/app/getoptionalpics.jsp?clipics=<cli>&cv=...
  • Issuer: p607vk.RunnableC21082e0 case 22, from PhotoPickerActivity.
  • Params: clipics = the contact’s CLI; cv.
  • Callback: yes. errorCode 1 = not an Eyecon user, 2 = exception, HTTP code otherwise.
  • Response schema: NOT RECOVERED. The raw body is handed to PhotoPickerActivity’s callback; the consuming code path was not decompiled. The DB columns that the result must feed are is_survey_pic (AbstractC0039a.f243o), survey_pic_url (f246p) and survey_pic_tag (f249q) - inference from DBContacts.java:3099-3103.
pic?cli=... - caller-ID photo download (binary)h6
  • URL / method: GET https://api.eyecon-app.com/app/pic?cli=<cli>&is_callerid=true&size=<small|big|null>&type=0&src=<src>&cancelfresh=0&cv=vc_749_vn_4.2026.04.23.1145_a
  • Issuer: p620w6.C21280h.m38434g(String src, int size, String cli, InterfaceC18539o cb) -> C18540p.m34736e(...) -> C18540p.m34732a(...).
  • Exact construction (C21280h.m38434g, lines 323-338):
Query paramValue
clithe raw CLI passed by the caller
is_calleridliteral true
sizeConstants.SMALL = "small" when size == 1, "big" when size == 2, otherwise the literal string "null"
typeliteral 0
srcthe src argument (feature tag)
cancelfresh0, then &cv=vc_749_vn_4.2026.04.23.1145_a appended
cvbuild stamp
  • Guard: C18312b.m34437h().m34452q("+" + cli) must be true (a dialable number), else the callback fires with error 812 and a null bitmap.
  • Response: raw image bytes -> BitmapFactory.decodeStream. Additional caching behaviour: the downloaded bitmap is PNG-compressed and stored as photo_bytes (rejected above 2048 KiB, or down-scaled to at most 1080x1920 first); a redirect and a photo_found boolean are stored in p554t5.C20337g.
  • “Balwan” filter: C18540p.m34733b parses remote-config key balwan_recognizer and throws FileNotFoundException (surfacing as HTTP code -404) when the URL matches a blocked urls[] + segments[] combination, so blocked Facebook CDN photos are never cached.
4.3 User profile, settings, spam, sharingh5
uploadpic.jsp - user profile photo upload (hand-built multipart)h6
  • URL / method: POST https://api.eyecon-app.com/app/uploadpic.jsp
  • Issuer: p620w6.C21280h.m38441n(); URL cached in C21280h.f56468d; driven by com.eyecon.global.MoreMenuAndSettings.UploadProfileWorker.
  • Guards: SP SP_KEY_NEED_UPDATE_PROFILE_PIC must be true; AbstractC9039h.m10665v(); the file <filesDir>/.Eyecon/.nomedia/temp_photo.jpg must decode to a bitmap. Returns -1 if any guard fails.
  • Headers: Content-Type: multipart/form-data;boundary=*****; the four e-auth* headers via C21273a.m38427k. setDoInput(true), setDoOutput(true), setUseCaches(false). No timeouts set.
  • Body (verbatim):
--*****\r\nContent-Disposition: form-data; name="msisdn"\r\n\r\n<clientId>\r\n
<JPEG bytes, compress format JPEG, quality 90>
\r\n--*****--\r\n
  • Response: read line-by-line and discarded. On HTTP 200 the SP flag is cleared and 1 is returned; 0 on any non-200 or exception.
updateuserdetails.jsp - generic user-attribute update (many distinct params)h6
  • URL / method: GET https://api.eyecon-app.com/app/updateuserdetails.jsp?<one-of-the-below>&cv=...
  • Issuer: p620w6.RunnableC21275c cases 2/3/4, p607vk.RunnableC21082e0 case 24, p516r5.RunnableC19874r case 12, p475p6.RunnableC19310j case default.
  • Response: ignored. Only the HTTP status gates a local SP write.
VariantParamValue sourceSP write on HTTP 200
adv_idadv_idGoogle Advertising ID, only sent when it differs from SP SP_KEY_SERVER_UPDATED_WITH_ADVERTISING_IDstores the new adv_id
mcmcWi-Fi MAC, only when SP is_mac_updated is falseis_mac_updated = true
versioncv onlybuild stampappVersion = "vc_749_vn_4.2026.04.23.1145_a"
FCMg_reg_idthe new FCM token, only when it differs from SP SP_FCM_TOKEN_EYECONstores SP_FCM_TOKEN_EYECON
IMEIimeiC18502p.m34691f(), only when it differs from SP imeis_updatedstores imeis_updated
usernameusernameSP username (the local profile name), only when it differs from SP usernameserverstores usernameserver
queued variantusername + cvvia C21653f -> table_api_requestsnone
addfbtoken.jsp - associate a Facebook token with the accounth6
  • URL / method: GET https://api.eyecon-app.com/app/addfbtoken.jsp?token=...&fbid=...&cv=...
  • Issuer: p620w6.RunnableC21275c case 1 - fired from C21280h’s constructor, i.e. once per process start when both SP values exist.
  • Params: token = SP facebookToken, fbid = SP facebookId, cv.
  • Response: ignored. Fully wrapped in try/catch (stack trace only).
mysettings - read / write the server-side user settings blobh6

Two shapes:

GETPOST
URLGET https://api.eyecon-app.com/app/mysettingsPOST https://api.eyecon-app.com/app/mysettings?type=..&key=..&value=..
gzipyes (Accept-Encoding: gzip)no
Issuerp620w6.RunnableC21276d case 8p572u4.RunnableC20597o case 7
Paramsnonetype, key, value (obj.toString())
Callbackyes - HTTP != 200 -> "Error code N", empty -> "Result is empty"yes - always calls back; error code 1 on exception
Responseraw body forwarded to the callbackignored

Note the same "mysettings" endpoint name is also used as a (misleading) log label for spam (RunnableC21082e0:217) and GET share (RunnableC20880f:210).

spam - fetch the server-side spam block-list deltah6
  • URL / method: GET https://api.eyecon-app.com/app/spam?list_version=<v>, gzip enabled, addAutoHeaders = true
  • Issuer: p607vk.RunnableC21082e0 case 21, from com.eyecon.global.Others.Tasks.DailyWorker.
  • Params: list_version = the client’s last-known list version string; no cv.
  • Callback: yes (C16937a); HTTP != 200 -> "Error code N", empty -> "Result is empty", otherwise the raw body.
  • Note: the remote flag spam_list_sync_interval defaults to -1 (disabled) and is read in a sibling class.
share - social-media share of a video/posth6
GETPOST
URLGET https://api.eyecon-app.com/app/share (gzip, addAutoHeaders = true)POST https://api.eyecon-app.com/app/share with a JSON body
Issuerp598v6.RunnableC20880f case defaultp607vk.RunnableC21093i case 24
GuardsEyecon user, share_video_list_sync_interval != -1Eyecon user
Requestno params{cli_list: [phone_number_in_server...], type: <provider>, content: <serialised media>, provider: <provider enum>}
Content-Type-application/json (via m38425i)
Callbackyes (C17485b)none - fire and forget
  • GET response (JSON array), keys read by p621w7.C21295o.m38463e: id (String), type (String), cli (String), content (a JSON string which is re-parsed), provider (String, optional), share_ts (Long epoch ms).
spamreport - user “this number is spam” feedbackh6
  • URL / method: POST https://api.eyecon-app.com/app/spamreport with a JSON body
  • Issuer: p599v7.RunnableC20908f case 4, through new C21652e(json, "data", url, "reportBlock", "application/x-www-form-urlencoded", "POST") -> queued.
  • Body: {type: "blocked", blocked_numbers: [<cli>, ...]}.
  • Observation: the body is a raw JSON string but the Content-Type header is set to application/x-www-form-urlencoded. Server-side parsing of this is therefore server-dependent.
  • Response: ignored. Bails out with an error log if the number list is empty or the user is not an Eyecon user.
name_report - “this name is wrong” feedbackh6
  • URL / method: GET https://api.eyecon-app.com/app/name_report?name=<name>&cli=<cli> (no cv)
  • Issuer: p544s8.RunnableC20081f case 1.
  • Params: name = the name shown on the contact, cli = the number. Observation: the two arguments are the other way round at the call site - C21872a("name", str) where str is the raw number and C21872a("cli", buttonText). Either the call site or the parameter names are wrong (flagged, not resolved).
  • Response: ignored. Guarded on Eyecon user + non-empty name + non-empty number.
  • URL / method: POST https://api.eyecon-app.com/app/social-mngr with an application/json body
  • Issuer: p541s5.RunnableC20060n case default -> C21652e -> queued.
  • Body: [{cli, stype, sid, b_pic}] - the CLI, an integer social status (-1 = not linked, 2/11 = CanTalk states, etc.), the social id, and a boolean.
  • Response: ignored.
grps/sgn - APK signing-certificate fingerprint report (RSA-encrypted)h6
  • URL / method: POST https://api.eyecon-app.com/app/grps/sgn, body type text/plain
  • Issuer: p516r5.RunnableC19874r case 11 -> C21652e -> queued.
  • Plaintext before encryption: {signatures: [<sha?>...], cid: <clientId>, random: <Math.random()>}.
  • Encryption: RSA with the public key R.string.pk_enc_2 (KeyFactory.getInstance("RSA").generatePublic(new X509EncodedKeySpec(Base64.decode(...)))), then C20369c.m37306n(...).
  • Guards: SP SP_KEY_DID_UPDATE_V1453 not yet set; Eyecon user; remote sig_report_config.is_enable (default false); cooldown_days_after_join (default 1) elapsed since sp_join_time.
  • Response: ignored. SP flag set immediately after enqueue.
  • Related key: R.string.pk_enc (RSA-2048 public key) is used by p284f7.AbstractC16468f0:362 for a different purpose (local signature verification of the APK’s own certificate).
shorten-url - URL shortenerh6
  • URL / method: POST https://api.eyecon-app.com/app/shorten-url, Content-Type: application/x-www-form-urlencoded, addAutoHeaders = true
  • Issuer: p607vk.RunnableC21082e0 case 26, built as an OkHttp FormBody.
  • Body params: url (the target URL, form-encoded), ead = "30".
  • Response: raw body = a short token. On HTTP 2xx and a non-empty token the app synthesises https://api.eyecon-app.com/pub/su/<token> and hands that to the callback.
  • Typical url value: https://api.eyecon-app.com/app/pub/redirectme.jsp?goto=geo:<lat>,<lon>?q=<lat>,<lon> (built in p308gj.C16770d:1166 with URLEncoder.encode(..., "UTF-8")). Observation: pub/redirectme.jsp itself is never requested directly - it is only ever embedded into shorten-url.
upgrade_pic2eyecon.jsp - legacy photo-migration triggerh6
  • URL / method: GET https://api.eyecon-app.com/app/upgrade_pic2eyecon.jsp with no parameters at all (new ArrayList(2) is empty, so not even cv).
  • Issuer: p620w6.RunnableC21275c case 6.
  • Response: ignored.
resetupdatedstatus.jsp - acknowledge the contact-update pollh6
  • Intended URL / method: GET https://api.eyecon-app.com/app/resetupdatedstatus.jsp?cv=..., issued from p607vk.RunnableC21082e0 case 25, labelled "resetupdatedstatus.jsp".
  • Observation / probable bug: the URL passed to m38423g is C21280h.f56466b, i.e. the base URL https://api.eyecon-app.com/app/ with no endpoint path. Either a genuine defect or a stale build; static analysis cannot tell. The success flag is code == 200 && body.length() > 0.
4.4 CanTalk (“can I talk?”) flowh5
cantalk.jsp - send a CanTalk invitationh6
  • URL / method: GET https://api.eyecon-app.com/app/cantalk.jsp?recvcli=<cli>&q=<message>
  • Issuers: p474p5.RunnableC19300e.run() and p474p5.AsyncTaskC19296a (dispatch value 19).
  • Params: recvcli = the normalised receiver CLI, q = the message text (CampaignEx.JSON_KEY_AD_Q == "q"), cv.
  • Response: "-1" -> allow; "" or "0" -> deny; any other non-empty value -> allow with a message.
getcantalkstatus.jsp - poll the status of a CanTalk requesth6
  • URL / method: GET https://api.eyecon-app.com/app/getcantalkstatus.jsp?notifiedcli=<cli>&cv=...
  • Issuer: p474p5.RunnableC19300e.run(); polled in a loop with Thread.sleep(1000) until f49586e flips.
  • Response: raw string; anything non-200 -> "".
cantalkreceived.jsp / cantalksaw.jsp - delivery/read receiptsh6
  • URL / method: GET https://api.eyecon-app.com/app/cantalkreceived.jsp?sendercli=<cli>&cv=... and the identical cantalksaw.jsp.
  • Issuer: both via p451o7.AbstractC19089d.m35398e -> AbstractC0062l0.m354i1(new AsyncTaskC19296a(), "", 34, cli, "cantalkreceived.jsp"), and com.eyecon.global.CanTalk.CanTalkActivity.onWindowFocusChanged -> "cantalksaw.jsp".
  • Response: ignored (fire-and-forget, iArr == null).
  • Note: both are triggered by FCM push payloads of type cantalk_q / cantalk_y / cantalk_n (see AbstractC19089d.m35398e).
4.5 History / eventsh5
sethistory.jsp - report a contact actionh6
  • URL / method: GET https://api.eyecon-app.com/app/sethistory.jsp?cv=...&to=<cli>&time=<sec>&type=<int>
  • Issuer: p607vk.RunnableC21088g0 case default.
  • Params: to = C18312b.m34437h().m34444e(cli); time = timestampMillis / 1000; type = integer action code; cv.
  • Guards: Eyecon user and !AbstractC9039h.m10667x() (i.e. not in the “not registered with real CLI” state).
  • Response: ignored.
eventreport.jsp - generic in-app event reportingh6
  • URL / method: POST https://api.eyecon-app.com/app/eventreport.jsp, body = a raw JSON object, Content-Type: application/x-www-form-urlencoded, dataName = "data" (so m38425i prefixes the body with data=).
  • Issuer: p620w6.C21280h.m38436i(String name, String type, JSONObject attributes) -> RunnableC21129z.
  • Body: {name, type, attributes: <caller JSONObject>, timestamp: <System.currentTimeMillis()/1000>}.
  • Auth: AbstractC9039h.m10665v() is passed as addAutoHeaders, so unauthenticated users send no auth headers.
  • Response: ignored. Exceptions are caught and reported to the global crash handler.
4.6 Voice2Text / TTM (voice2text.eyecon-app.com)h5

All of these receive e-auth-* headers because the host contains eyecon-app.com.

#EndpointMethodParams / bodyIssuerNotes
1/init-ttmPOST application/json{mycli, purchases: <JSONArray>, freeTrialCode: <remote "ttm_free_trial_code">}p620w6.RunnableC21274b case 0Body sent only if it differs from SP SP_KEY_LAST_INIT_TTM_JSON. On HTTP 200 the body is stored and parsed; failures logged.
2/ackPOST application/jsonthe acknowledgement objectp620w6.RunnableC21274b case defaultResponse logged only; timing measurement is broken (elapsedRealtime() - elapsedRealtime()).
3/start-ttmPOST application/json{mycli, acli, calltag, purchases}p572u4.RunnableC20597o case 6Accepts any 2xx. Response forwarded to the callback.
4/no-ttmPOST application/json{acli, mycli}as.RunnableC3316o case 9Response logged. mycli = SP authenticatedCli.
5/getTextGETmycli = SP authenticatedClip620w6.RunnableC21276d case 1 (checkMessagesTtm)Accepts any 2xx and a non-empty body; raw text handed to C15802b.
6/ttm-get-tokens-amountGET, jsonAccept = truemyclip620w6.RunnableC21276d case 7Response parsed as JSON into C3502i.
7/ttm-call-forwarding-resultGETmyclip620w6.RunnableC21276d case 3Response parsed as JSON; .m14606i() (int) extracted.
4.7 Virality / referralh5
EndpointMethodParamsIssuerResponse
virality/viral_idGETnonep620w6.RunnableC21276d case 9raw string; the app builds https://eyecon-app.com/vrs/evid-<id> and stores SP SP_KEY_MY_VIRAL_ID
virality/viral_id_checkGETnonep620w6.RunnableC21276d case 4raw string forwarded to the callback
virality/premium_statusGETnonep620w6.RunnableC21276d case 2, from InstallReferrerReceivera bare long = premium expiry epoch seconds; multiplied by 1000 client-side
4.8 Messages / pushh5
EndpointMethodBody / paramsIssuerNotes
messages/getGET, gzipnonep620w6.RunnableC21276d case 5Requires the sms response header from getupdatedcontactsinfo.jsp to be > 0; raw body forwarded
messages/receivedPOST application/jsonJSONArray of external SMS message idsp620w6.RunnableC21278f case default -> C21652e -> queuedSP sp_new_external_sms_in_server is cleared immediately after enqueue, before the request is confirmed (fire-and-forget)
4.9 Toki (push-to-talk) - non-HTTPh5
  • Endpoint: ptt.eyecon-app.com, UDP (java.net.DatagramSocket), port chosen from remote key toki_ports = [5620, 50000-50010], fallback 5620 (p720zq.AbstractC22312g.m39737C). The server address can be overridden per-intent by INTENT_KEY_SERVER_ADDRESS.
  • Issuer: p703z7.C22122v0 -> p633wl.C21450m.m38608b(host, port) -> p340i7.RunnableC17180l0 case 6 -> as.C3349u2(InetAddress, port, session).
  • Control messages are JSON, built by C21450m.m38606c(cli, cid, checksum) and always carrying:
FieldValue
cliC18312b.m34437h().m34444e(authenticatedCli)
vn"4.2026.04.23.1145"
vc749
e-auth-v"e1"
e-auththe CID
e-auth-cthe m38421e checksum

plus, per message type: invited_clis (CHANNEL_INVITE), channel_id + invitation_id (CHANNEL_JOIN, INVITE_REJECT, SESSION_STATUS), session_id, sound_fx_id + type = "playSoundFX" (CHANNEL_BROADCAST_COMMAND), channel_id + session_id (TALK_DONE), status.

  • Media is raw UDP: two packets are sent per frame, the second XORed against the previous frame for delta compression, with a rolling 32-bit sequence number written into bytes 7..10. No encryption at the transport layer is visible in the decompiled code (inference: the auth is control-plane only).
  • Auth failure: if SP authenticatedCli is null the app calls AbstractC9039h.m10641U("", "Toki - connect canceled not eyecon user") - the same destructive 401 handler.
4.10 Non-Eyecon destinations initiated by the apph5
EndpointMethodParams / bodyIssuerNotes
http://numberlookup.instabridge.com/respond-smsPOST (Retrofit + Moshi){contactName, number, messages:[{role: "User"|"System", message}], locale, messagesInSession, userName}p636wo.C21474f, interface p646x5.InterfaceC21626aCLEARTEXT HTTP. Response is a polymorphic RespondSmsResponse: NewMessage{message} or EndConversation. Driven by the AI SMS assistant.
https://api.paperquotes.com/apiv1/quotes/?lang=&order=-likes&limit=&maxlength=&curated=1GETlang (iw->he), order=-likes, limit=<StatisticData.ERROR_CODE_NOT_FOUND>, maxlength (96, or 117 for hi), curated=1; only on the first pagep284f7.C16476j0.m32047bAuthorization: Token e2eeb1aa9f32eb07fa04595a0c457ecb6fadb772. Response keys results (JSONArray) and next; pages are chained via SP NextQuotesUrl_v4. <html> / <!doctype bodies are rejected.
https://us1.locationiq.com/v1/search.php (country us) else https://eu1.locationiq.com/v1/search.phpPOSTq (query/phone), format=json, limit=1, countrycodes=<user country lower-case>, key=pk.f8b1a66cf0f04c66264e7e72a00fac7fp379k5.RunnableC17750b case 24Response keys lat, lon. HTTP 429 logs “exceeded the per-X rate-limits”. addAutoHeaders = false - no Eyecon auth headers.
https://workers.degoo.com/locationGETnonep008a7.RunnableC0064m0 case 0Response keys latitude, longitude; used only as a feature gate (Bangalore / Stockholm proximity). addAutoHeaders = false.
https://translate.google.com/translate_a/single?client=at&dt=t&dj=1&sl=auto&tl=<lang>&q=...GETq = "MY NAME IS " + <contact name> (prefix only when the target language is en)p344ic.RunnableC17219d1 case 24Response: sentences[0].trans, the "MY NAME IS " prefix stripped. Sends a contact name to Google. Chrome-112-Android User-Agent.
https://<store_download_url>/getItem/<item>POST application/getStoreItem{country, language, cli, cid, version_name: "4.2026.04.23.1145", version_code: 749, product: {...}, item: {...}}p379k5.C17754f.m34065astore_download_url defaults to "" (disabled). Non-200 -> StoreException. Response is a binary file.
Instagram / Facebook / TikTok profile & mediaGETsee p599v7.C20917o.m38009h, p303g5.RunnableC3316o, p541s5.RunnableC20056j, p607vk.RunnableC21093i case defaultPhoto picker / video sharingFull Chrome-112 (mobile and desktop) header sets including sec-ch-ua*, sec-fetch-*, and a spoofed authority: www.facebook.com. Facebook media URLs are extracted with the remote regex facebooke_Extract_URL_Regex, Instagram .mp4 URLs with findFirstMp4UrlForInstagramRegex.
https://www.tiktok.com/oembed?url=<videoUrl>GET-p621w7.C21295o:216Used to enrich shared TikTok posts.
Google reverse-image searchWebViewsearch_url = https://www.google.com/search?hl=en&udm=2&q=<name>+<phone>, max_links=75, JS injected via remote google_photos_link_loaderp420m7.C18533lUp to 5 WebViews, custom user_agent from remote config, JS bridge Android.onBigLink(...). Scrapes imgurl out of the results page.
join.eyecon-app.com--p671y4.C21845b:72Registered as the Singular branded domain (http://, cleartext) plus Singular app key instabridge_ffcbcb0b / secret ddddafd404a9a4d918bae58070b136d7.

5. Telemetry / analytics endpointsh4

These are separated from the functional API because they are not part of the caller-ID feature.

5.1 First-party telemetryh5
EndpointMethodPayloadIssuerGuard
eventreport.jspPOST application/x-www-form-urlencoded (data=<json>){name, type, attributes, timestamp}C21280h.m38436ialways (headers only if authenticated)
<analytics_worker_url>POST multipart/form-dataZIP containing data.json = {cli, fake_cli, call_logs:[{phone, date, duration_in_seconds, type, name}]}; form field name call_log, filename <cli>.zip, part content-type application/octet_streamcom.eyecon.global.Analytics.EyeconAnalyticsWorker.m9989cremote analytics_worker_url != "disabled_by_remote" and non-empty (default disabled); Eyecon user; READ_CALL_LOG permission; non-empty CLI
<server_report_url>POST, gzip bodyform field data = <JSON>p620w6.RunnableC21278f case 0 (sendCallReport)remote server_report_url defaults to ""
eyecon_remote_config_urlGETnonep005a4.RunnableC0031c case 17none; cleartext default URL

EyeconAnalyticsWorker details:

  • Scheduled as unique periodic WorkManager work "EyeconAnalyticsWorker" with interval analytics_worker_repeat_interval_time (default 1440 minutes), NetworkType.CONNECTED.
  • Window of logs is [SP_KEY_EYECON_ANALYTICS_LAST_UPLOAD_TIME_V2, now].
  • Remote key analytics_worker_reload_json = {"id":1,"from_date":"01/03/2024"}. When the id changes the window start is replaced with the earlier of the stored time and the parsed from_date - i.e. the server can instruct the client to re-upload historical call logs.
  • Because cli and fake_cli are both included, the server can correlate the pre-validation (“fake”) identity with the post-validation (“real”) identity. The client deliberately re-sends with the real CLI once the first upload succeeds (SP_KEY_NEED_UPLOAD_LOGS_WITH_FAKE_CLI).
  • calls_to_json per record: phone, date (UTC yyyy-MM-dd HH:mm:ss), duration_in_seconds, type (C19856f.m36609d), and optionally name.
  • Result handling: 1 = success, -1 = failure (-> Result.retry()), 0 = nothing to do (-> success()).
5.2 Embedded analytics / attribution SDKs (vendor traffic)h5
SDKEvidence
Firebase Analytics / Crashlytics / Performance / Remote Config / FCM / In-App MessagingMyApplication, FirebasePerfUrlConnection.instrument(...) on every HttpURLConnection, firebase-settings.crashlytics.com, firebaseremoteconfigrealtime.googleapis.com
Amplitudeapi2.amplitude.com, api.eu.amplitude.com, static.amplitude.com; is_amplitude_enabled default true; amplitudeNotTrackableEvents lists ~90 internal events that are sent server-side instead
Singularapp key + secret in p671y4.C21845b:72; singular_events_json frequency caps
AdjustR.string.adjust_app_token = m23t9uwrtvy8
Meta (Facebook) App Eventscom.facebook.appevents.*
AppLovin, Google Ads/AdMob, InMobi, Mintegral, Vungle, Pangle/ByteDance, Fyber FairBid, Inneractive, AdColony, Digital Turbine, Crackle/Mobitech, Amazon Adsvendor SDKs, config in AbstractC15804d

Observation: the app also uses a bespoke in-house event bus (p032b5.AbstractC3464e, C3465f, p672y5.C21867v) whose events are pushed to MainProcessReceiver via EYECON_ACTION_REPORT_EVENT and mirrored to eventreport.jsp.


6. Response-handling, retry and error semanticsh4

6.1 Status codes explicitly checkedh5
CodeMeaning in the appWhere
200success everywhere; body readall helpers
2xx (range)treated as success by the TTM and virality paths (code >= 200 && code < 300)p620w6.RunnableC21276d cases 1/3/7
-10synthetic “no error” marker used as the initial value of several int[] holders; mapped to CLIENT_RETURN_WITH_NO_ERRORC21273a.m38423g, C21280h.m38437j
-1generic failure marker passed through HttpURLConnection exception pathsC21651d/52e/53f catch blocks
-404synthetic “photo blocked / not found”p420m7.C18540p.m34732a
-2, 0, 1, 2, -404, 812callback-specific error codes (2 = “wrong number / no CLI”, 812 = “not a dialable number”, 0 = cache hit)InterfaceC21279g implementors
401see section 3.5C21273a.m38420d
700synthetic code returned when a 401 came from a non-Eyecon host or without an E-Mark header (androidx ... TypedValues.TransitionType.TYPE_DURATION)C21273a.m38420d
404photo loader caches the URL as “not found” (photo_found = false); getnames.jsp invalidates the cached nameC18540p, RunnableC2598c
6.2 Retry / persistence of failed requestsh5

table_api_requests (SQLite) is the durable outbox. Flow:

  1. C21649b.m38854c(request) -> RunnableC21301b case 6 inserts a row (url, json, server_retry_count, uuid) and immediately enqueues RunnableC21275c(12) on the ApiRequestDB executor.
  2. RunnableC21275c case 12 (ApiRequestWorker.doWork) reads all rows (SELECT * FROM table_api_requests) and rehydrates each into the same request class via C21649b.m38853b(...).
  3. Per row:
    • response 200 -> delete the row;
    • response != 200 and server_retry_count >= ApiRequestServerRetryCount (remote default 3, range up to 60) -> delete the row (give up);
    • otherwise -> increment server_retry_count.
  4. If at least one row was retried, re-enqueue ApiRequestWorker with setInitialDelay(apiWorkRepeatIntervalMinutes) (remote default 60 min), BackoffPolicy.LINEAR 1 hour, NetworkType.CONNECTED.
  5. addcontacts.jsp gets a special side effect on success.

Additional ad-hoc retries:

  • cli_auth/checkcode and social_auth/validatetoken: exactly one retry after a 5 000 ms sleep on Throwable.
  • PaperQuotes: none.
  • OkHttp (m38417a): default retryOnConnectionFailure = true.
  • getnames.jsp: no retry, but a local cache + HTTP 404 invalidation.
6.3 Exception handlingh5
  • All HttpURLConnection helpers catch IOException (and OutOfMemoryError in the x6 request wrappers) and return -1.
  • C21273a.m38423g returns "" on any non-200; m38422f/m38424h/m38425i propagate the code through int[].
  • Unhandled exceptions inside lambdas are routed to com.facebook.appevents.AbstractC9039h.m10631K(Throwable) -> m10630J -> in the main process, RunnableC0031c appends the stack trace to SP exceptions_history (capped at 60 000 chars) and prefixes Vn: 4.2026.04.23.1145; in other processes it is forwarded to MainProcessReceiver as EYECON_ACTION_EXCEPTION.
  • Firebase Performance is auto-instrumented on every connection (FirebasePerfUrlConnection.instrument(...)), so network timings are reported to Google.
6.4 Redirect behaviourh5
PathBehaviour
m38423g (query-string API)setInstanceFollowRedirects(true) - the platform follows redirects automatically, including cross-scheme
m38425i, m38424hplatform default (true for HttpURLConnection)
m38417a (OkHttp)OkHttp default: follow, up to 20, but not across http <-> https
C18540p photo loadermanual, max 5 hops, http/https only, else SecurityException("illegal URL redirect"); Eyecon-hosted photos redirecting off-domain are recorded and blocked from caching
uploadpic.jspsetUseCaches(false), no explicit redirect policy
6.5 Character encodingh5

Request params: UTF-8 (getBytes(StandardCharsets.UTF_8)) for gzip/JSON bodies; OkHttp canonical query encoding for URL params. Responses: UTF-8 (accept-charset: UTF-8, new String(bytes, UTF_8)).


7. Device-fingerprint parameter seth4

p620w6.C21280h.m38430c(ArrayList) is the single helper that appends the fingerprint to a request. It is called by join.jsp, cli_auth/gettransport, and (partly) by other flows.

7.1 Fingerprint parametersh5
ParamValueSource (observation)
cv"vc_749_vn_4.2026.04.23.1145_a"constant
installer_nameSP Eyecon-InstallerName, else the installer package’s label; "Google Play" normalised; else "manually or unknown source"p008a7.AbstractC0042b0.m174W()
n_simsnumber of SIM subscriptions foundp418m5.C18502p.m34694i().size() (TelemetryManager + SubscriptionManager)
timeSystem.currentTimeMillis()direct
is_sms_sending_available"true" / "false"AbstractC0077t.m418C0(); on Xiaomi an extra AppOpsManager check for SEND_SMS is applied
is_whatsapp_installed"true" / "false"AbstractC0062l0.m334C1(EnumC19861h0.WHATSAPP)
mcWi-Fi MAC address, uppercase hex, :-separated, from NetworkInterface "wlan0"AbstractC0042b0.m175X()
device_idSettings.Secure "android_id"AbstractC0042b0.m172U()
adv_idGoogle Advertising IDp448o4.InterfaceC19056i.f48663a.m35361a() (cached in Eyecon.SP_KEY_ADVID)
imeiup to 3 IMEIs (getDeviceId, getDeviceIdGemini, getSimSerialNumberGemini, getDeviceIdDs), de-duplicated, comma-joinedp418m5.C18502p.m34691f() via reflection (AbstractC0085x.m463n0)
client_ccLocale.getDefault().getCountry()direct
time_zoneTimeZone.getDefault().getID()direct
device_manuBuild.MANUFACTURER (only when non-empty)direct
device_modelBuild.MODELdirect

Additional identifiers sent on specific endpoints but outside m38430c:

ParamEndpointSource
osjoin.jsp, cli_auth/gettransport"Android " + Build.VERSION.RELEASE + " ; " + Build.VERSION.SDK_INT
devicenamejoin.jsp, cli_auth/gettransportAbstractC0062l0.m357l1()
public_idjoin.jspSP SP_UUID
cli_ccjoin.jsp, addcontacts.jspcaller-supplied / getSimCountryIso().toLowerCase()
user_lang / possible_langsjoin.jsp / addcontacts.jspAbstractC0062l0.m356k1()
sim_cc / user_ccsocial_auth/validatetokenSIM country / SP userCountryISO
country / languagestore getItemLocale display language/country
countrycodesLocationIQAbstractC0062l0.m371z1().toLowerCase()
randomgrps/sgnMath.random()
7.2 Persistent local identifiersh5
SP keyMeaningSent as
clientIdthe CID - the primary account credentiale-auth header
authenticatedClithe validated phone numbermycli on all TTM calls; cli on updateSgn/Toki
SP_UUIDpublic id / analytics user idpublic_id, pi response header, Amplitude/Singular/FB user id
SP_REJOIN_OLD_GENERATED_CLIpreviously server-generated CLIgenerated_cli
SP_KEY_INVITER_IDreferrerviral_id
SP_KEY_FAKE_AUTHENTICATED_CLIpre-validation “fake” numberfake_cli in the analytics ZIP
SP_KEY_MY_VIRAL_IDown referral idused to build eyecon-app.com/vrs/evid-<id>
SP_UUID / Eyecon.SP_KEY_ADVIDanalytics / adsas above

8. Secrets, tokens and key constantsh4

8.1 In Java source (compiled into the DEX)h5
SecretValueWhereRole
rc_keyPgdtSBeR0MumR7fOp620w6.C21273a:47e-auth-k auth header value; remotely overridable (section 3.3)
circus_key (remote default)PgdtSBeR0MumR7fOp236d5.AbstractC15804d:103the remote-config twin of rc_key
LocationIQ keypk.f8b1a66cf0f04c66264e7e72a00fac7fp379k5.RunnableC17750b:580LocationIQ API key, sent as key=
Singular app secretddddafd404a9a4d918bae58070b136d7 (app key instabridge_ffcbcb0b)p671y4.C21845b:72attribution
cid/token for social providersread from SP at runtimep620w6.RunnableC21275c:104-110Facebook token forwarded to addfbtoken.jsp
pk_enc_22048-bit RSA public keyres/values/strings.xml, used at p516r5.RunnableC19874r:191encrypts the grps/sgn payload
pk_enc2048-bit RSA public keyres/values/strings.xml, used at p284f7.AbstractC16468f0:362local APK-signature verification
cipher_key"Ewu"res/values/strings.xml, used at p379k5.RunnableC17750b:401Blowfish key prefix; the full key is R.string.cipher_key + <random 0..999 stored in SP "cipher_key"> + "sTQHGK" - used for local TTM message encryption
Google API keyAIzaSyCD1Lmwj_ln-h4RMLHApAZjuPZNWAzJi3MR.string.google_api_key / google_crash_reporting_api_keyGoogle services
Google app / project ids1:820458390093:android:93b50d6644b00155, gcm_defaultSenderId=820458390093, project_id=eyecon-app, default_web_client_id, an extra OAuth client id 820458390093-5bi3hdnbtrj6lugkjm0t1ot6sftk7cgq.apps.googleusercontent.comres/values/strings.xml, p614w0.AbstractC21232a:344Firebase / Sign-In
Facebook app id / client token1716660955257637 / afa3f38c55f24c73b177afc962969b15res/values/strings.xmlFacebook Login
Adjust tokenm23t9uwrtvy8R.string.adjust_app_tokenattribution
AppVestor980d4e3f-d057-4e5b-9034-1587edadf5a4 / 8c830f45-9f86-48d3-bcca-4e14fe2c1129R.string.app_vestor_*attribution
AppLovin SDK keyUZvHsQBqsDCf0qv_yVDKn0UeTmRknnQSgTT3HP7HFikqs8IxdD-7uWdloUQVGTn09CQ0LThxLm6tfMnKDFS9xaR.string.applovin_keymediation
Crackle/Mobitech key blobfai=1:917722272265:android:af67d7515574ed8c689ed8~~~fak=AIzaSyBxXQfYa0khnAZ2-NiK3WQV-4JbMWCw6ls~~~fpi=eyecon---eyecon-caller-id...R.string.crackle_keymediation (contains another Firebase key)
AdColony app idapp17ccb9995f3f46c682R.string.adcolony_app_idads
Amazon Ads key905c0238-13dd-4cf9-af3d-90f64672a4a9R.string.amazon_app_keyads
AdMob app idca-app-pub-2959190743815944~6092014645 plus ~30 ad-unit idsR.string.*, AbstractC15804dads
The Trade Desk UID2subscription_id TSqMwCWMkK, public_id UID2-X-P-MFkwEwYHKoZIzj0CAQ...AbstractC15804d:362 (uid2_config), consumed by p516r5.C19845p:75identity
Mobitech publisher keysEC170523APP, EC180623OAbstractC15804dad publisher keys
8.2 In res/values/strings.xml (secret-bearing string resources)h5
KeyValueUsed by
quotes_api_ide2eeb1aa9f32eb07fa04595a0c457ecb6fadb772p284f7.C16476j0:102 as Authorization: Token ... against api.paperquotes.com
cipher_keyEwuBlowfish key prefix for local TTM message encryption
pk_enc, pk_enc_2RSA public keys (see above)grps/sgn and local signature verification
google_api_key, google_crash_reporting_api_keyAIzaSyCD1Lmwj_ln-h4RMLHApAZjuPZNWAzJi3MGoogle
adjust_app_token, app_vestor_api_token, app_vestor_app_key, applovin_key, crackle_key, adcolony_app_id, amazon_app_key, facebook_app_id, facebook_client_token, admob_app_idsee 8.1attribution / ads
default_web_client_id, google_app_id, gcm_defaultSenderId, project_idsee 8.1Firebase
com.google.firebase.crashlytics.mapping_file_id7c6f28717cc9432e872d3e1b88cf99eaCrashlytics
8.3 In remote config (p236d5.AbstractC15804d, the compiled-in defaults)h5
KeyDefaultNotes
circus_keyPgdtSBeR0MumR7fOthe auth key
uid2_configTTD subscription + public ididentity token
analytics_worker_url"disabled_by_remote"call-log exfiltration endpoint
server_report_url""call-report endpoint
eyecon_remote_config_urlhttp://cloudfront-cdn-all.eyecon-app.com/configs/ab-test.txtcleartext config channel that also carries circus_key
store_download_url / store_url""store endpoints
dynamic_area_urlcards.eyecon-app.comcards host (no call site found)
call_contest_report_calls_urlhttps://apps.eyecon-app.com/call-contest/a/report-calls(no call site found)
ac_ads_test_internet_connection_urlhttps://www.google.comconnectivity probe
user_agentMozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 ... Chrome/123.0.0.0 Safari/537.36WebView UA for Google image search
balwan_recognizerJSON blocklist of fbcdn.net URL fragmentsphoto-cache poisoning defence
google_photos_link_loaderJSON with the Google search URL + injected JSphoto search
mobitech_*publisher keys and injected <script> HTMLads
toki_ports[5620,50000-50010]Toki UDP ports
welcome_back_notification_urlGoogle Form URLsurvey

9. Cleartext (non-TLS) transmission - consolidatedh4

#DestinationWhat is sentSeverity
1http://cloudfront-cdn-all.eyecon-app.com/configs/ab-test.txtnothing (a GET), but the response is the config map that contains circus_key, endpoint URLs and feature flags - an active MITM can rewrite every endpoint and the auth keyHIGH
2http://numberlookup.instabridge.com/respond-smscontact name, phone number, full SMS conversation, locale, usernameHIGH
3http://join.eyecon-app.com/configured as the Singular branded domain (deep-link / attribution)MEDIUM
4ptt.eyecon-app.com over plain UDPToki control JSON (incl. e-auth, e-auth-c, CLI) and raw voice mediaMEDIUM
5Global cleartext permissionandroid:usesCleartextTraffic="true" + <base-config cleartextTrafficPermitted="true">, and <debug-overrides> trusts user-installed CAsHIGH (enables interception of any endpoint if a future/remote value uses http)

10. Quick endpoint indexh4

#MethodURLCallbackAuthIssue
1GET/app/join.jspyesno (no CID yet)p365jd.RunnableC17614z
2GET/app/cli_auth/gettransportyesconditionalp365jd.RunnableC17614z
3GET/app/cli_auth/checkcodeyesconditionalp516r5.RunnableC19875s
4GET/app/social_auth/use_cli_validationyesyesp607vk.RunnableC21082e0
5GET/app/social_auth/validatetokenyesyesp607vk.RunnableC21093i
6GET/app/deleteme.jspyesyesp598v6.RunnableC20874a
7POST (gzip body)/app/addcontacts.jspqueueyesp620w6.C21280h
8POST/app/removecontacts.jspqueueyesp541s5.RunnableC20065s
9GET/app/getupdatedcontactsinfo.jspyesyesp607vk.RunnableC21093i
10GET/app/resetupdatedstatus.jsp (URL actually the base path)yesyesp607vk.RunnableC21082e0
11GET/app/getfreshpicsjson.jspyesyesandroidx.fragment.app.RunnableC2598c
12GET/app/getnames.jspyesyesRunnableC2598c, p599v7.RunnableC20908f
13GET/app/notifyaddcontact.jspnoyesas.RunnableC3316o
14GET/app/getcontactslang.jspnoyesp620w6.C21280h
15GET/app/picapproval.jspyes / queueyesp572u4.RunnableC20597o
16GET/app/getoptionalpics.jspyesyesp607vk.RunnableC21082e0
17GET (image)/app/pic?cli=...yesyesp620w6.C21280h.m38434g
18POST (multipart)/app/uploadpic.jspyesyesp620w6.C21280h
19GET x6/app/updateuserdetails.jspnoyesp620w6.RunnableC21275c et al.
20GET/app/addfbtoken.jspnoyesp620w6.RunnableC21275c
21GET (gzip)/app/mysettingsyesyesp620w6.RunnableC21276d
22POST/app/mysettingsyesyesp572u4.RunnableC20597o
23GET (gzip)/app/spamyesyesp607vk.RunnableC21082e0
24GET (gzip)/app/shareyesyesp598v6.RunnableC20880f
25POST (JSON)/app/sharenoyesp607vk.RunnableC21093i
26POST/app/spamreportqueueyesp599v7.RunnableC20908f
27GET/app/name_reportnoyesp544s8.RunnableC20081f
28POST (JSON)/app/social-mngrqueueyesp541s5.RunnableC20060n
29POST (RSA, text/plain)/app/grps/sgnqueueyesp516r5.RunnableC19874r
30POST (form)/app/shorten-urlyesyesp607vk.RunnableC21082e0
31GET/app/upgrade_pic2eyecon.jspnoyesp620w6.RunnableC21275c
32GET/app/cantalk.jsppollyesp474p5.*
33GET/app/getcantalkstatus.jsppollyesp474p5.RunnableC19300e
34GET/app/cantalkreceived.jspnoyesp474p5.AsyncTaskC19296a
35GET/app/cantalksaw.jspnoyesCanTalkActivity
36GET/app/sethistory.jspnoyesp607vk.RunnableC21088g0
37POST/app/eventreport.jspnoconditionalp607vk.RunnableC21129z
38GET/app/virality/viral_idyesyesp620w6.RunnableC21276d
39GET/app/virality/viral_id_checkyesyesp620w6.RunnableC21276d
40GET/app/virality/premium_statusyesyesp620w6.RunnableC21276d
41GET (gzip)/app/messages/getyesyesp620w6.RunnableC21276d
42POST (JSON)/app/messages/receivedqueueyesp620w6.RunnableC21278f
43GET (gzip)/app/get_store_productsyesyesp620w6.RunnableC21276d
44POST (JSON)voice2text/init-ttmyesyesp620w6.RunnableC21274b
45POST (JSON)voice2text/acknoyesp620w6.RunnableC21274b
46POST (JSON)voice2text/start-ttmyesyesp572u4.RunnableC20597o
47POST (JSON)voice2text/no-ttmnoyesas.RunnableC3316o
48GETvoice2text/getTextyesyesp620w6.RunnableC21276d
49GETvoice2text/ttm-get-tokens-amountyesyesp620w6.RunnableC21276d
50GETvoice2text/ttm-call-forwarding-resultyesyesp620w6.RunnableC21276d

Plus, referenced but never called directly: /app/pub/redirectme.jsp (embedded into shorten-url), /app/pub/su/<token> (synthesised from the shorten-url response).


11. Confidence / Unknownsh4

11.1 Things static analysis could NOT determineh5

Server-side behaviour

  1. No server code. Every schema, validation rule, rate limit, and business rule on api.eyecon-app.com, voice2text.eyecon-app.com and ptt.eyecon-app.com is unknown. Nothing here proves what the server checks - only what the client sends.
  2. E-Mark semantics. The app treats a 401 + E-Mark header as “this CID has been revoked”. Whether Eyecon also uses E-Mark for softer states (rate limiting, abuse detection, forced re-validation) is unknown. The cid response header on a 401 is read but its role is not documented anywhere in the client.
  3. Remote Config trust model. Whether circus_key (and therefore e-auth-k) can be changed through the cleartext eyecon_remote_config_url depends on a server-side allow-list that cannot be seen from the APK. Likewise, whether Firebase Remote Config values are signed and validated client-side could not be determined from the decompiled call sites alone.
  4. Which response fields are actually required. Where the client reads only a subset of a JSON object, additional server-side fields may exist and may be mandatory. For example cli_auth/gettransport clearly defines details{} variants, but whether other fields are always present is unknown.

Schemas not recovered

  1. getoptionalpics.jsp response - the consuming lambda was not decompiled; the JSON keys are unknown (only the DB columns it must populate are known).
  2. spam (list_version) response and mysettings GET response - the bodies are forwarded to callbacks whose parsers were not recovered.
  3. messages/get and messages/received payload schemas - only “a JSONArray of ids” and “a raw array” could be determined.
  4. get_store_products response and the getItem binary format.
  5. analytics_worker_url ingestion format beyond data.json; and whether the server also stores the multipart filename (the CLI).
  6. Toki UDP wire format - the byte-level packet layout is partially visible (15-byte handshake, XOR delta frames, 32-bit sequence in bytes 7..10) but the control-channel opcode table is not.

Client-side gaps

  1. addcontacts.jsp caller. p620w6.C21280h.m38428a(String, boolean) has no call site in the decompiled tree. Either the caller is in a class JADX failed to decompile, or the method is reached via a path JADX could not resolve. The contacts payload format is therefore inferred from the structurally identical removecontacts.jsp builder (p541s5.RunnableC20065s:92-96) - medium-high confidence, not certain.
  2. name_report argument order. p544s8.RunnableC20081f:52-53 sends ("name", str) and ("cli", buttonText), but at the call site str is the raw number and buttonText is the displayed name. Either the call site is inverted or the server’s parameter names differ from the client’s intent. Unresolved.
  3. resetupdatedstatus.jsp URL. The app requests the bare base URL https://api.eyecon-app.com/app/ while labelling the request "resetupdatedstatus.jsp". Either a genuine defect in this build or intentional (a root-level ping). Unresolved.
  4. dynamic_area_url / cards.eyecon-app.com and call_contest_report_calls_url / apps.eyecon-app.com are defined in remote config but have no call site in the decompiled tree. Either the calling code was not decompiled, or the keys are dead. The “Dynamic Area” subsystem (p672y5.C21856k, 146 KB of decompiled code) is heavily used and does read dynamic_area_url-shaped values indirectly, so the cards host is probably contacted, but the exact endpoint path could not be determined.
  5. OkHttp callTimeout exact value. C19225b0’s obfuscated field initialisers (10000, 10000, 10000, 60000, <const>) are consistent with connect/read/write = 10 s and an overall call timeout of 60 s, but the field-to-semantics mapping is an inference from OkHttp’s known Builder layout.
  6. Whether the reflective write to static final rc_key succeeds at runtime. This is ART-version dependent and cannot be confirmed statically.
  7. Value of ApiRequestSyncInterval (remote default 20) vs. apiWorkRepeatIntervalMinutes (60). The former is referenced in remote config but its reading site was not located; the retry cadence documented above uses the latter.
  8. Which of ServerRetryCount, share_video_list_sync_interval etc. are country-overridden. The remote-config helper (AbstractC15804d.m31082c) supports country-specific values, but the effective per-country overrides live server-side.
  9. Server-side validation of the grps/sgn RSA payload, cid issuance rules, join.jsp re-join rules (svr, sd=0), and the SERVER_BANNED_BY_TRAFFIC_ANALYZER heuristics. All unknown.
  10. Whether the quoted-channel (Toki) media is encrypted. No transport encryption is visible in the decompiled de-facto code; the Blowfish key (cipher_key + random + "sTQHGK") appears to be used for local TTM message storage, not for the wire.
11.2 Observation vs. inference summaryh5
ClaimStatus
Base URL https://api.eyecon-app.com/app/ and all 43 paths under itObservation (literals + call sites)
7 voice2text pathsObservation
Auth header names, the e-auth-c algorithm, the host gateObservation
rc_key remote override via reflectionObservation (code); the runtime effect is inference
Every parameter is percent-encoded and placed in the query string (except gzip POST / raw-body POST)Inference (from the OkHttp HttpUrl.Builder usage - high confidence)
Device fingerprint field list and originsObservation
addcontacts.jsp contacts payload formatInference from removecontacts.jsp (medium-high confidence)
getoptionalpics.jsp response schemaUnknown
Number of retries / retry intervalObservation (ApiRequestServerRetryCount = 3, apiWorkRepeatIntervalMinutes = 60)
Endpoint hostnamesObservation, except cards.eyecon-app.com and apps.eyecon-app.com whose call sites were not found
11.3 Method notesh5
  • Decompiled with JADX; JADX names, not original identifiers. Method-name hints such as "renamed from: w6.a" come from the JADX comments retained in the files.
  • Several JADX methods failed to decompile (notably androidx.fragment.app.RunnableC2598c.run(), whose raw Dalvik listing was recovered in a comment block and was used to document getnames.jsp / getfreshpicsjson.jsp).
  • com.facebook.appevents.* is application code, not the Facebook SDK (it imports both, but the class AbstractC9039h is Eyecon’s own Authenticator/AppUtils).
  • Third-party library packages were excluded from the endpoint inventory except where the app itself hard-codes an identifier or a URL.
  • No dynamic analysis, no packet capture, and no interaction with any Eyecon server were performed.

Part IV - Data and identityh2

Twenty-one database tables, the preference files that front them, and the complete lifecycle of the one identifier that matters.

The clientId chain took three passes to pin down and the reasoning is preserved here because the first two passes were wrong in instructive ways. The final answer is a single read site, two write sites, one preference key, one issuing endpoint. What made it hard was not obfuscation, it was that the same short method name appears in unrelated classes and grep alone leads you to the wrong one.

MICO (com.eyecon.global) — Data-Layer Referenceh3

App: com.eyecon.global (Eyecon / MICO caller-ID, spam blocking, contacts, SMS, second phone number, call recording) App version (build): 4.2026.04.23.1145 Source root: C:\Users\btc_e\Downloads\mico-analysis\src\sources Resources root: C:\Users\btc_e\Downloads\mico-analysis\src\resources Method: static analysis of a fully decompiled (JADX) ProGuard-obfuscated tree. Third-party libraries (androidx.*, com.google.*, kotlin.*, com.facebook.*, ad SDKs, etc.) are summarised but not treated as app data.


0. Executive summaryh4

ItemCount
Distinct SQLite database files (all)8
App-owned databases2 (contactdb + transient eyecon_restore_temp_database)
Tables in app-owned DBs21 in contactdb
Room databases1 declared (EyeconRoomDB) but dead code / not wired — see §3.1
Room DAOs (@Dao) with real SQL0 in app code (1 in Moloco SDK)
SQLiteOpenHelper subclasses (app)1 (p008a7.C0041b) + 1 overload used for restore
Schema migrations recovered1 → 57 (40 distinct migration steps)
App SharedPreferences keys documented~250 (170 literal SP_*/sp_* keys + ~80 unprefixed keys + 8 dynamic key families)
Native SharedPreferences files (app)3 (spam_prefs, after_call_feedback_prefs, default <pkg>_preferences)
App-owned DataStore0 (all DataStore usage is Firebase / DataTransport SDK)
On-device personal-data classescontacts, call history, SMS bodies, voicemail transcripts, call/audio recordings, contact photos, spam/block lists, notification contents

1. Database inventory (all databases found)h4

#Database file nameOwnerKindVersionLocationTables
1contactdbMICO app (p008a7.C0041b, original a7.b)SQLiteOpenHelper (raw SQL)57/data/data/com.eyecon.global/databases/contactdb21 (§2)
2eyecon_restore_temp_databaseMICO app (backup/restore staging copy of contactdb)SQLiteOpenHelper (same class, name overridden)57databases/eyecon_restore_temp_databasesame 21 (mirror)
3eyecon_temp_dbMICO app (rename target during restore)plain file copyn/adatabases/eyecon_temp_db—
4androidx.work.workdbAndroidX WorkManagerRoom (v23, autoMigrations)23databases/androidx.work.workdbWorkSpec, WorkName, WorkTag, SystemIdInfo, WorkProgress, Dependency, Preference, room_master_table
5com.google.android.datatransport.eventsGoogle DataTransport (FCM)SQLiteOpenHelper (ec.C16178l, original ec.l)7databases/com.google.android.datatransport.eventsevents, event_metadata, event_payloads, transport_contexts, log_event_dropped, global_log_event_state
6metrics-dbMoloco SDKRoom (com.moloco.sdk.acm.p221db.MetricsDb)1databases/metrics-dbevents
7com.amplitude.api / com.amplitude.api_<instance>Amplitude SDK (legacy bridge)SQLiteOpenHelper (p641x0.C21558f, original x0.f)4 (read-only)databases/com.amplitude.api*store, long_store, plus per-event tables (id, event)
8com.im_10.8.7.dbInMobi SDKSQLiteOpenHelper1databases/com.im_10.8.7.dbInMobi media cache
—download_record (Mbridge)Mintegral SDKSQL constant, helper stripped—Mbridge-manageddownload_record

Evidence of the restore DB (app uses a 4-arg constructor of the same helper):

// p282f5/C16434r.java:267
C0041b c0041b3 = new C0041b(MyApplication.f12163h, "eyecon_restore_temp_database", null, 57);
// p261e6/RunnableC16126b.java:124-131 (atomic swap during restore)
File databasePath = MyApplication.f12163h.getDatabasePath("contactdb");
File file = new File(databasePath.getParentFile(), "eyecon_restore_temp_database");
File file2 = new File(databasePath.getParentFile(), "eyecon_temp_db");
databasePath.renameTo(file2);
file.renameTo(databasePath); // if that fails -> revert

2. App database contactdb — full schema (v57)h4

Declared in p008a7/C0041b.java:

public C0041b(Context context) {
super(context, "contactdb", (SQLiteDatabase.CursorFactory) null, 57);
}

Singleton accessor: C0041b.m156t() (original a7.b.t()), context MyApplication.m10342d(). Wrapper helpers: m160D(timeoutMs) → writable DB + beginTransaction(); m161R(sql) → rawQuery; m162v() / m163y() → readable / writable with 5 s retry loops; m147Q(table, col, db) → PRAGMA table_info column-existence probe.

Column-name constants live in p008a7.AbstractC0039a (original a7.a), fields f133A … f278z1. All names below were resolved against that class’ static {} initialiser.

2.1 Table listh5
#TablePurposeSensitive?
1contactsMaster address book mirror + per-contact flags/scores/photosYES
2historyCall / event history (per contact)YES
3socialSocial-network (Facebook) ids linked to contactsYES
4fresh_picsServer-delivered “fresh photo” records + URLsMedium
5names_photos_cacheCached contact photo BLOBsYES
6recorded_notesVoice-note recordings indexYES
7sms_tableExternal SMS messages (body, sender)YES
8ttm”Talk To Me” voicemail text-to-speech transcript messagesYES
9block_listUser-blocked numbersYES
10spam_listSpam numbers reported by userYES
11db_table_notification_readerCaptured notification contents (message, sender, photo)YES
12db_table_reverse_lookup_idReverse number-lookup resultsMedium
13table_share_mediaShared-media (audio/video “cards”) playback stateLow
14table_api_requestsPersistent outbound HTTP API request queueMedium
15dynamic_area_ticketsDynamic-Area (“cards”) server ticket stateLow
16dynamic_area_subjectsDynamic-Area subject definitions + cron scheduleLow
17statistics_questionsSurvey/statistics question logLow
18shared_preference_v2Current app SharedPreferences backing storeYES
19shared_preferenceLegacy app SharedPreferences (Java-serialised HashMap BLOB)YES
20merge_contactsContact-merge candidate JSON blobsMedium
21merge_historyContact-merge history JSON blobsMedium
2.2 contacts (48 columns)h5
CREATE TABLE IF NOT EXISTS contacts (
ab_raw_id INTEGER ,
contact_id INTEGER,
cli TEXT ,
cli_in_server TEXT,
name TEXT,
is_pic_synced INTEGER DEFAULT 0,
update_date DATETIME DEFAULT NULL,
cli_type TEXT DEFAULT NULL,
is_primary INTEGER DEFAULT 0,
is_single INTEGER DEFAULT 0,
sent_to_server INTEGER DEFAULT 0,
is_survey_pic INTEGER DEFAULT 0,
survey_pic_url TEXT,
survey_pic_tag TEXT,
has_photo INTEGER DEFAULT 0,
contact_photo_uri TEXT DEFAULT NULL,
is_sub INTEGER DEFAULT 0,
is_viewer INTEGER DEFAULT 0,
os_type INTEGER DEFAULT 0,
score FLOAT DEFAULT 0.0,
last_action_date_long DATETIME DEFAULT NULL,
is_favorite INTEGER DEFAULT 0,
is_like INTEGER DEFAULT 0,
was_invited INTEGER DEFAULT 0,
pic_process_done INTEGER DEFAULT 1,
last_approved_tag TEXT,
facebook_id TEXT DEFAULT '0',
favorite_timestamp DATETIME DEFAULT NULL,
can_talk_status INTEGER DEFAULT 0,
can_talk_timestamp DATETIME DEFAULT NULL,
is_hash_updated INTEGER DEFAULT 0,
fresh_pic_init_tag TEXT DEFAULT NULL,
fresh_pic_init_url TEXT DEFAULT NULL,
default_sim_id TEXT DEFAULT '',
pinned_position INTEGER DEFAULT 0,
has_fresh_pic_init INTEGER DEFAULT 0,
custom_phone_number_label TEXT NOT NULL DEFAULT '',
pre_eyecon_usage FLOAT DEFAULT 0.0,
is_can_talk_enable INTEGER NOT NULL DEFAULT 0,
vn INTEGER NOT NULL DEFAULT 0,
DB_COLUMN_UNICODE_SCORE INTEGER NOT NULL DEFAULT 0,
eyecon_favorites_pinned_pos INTEGER NOT NULL DEFAULT -1,
account_type TEXT NOT NULL DEFAULT '',
default_cis TEXT NOT NULL DEFAULT '',
storage_photo_path TEXT NOT NULL DEFAULT '',
ab_photo_type INTEGER NOT NULL DEFAULT 0,
heart_ts DATETIME NOT NULL DEFAULT 0,
contact_action_date_mili DATETIME DEFAULT NULL,
fav_pin_pos INTEGER NOT NULL DEFAULT 2147483647,
extra_values_json TEXT NOT NULL DEFAULT '{}'
)

Notes

  • At schema v≤13 ab_raw_id was declared INTEGER PRIMARY KEY (the table was then rebuilt at v14-19 back to plain INTEGER).
  • Declared-but-unused constant: has_thumbnail_photo (AbstractC0039a.f208c0) is not in the table.
  • ROWID (f192V0) and rowid (f203a1) are pseudo-columns selected at runtime by several queries.

Indices

CREATE INDEX IDX_CONTACTS_CLI ON contacts (cli);
CREATE INDEX IDX_CONTACTS_SERVER_CLI ON contacts (cli_in_server);
CREATE INDEX IDX_CONTACTS_ID ON contacts (contact_id);
CREATE UNIQUE INDEX IDX_CONTACTS_CLI_NAME_ID ON contacts (cli_in_server, name, contact_id);
2.3 historyh5
CREATE TABLE IF NOT EXISTS history (
contact_id INTEGER ,
cli_in_server TEXT ,
cli TEXT ,
last_action_date TEXT ,
update_date DATETIME DEFAULT NULL,
action_type INTEGER ,
duration INTEGER NOT NULL DEFAULT -1 ,
has_photo INTEGER DEFAULT 0,
cli_type TEXT DEFAULT NULL,
name TEXT ,
account_id TEXT NOT NULL DEFAULT '' ,
is_spam INTEGER NOT NULL DEFAULT 0 ,
is_suspiciousSpam INTEGER NOT NULL DEFAULT 0 ,
has_name INTEGER NOT NULL DEFAULT 0 ,
participants TEXT NOT NULL DEFAULT '' ,
PRIMARY KEY (contact_id, cli_in_server)
)
CREATE INDEX unique_contact_id ON history (contact_id);

onCreate immediately follows with DELETE FROM history; (fresh install only).

2.4 socialh5
CREATE TABLE IF NOT EXISTS social (
contact_id INTEGER ,
cli_in_server TEXT ,
social_type INTEGER ,
social_id TEXT ,
social_name TEXT DEFAULT '0',
social_score INTEGER ,
social_source INTEGER DEFAULT 0,
social_last_change DATETIME DEFAULT NULL
)
2.5 fresh_picsh5
CREATE TABLE IF NOT EXISTS fresh_pics (
cli_in_server TEXT ,
contact_id INTEGER ,
name TEXT ,
pic_status_in_server INTEGER ,
big_pic_url TEXT ,
small_pic_url TEXT ,
fresh_pic_tag TEXT ,
pic_seen INTEGER DEFAULT 0,
received_date DATETIME DEFAULT CURRENT_TIMESTAMP ,
PRIMARY KEY (cli_in_server)
)
2.6 names_photos_cacheh5
CREATE TABLE IF NOT EXISTS names_photos_cache (
cli_in_server TEXT NOT NULL DEFAULT '',
data_type INTEGER NOT NULL DEFAULT 0 ,
data BLOB DEFAULT NULL ,
modify_time TIMESTAMP DEFAULT CURRENT_TIMESTAMP ,
PRIMARY KEY (cli_in_server, data_type)
)
2.7 recorded_notesh5
CREATE TABLE IF NOT EXISTS recorded_notes (
create_timestamp DATETIME DEFAULT CURRENT_TIMESTAMP,
contact_id INTEGER DEFAULT -1,
cli TEXT NOT NULL DEFAULT '',
cli_in_server TEXT NOT NULL DEFAULT '',
name TEXT NOT NULL DEFAULT '',
text_note TEXT NOT NULL DEFAULT '',
record_duration INTEGER DEFAULT -1,
call_direction INTEGER NOT NULL DEFAULT -1,
is_played INTEGER DEFAULT 0,
is_saved INTEGER DEFAULT 0,
file_name TEXT NOT NULL DEFAULT ''
)

Audio payloads themselves live on disk (RecordedNotes/ / RecordedCalls/, §7).

2.8 sms_tableh5
CREATE TABLE IF NOT EXISTS sms_table (
date_ms INTEGER DEFAULT 0,
no_read INTEGER DEFAULT 0,
body TEXT DEFAULT '', -- FULL SMS TEXT
sender TEXT DEFAULT '', -- phone number
message_id TEXT DEFAULT '',
sender_name TEXT DEFAULT ''
)
2.9 ttm (“Talk To Me” voicemail transcripts)h5
CREATE TABLE IF NOT EXISTS ttm (
cis TEXT NOT NULL DEFAULT '',
cli TEXT NOT NULL DEFAULT '',
text TEXT NOT NULL DEFAULT '', -- voicemail transcript text
message_id TEXT NOT NULL DEFAULT '',
time_ms DATETIME DEFAULT CURRENT_TIMESTAMP,
is_played INTEGER DEFAULT 0,
is_read INTEGER DEFAULT 0
)
2.10 block_listh5
CREATE TABLE IF NOT EXISTS block_list (
contact_id INTEGER DEFAULT NULL,
cli TEXT DEFAULT '',
cli_in_server TEXT DEFAULT '',
name TEXT DEFAULT '',
is_enable INTEGER DEFAULT 1,
create_timestamp DATETIME DEFAULT CURRENT_TIMESTAMP
)
2.11 spam_listh5
CREATE TABLE IF NOT EXISTS spam_list (
contact_id INTEGER DEFAULT NULL,
cli TEXT DEFAULT '',
cli_in_server TEXT DEFAULT '',
name TEXT DEFAULT '',
spam_type INTEGER DEFAULT 1,
create_timestamp DATETIME DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (cli_in_server)
)
2.12 db_table_notification_readerh5
CREATE TABLE IF NOT EXISTS db_table_notification_reader (
cis TEXT PRIMARY KEY,
name TEXT DEFAULT '' ,
essage TEXT DEFAULT '' , -- notification text (sic: missing 'm')
is_group INTEGER DEFAULT 0 ,
name_group TEXT DEFAULT '' ,
photo TEXT DEFAULT '' ,
time_ms INTEGER DEFAULT 0 ,
source INTEGER DEFAULT 0
)
2.13 db_table_reverse_lookup_idh5
CREATE TABLE IF NOT EXISTS db_table_reverse_lookup_id (
cis TEXT PRIMARY KEY,
name TEXT DEFAULT '',
time_ms INTEGER DEFAULT 0
)
2.14 table_share_mediah5
CREATE TABLE IF NOT EXISTS table_share_media (
url TEXT DEFAULT '',
provider TEXT DEFAULT '',
cis TEXT DEFAULT '',
is_play INTEGER DEFAULT 1 ,
is_seen INTEGER DEFAULT 1 ,
total_time_played_ms INTEGER DEFAULT 0 ,
time_ms DATETIME DEFAULT CURRENT_TIMESTAMP ,
type TEXT DEFAULT '' ,
server_id TEXT PRIMARY KEY DEFAULT '' ,
alias TEXT DEFAULT ''
)
2.15 table_api_requestsh5
CREATE TABLE IF NOT EXISTS table_api_requests (
url TEXT DEFAULT '',
json_key TEXT DEFAULT '',
server_retry_count INTEGER DEFAULT 0,
db_column_uuid TEXT DEFAULT ''
)
2.16 dynamic_area_ticketsh5
CREATE TABLE IF NOT EXISTS dynamic_area_tickets (
id TEXT NOT NULL DEFAULT '',
subject TEXT NOT NULL DEFAULT '',
had_user_interaction INTEGER NOT NULL DEFAULT 0,
local_ticket INTEGER NOT NULL DEFAULT 0,
max_appearances INTEGER NOT NULL DEFAULT 0,
appearances INTEGER NOT NULL DEFAULT 0,
order_value INTEGER NOT NULL DEFAULT 0,
last_seen TIMESTAMP DEFAULT 0 ,
screens TEXT NOT NULL DEFAULT '["all"]' ,
PRIMARY KEY (id)
)
2.17 dynamic_area_subjectsh5
CREATE TABLE IF NOT EXISTS dynamic_area_subjects (
type TEXT NOT NULL DEFAULT '',
subject TEXT NOT NULL DEFAULT '',
subject_description TEXT NOT NULL DEFAULT 'Has no description',
subject_path TEXT NOT NULL DEFAULT 'Has no path',
priority INTEGER NOT NULL DEFAULT 0,
max_per_session INTEGER NOT NULL DEFAULT -1,
absolute_position INTEGER NOT NULL DEFAULT -1,
first_position INTEGER NOT NULL DEFAULT 0,
steps INTEGER NOT NULL DEFAULT 1,
one_per_x_days REAL NOT NULL DEFAULT 0,
last_seen TIMESTAMP DEFAULT 0 ,
schedule_expression TEXT NOT NULL DEFAULT '* * * * *',
PRIMARY KEY (subject)
)
2.18 statistics_questionsh5
CREATE TABLE IF NOT EXISTS statistics_questions (
last_seen TIMESTAMP DEFAULT 0 ,
question_type INTEGER NOT NULL DEFAULT 0,
call_direction INTEGER NOT NULL DEFAULT 1,
duration INTEGER NOT NULL DEFAULT 1,
type TEXT NOT NULL DEFAULT '',
main_cis TEXT NOT NULL DEFAULT '',
main_cli TEXT NOT NULL DEFAULT '',
secondary_cis TEXT NOT NULL DEFAULT ''
)
2.19 shared_preference_v2 — current SharedPreferences backing storeh5
CREATE TABLE IF NOT EXISTS shared_preference_v2 (
key TEXT ,
data_type INTEGER , -- 0=String 1=Boolean 2=Integer 3=Long 4=Float 5=Double
data_part INTEGER , -- chunk index for long Strings
data TEXT DEFAULT NULL
)

Write path (p274eo/C16303b.m31713A): the whole table is DELETEd and re-inserted inside one transaction; String values are UTF-8 split into <=102400-byte parts. Read path (m31717e): SELECT * FROM shared_preference_v2 ORDER BY key, data_part ASC.

2.20 shared_preference — legacy SharedPreferences (v28+)h5
CREATE TABLE IF NOT EXISTS shared_preference ( data BLOB DEFAULT NULL )

A single row holds a Java-serialised HashMap (optionally GZIP-wrapped) chunked into 1000000-byte rows:

// p274eo/C16303b.m31718g (read)
c0041b.m161R("SELECT substr(data, " + i3 + ", 1000000) AS data FROM shared_preference");
// -> ObjectInputStream(new ByteArrayInputStream(bytes)).readObject()

WARNING (high severity): ObjectInputStream.readObject() on data stored inside the app-private SQLite file. The backup/restore flow swaps a raw contactdb file (section 7), so this is a Java-deserialisation gadget surface if the DB can be substituted.

2.21 merge_contacts / merge_historyh5
CREATE TABLE IF NOT EXISTS merge_contacts ( data TEXT NOT NULL DEFAULT '' )
CREATE TABLE IF NOT EXISTS merge_history ( data TEXT NOT NULL DEFAULT '' )

Single-column JSON-blob tables (serialised merge-candidate / merge-history lists).


3. DAOs / data-access layerh4

3.1 Room: declared but NOT usedh5

com/eyecon/global/Others/Objects/EyeconRoomDB.java:

@Database(entities = {AbstractC18543s.class}, exportSchema = false, version = 1)
public abstract class EyeconRoomDB extends RoomDatabase {
public abstract InterfaceC18541q m10353c(); // original m7.q.c
}

Findings

  • No Room.databaseBuilder(...) call anywhere references EyeconRoomDB — the class is never instantiated (dead code).
  • The declared “entity” p420m7.AbstractC18543s (original m7.s) is not an entity — it is a bitmap/Exif/ImageIO utility class with no @Entity annotation and no table fields.
  • The declared “DAO” p420m7.InterfaceC18541q is an empty interface with no @Dao annotation and no methods.
  • Conclusion: MICO does not use Room at runtime. All persistence is raw SQL through a7.b.
3.2 App data-access classes (the de-facto “DAO layer”)h5
ClassOriginalTables touchedStyle
com.eyecon.global.Contacts.Database.DBContactsDBContactscontacts, history, fresh_picsContentValues + insert/update/delete/replace on a7.b
p008a7.C0041ba7.ball 21DDL, migrations, rawQuery, transactions
p274eo.C16303beo.bshared_preference_v2, shared_preferenceSP persistence engine
p620w6.RunnableC21275c (ApiRequestDB)w6.ctable_api_requestsraw SELECT/UPDATE + WorkManager retry
p575u7.AbstractC20698s (SmsDB)u7.ssms_tablequery() + content-provider merge
p575u7.RunnableC20680b (ExternalSmsDB)u7.bsms_tablequery() + insertWithOnConflict(..., 5 /*CONFLICT_REPLACE*/)
p648x7.C21670q (SpamDB)x7.qspam_listraw SELECT
p623w9.RunnableC21301bw9.bspam_listraw SELECT
p260e5.C16102de5.drecorded_notesraw SELECT
p672y5.C21851fy5.fdynamic_area_tickets, dynamic_area_subjectsraw SELECT/delete
p672y5.C21856ky5.kdynamic_area_tickets, statistics_questionsraw SELECT
p284f7.RunnableC16473i (DynamicAreaDataLoader)f7.idynamic_area_ticketsraw SELECT + UPDATE
p379k5.RunnableC17750b (TtmDatabase)k5.bttmrawQuery("SELECT * FROM ttm")
3.3 Complete raw-SQL inventory (app code)h5
SQLLocationReturns / effect
PRAGMA table_info(<table>)a7.b.m147QColumn-existence probe
select ROWID as ROWID, cli_in_server, cli, name from contacts ORDER BY cli , has_photo ASC , cli , name COLLATE NOCASE DESCa7.b.m148SRe-numbers contact_id from ROWID (migration v36)
SELECT * FROM table_api_requestsw6.c:270Pending API queue -> ArrayList<C21650c>
UPDATE table_api_requests SET server_retry_count = server_retry_count + 1 WHERE db_column_uuid = ?w6.c:321/375/430int rows affected
DELETE FROM table_api_requests WHERE db_column_uuid = ?w6.c:347/401/456int rows deleted
DELETE FROM historya7.b.onCreate:515fresh-install wipe
UPDATE contacts SET score = call_count + sms_count + whatsapp_count + can_talk_counta7.b.onUpgrade v5migration
INSERT INTO contacts SELECT * FROM tmpa7.b.onUpgrade v<=13 / v<=19table-rebuild data copy
UPDATE contacts SET has_photo = 1 WHERE has_photo > 1v<16migration
UPDATE contacts SET has_fresh_pic_init = 1 WHERE fresh_pic_init_url IS NOT NULLv<18migration
UPDATE contacts SET <col> = '' / 0 / -1 WHERE <col> IS NULL (x7 columns)v<28null backfill
UPDATE contacts SET contact_action_date_mili = last_action_date_longv<36migration
UPDATE contacts SET fav_pin_pos = 2147483647 WHERE fav_pin_pos = 0v<41migration
SELECT 1 FROM shared_preference_v2 LIMIT 1eo.b:235post-delete sanity check
SELECT * FROM shared_preference_v2 ORDER BY key, data_part ASCeo.b:326full SP map (HashMap<String,Object>)
SELECT substr(data, N, 1000000) AS data FROM shared_preferenceeo.b:510legacy SP HashMap (deserialised)
query("sms_table", null, "message_id = ?", {id}, ...)u7.b:64Cursor
query("sms_table", null, null, ..., orderBy "date_ms DESC")u7.s:298ArrayList<C20678a>
insertWithOnConflict("sms_table", ..., CONFLICT_REPLACE)u7.b:84row id
select * from spam_list WHERE cli_in_server = '<cli>'x7.q:26C21657d or null
select * from spam_list order by cli_in_serverw9.b:281ArrayList<C21657d>
SELECT * FROM spam_list WHERE spam_type = 3 ORDER BY cli_in_serverw9.b:342Cursor
select * from recorded_notes order by create_timestamp DESCe5.d:274ArrayList<C16104f>
SELECT DISTINCT subject FROM dynamic_area_subjectsy5.f:171HashSet<String>
SELECT DISTINCT subject FROM dynamic_area_ticketsy5.f:203HashSet<String>
SELECT * FROM dynamic_area_ticketsy5.f:984Cursor
SELECT * FROM dynamic_area_subjectsy5.f:1077Cursor
SELECT COUNT(*) FROM dynamic_area_tickets LIMIT 1y5.k:185int
SELECT rowid, * FROM statistics_questions ORDER BY last_seen [LIMIT n OFFSET m]y5.k:1469/2304, e6.d:507ArrayList<C16445c>
SELECT <col> FROM dynamic_area_tickets WHERE <col> != -1 AND <col> = '<ticket>' LIMIT 1f7.i:149Cursor (count)
UPDATE dynamic_area_tickets SET <col> = abs(random() % 1000000) WHERE <col> = '<ticket>'f7.i:176randomise appearance bucket
SELECT * FROM ttmk5.b:435ArrayList<C18707f> (voicemail transcripts)
select 1a7.p0:204DB liveness probe
3.4 Contact-provider (read-only, not app storage)h5

The app reads — never writes — system providers from p516r5.C19854e, p261e6.C16128d, p284f7.C16483n:

  • ContactsContract.CommonDataKinds.Phone.CONTENT_URI -> starred, display_name, data4, data1
  • ContactsContract.Data.CONTENT_URI -> photo_uri, photo_thumb_uri; WhatsApp profile mimetype vnd.android.cursor.item/vnd.com.whatsapp.profile
  • ContactsContract.RawContacts.CONTENT_URI -> account_name, account_type
  • CallLog.Calls.CONTENT_URI -> full call log (Calls-Contest counters + call_logs backup archive)
  • Telephony.Sms.CONTENT_URI, content://sms/inbox, Telephony.MmsSms.CONTENT_CONVERSATIONS_URI, content://mms-sms/conversations/...

4. Migrations & schema-version history (contactdb 1 -> 57)h4

All in a7.b.onUpgrade(db, old, new); a helper m147Q(table, column, db) reads PRAGMA table_info so late migrations are idempotent. onDowngrade is empty (no-op -> potential crash on downgrade).

From -> ToOperation
1 -> 2ALTER TABLE contacts ADD COLUMN favorite_timestamp DATETIME DEFAULT NULL
2 -> 3ALTER TABLE contacts ADD COLUMN can_talk_status INTEGER DEFAULT 0; ADD can_talk_timestamp DATETIME DEFAULT NULL
3 -> 4CREATE TABLE social
4 -> 5ALTER TABLE contacts ADD COLUMN contact_photo_uri TEXT DEFAULT NULL
5 -> 6ALTER TABLE contacts ADD COLUMN score INTEGER DEFAULT 0; UPDATE contacts SET score = call_count + sms_count + whatsapp_count + can_talk_count
6 -> 7DELETE FROM social; set SP SP_REQUIRED_SERVER_CONTACTS_REFRESH = true
7 -> 8CREATE TABLE fresh_pics; ALTER TABLE contacts ADD COLUMN is_hash_updated INTEGER DEFAULT 0
8 -> 9ALTER TABLE social ADD COLUMN social_source INTEGER DEFAULT 0
9 -> 10ALTER TABLE contacts ADD COLUMN fresh_pic_init_url TEXT DEFAULT NULL; ADD fresh_pic_init_tag TEXT DEFAULT NULL
<=13 -> 14Rebuild contacts: RENAME TO tmp -> recreate with ab_raw_id INTEGER PRIMARY KEY -> INSERT INTO contacts SELECT * FROM tmp -> DROP TABLE tmp
<16 -> 15ALTER TABLE contacts ADD COLUMN default_sim_id TEXT DEFAULT ''; UPDATE contacts SET has_photo = 1 WHERE has_photo > 1
16 -> 17ALTER TABLE contacts ADD COLUMN pinned_position INTEGER DEFAULT 0
17 -> 18ALTER TABLE contacts ADD COLUMN has_fresh_pic_init INTEGER DEFAULT 0; UPDATE ... SET = 1 WHERE fresh_pic_init_url IS NOT NULL
<=19 -> 18Rebuild contacts again (same 32-column shape, PK removed)
19 -> 20ALTER TABLE contacts ADD COLUMN custom_phone_number_label TEXT NOT NULL DEFAULT ''
20 -> 21CREATE TABLE block_list
21 -> 22ALTER TABLE contacts ADD COLUMN pre_eyecon_usage INTEGER NOT NULL DEFAULT 0
22 -> 23+ is_can_talk_enable INTEGER NOT NULL DEFAULT 0
23 -> 24+ DB_COLUMN_UNICODE_SCORE INTEGER NOT NULL DEFAULT 0
24 -> 25+ eyecon_favorites_pinned_pos INTEGER NOT NULL DEFAULT -1
25 -> 26+ account_type TEXT NOT NULL DEFAULT ''
26 -> 27+ default_cis TEXT NOT NULL DEFAULT ''
27 -> 28CREATE TABLE shared_preference ( data BLOB DEFAULT NULL ) + 7 UPDATE ... WHERE col IS NULL backfills
28 -> 29ALTER TABLE history ADD COLUMN account_id TEXT NOT NULL DEFAULT ''
29 -> 30CREATE TABLE recorded_notes
30 -> 31ALTER TABLE recorded_notes ADD COLUMN is_saved INTEGER NOT NULL DEFAULT 0
<32 -> 31ALTER TABLE contacts ADD COLUMN storage_photo_path TEXT NOT NULL DEFAULT ''
32 -> 33+ ab_photo_type INTEGER NOT NULL DEFAULT 0
33 -> 34+ heart_ts DATETIME NOT NULL DEFAULT 0; ALTER TABLE history ADD is_spam, ADD has_name
<36 -> 35+ contact_action_date_mili DATETIME DEFAULT NULL; backfill from last_action_date_long; then a7.b.m148S() re-keys contact_id from ROWID
36 -> 37CREATE TABLE merge_history, CREATE TABLE merge_contacts
37 -> 38ALTER TABLE history ADD COLUMN participants TEXT NOT NULL DEFAULT ''
38 -> 39ALTER TABLE contacts ADD COLUMN vn INTEGER NOT NULL DEFAULT 0
39 -> 40CREATE TABLE names_photos_cache; + fav_pin_pos INTEGER NOT NULL DEFAULT 2147483647
40 -> 41UPDATE contacts SET fav_pin_pos = 2147483647 WHERE fav_pin_pos = 0
41 -> 42ALTER TABLE recorded_notes ADD COLUMN call_direction INTEGER NOT NULL DEFAULT 2147483647
42 -> 43CREATE TABLE spam_list; ALTER TABLE history ADD COLUMN is_suspiciousSpam INTEGER NOT NULL DEFAULT 0
<45 / ==45 -> 44CREATE TABLE dynamic_area_tickets, dynamic_area_subjects, statistics_questions; ALTER TABLE history ADD COLUMN duration INTEGER NOT NULL DEFAULT -1 (the ==45 branch re-runs these wrapped in try/catch)
46 -> 47ALTER TABLE dynamic_area_tickets ADD COLUMN order_value INTEGER NOT NULL DEFAULT 0
<50 -> 49ALTER TABLE dynamic_area_subjects ADD COLUMN subject_path TEXT NOT NULL DEFAULT 'Has no path'; ADD subject_description ... DEFAULT 'Has no description' (guarded by PRAGMA table_info)
50 -> 51CREATE TABLE table_share_media
51 -> 52dynamic_area_tickets + screens TEXT NOT NULL DEFAULT '["all"]'; statistics_questions + main_cli; contacts + extra_values_json TEXT NOT NULL DEFAULT '{}'
52 -> 53CREATE TABLE db_table_notification_reader
53 -> 54CREATE TABLE db_table_reverse_lookup_id
54 -> 55CREATE TABLE shared_preference_v2, CREATE TABLE table_api_requests, CREATE TABLE ttm
55 -> 56ALTER TABLE db_table_notification_reader ADD COLUMN source INTEGER DEFAULT 0
56 -> 57CREATE TABLE sms_table

Other schema-version facts

  • Room EyeconRoomDB = version 1 (unused).
  • DataTransport com.google.android.datatransport.events = version 7, linear migration list of 7 steps (0->7), defined in ec.C16178l.f39901e:
    1. create events / event_metadata / transport_contexts + events_backend_id + contexts_backend_priority indices
    2. transport_contexts + extras BLOB; new unique index; drop old index
    3. events + payload_encoding TEXT
    4. events + inline BOOLEAN NOT NULL DEFAULT 1; recreate event_payloads
    5. create log_event_dropped, global_log_event_state (+ seed row)
    6. events + product_id INTEGER
    7. events + pseudonymous_id TEXT, experiment_ids_clear_blob BLOB, experiment_ids_encrypted_blob BLOB
  • WorkManager androidx.work.workdb = version 23, migrations 1->2, 2->3, 3->4 … 21->22, autoMigrations 13->14, 14->15, 16->17, 17->18, 18->19, 19->20, 20->21, 22->23, plus fallbackToDestructiveMigration().

5. SharedPreferencesh4

5.1 Architecture — MICO does not use Android SharedPreferences for its own stateh5
ClassOriginalRole
p284f7.SharedPreferencesC16497yf7.yCustom SharedPreferences implementation. In-memory HashMap (f41007f), contains/getBoolean/getFloat/getInt/getLong/getString/getStringSet, m32085d(key) = atomic increment, static m32082b(default, key) getter, m32083c(key, default) = JSON getter. Backed by contactdb.shared_preference_v2, persisted on a background thread (eo.C16303b). registerOnSharedPreferenceChangeListener throws UnsupportedOperationException.
p284f7.SharedPreferencesEditorC16496xf7.xCustom SharedPreferences.Editor. m32076c(value,key), m32077d(key,boolean), m32078e(key,int), m32079f(key,json), m32080g(key,long), m32075b(key,max) = saturating counter, apply()/commit()/clear(). Writes are full-table rewrites.
MyApplication.m10348j() / m10347i()MyApplication.j() / .i()Global accessors for the SP instance / editor, created lazily.

Side effects of the custom SP

  • MyApplication.onCreate() clears the entire preference map on every launch unless f41010a says the load completed; otherwise it triggers a background load.
  • On SQLiteDatabaseCorruptException / SQLiteFullException the app calls ActivityManager.clearApplicationUserData() (full wipe) and System.exit(0).
  • SharedPreferencesC16497y registers a broadcast receiver for EYECON.INTENT_ACTION_DATA_WAS_UPDATED.
5.2 Native Android SharedPreferences files owned by the apph5
File name (MODE_PRIVATE)WhereKeys
spam_prefsp672y5.C21867v.m39107r()reportSpam, shareSpam — JSON dialog-throttle state (written by m39108u(type, key, text, buttonText))
after_call_feedback_prefsaf.RunnableC0127cafter_call_<state-lowercase> -> StringSet (per after-call feedback state)
<pkg>_preferences (PreferenceManager.getDefaultSharedPreferences)gf.AbstractC16733cEYECON.isSwitchedToNativeSP (Boolean), Locale.Helper.Selected.Language (String)

Third-party SP files present (not app data): com.google.firebase.*, com.google.firebase.inappmessaging, com.facebook.*, com.facebook.sdk.*, com.facebook.internal.*, amplitude-android-*, amplitude-events-*, amplitude-identify-intercept-*, amplitude-session-replay-replay-sequence, MessageAssistantStorage, odt_storage (DataTransport), frc_*_settings, __GOOGLE_FUNDING_CHOICE_SDK_INTERNAL__, NonNullPackage, FirebaseHeartBeat*, com.google.android.gms.appid.

5.3 Complete app key list (grouped by feature area)h5

170 distinct SP_* / sp_* literals + ~80 additional un-prefixed app keys + 8 dynamic key families. Unless noted, all are stored in contactdb.shared_preference_v2.

5.3.1 Lifecycle, install, registration, server synch6
KeyTypeInferred meaning
sp_install_timelongFirst install epoch-ms
SP_KEY_APP_FIRST_OPEN_TIMElongFirst app-open timestamp
SP_KEY_ORIGNALY_INSTALL_VERSIONStringFirst installed versionCode
SP_KEY_INSTALL_REFERRERStringInstall referrer
SP_KEY_app_instance_idStringPer-install instance uuid
SP_UUIDStringClient UUID sent to api.eyecon-app.com
last_app_v_code_v5intLast launched versionCode
current_eyecon_versionStringCurrent known client version
SP_KEY_IS_REJOINboolUser re-joined a contest after leaving
SP_REJOIN_OLD_GENERATED_CLIStringOld generated CLI kept for rejoin
SP_JOIN_VERSION / SP_JOIN_VERSION_CODEString/intVersion at contest join
sp_join_timelongContest join timestamp
SP_KEY_NOT_REGISTERED_WITH_REAL_CLI_V2boolRegistration done only with a generated number
SP_KEY_INIT_ADD_CONTACTS_COUNTintContacts added during onboarding
SP_REQUIRED_SERVER_CONTACTS_REFRESHboolForce server-side contacts refresh
SP_KEY_DID_UPDATE_V1453boolOne-off migration for build 1453
SP_KEY_STARTING_RESTORE_PROCESSboolRestore in progress
SP_KEY_LAST_TIME_BACKUPlongLast Google-Drive backup timestamp
SP_KEY_IS_AFTER_RESTOREboolPost-restore flag
SP_KEY_IS_AFTER_RESTORE_DA / SP_KEY_IS_AFTER_RESTORE_DA_STATSboolPost-restore Dynamic-Area flags
SP_KEY_SHOW_BACKUP_RESTORE_SUCCESS_DIALOGboolShow restore success dialog
SP_KEY_FIXED_RC_TIMESTAMPlongRemote-config timestamp fix
SP_KEY_LAST_EXIST_EVENT_TIMElongLast “exist” event time
SP_KEY_SYSTEM_EVENT_SENTboolSystem event already sent
Eyecon-InstallerNameStringDetected installer (store)
sp_isCalenderAvailableboolCalendar-app availability probe (constant in a7.a)
5.3.2 Caller ID / dialer / call screening / recordingh6
KeyTypeInferred meaning
SP_CALLER_ID_MODE-EYECONint/StringCaller-ID window mode
SP_DUAL_SIM_ALL_CONTACTS_DEFAULT-EYECON_v2StringDefault SIM for “all contacts” import
SP_EYECON_DEF_ACCOUNT_V2StringDefault phone account handle
SP_KEY_DEFAULT_PHONE_ACCOUNTStringSelected PhoneAccount id
SP_KEY_DEFAULT_NAVIGATION_APPStringPreferred nav app package
SP_KEY_RECORD_CALLS_MODEintCall-recording mode preference
SP_KEY_AUDIO_RECORDING_MODEintIn-call recording UI mode
SP_KEY_AUDIO_RECORDING_MODE_JSONString (JSON)Recording-mode config JSON
SP_KEY_AUDIO_RECORD_PROPERTIES_JSON_V4String (JSON)Recording codec/rate properties
SP_KEY_IS_CALL_RECORDING_ACTIVITY_ENABLEboolCall-recording screen enabled
SP_KEY_CALL_RECORDING_MODES_BUBBLE_SHOWNboolCoaching bubble shown
SP_KEY_DID_EVER_CLICK_BUBBLE_FOR_RECORDING_MODEboolBubble click marker
SP_KEY_LAST_CALL_RECORD_IDlongId of last recorded call
SP_KEY_BLOCK_SPAM_CALLSboolSpam-call blocking toggle
SP_KEY_DID_BLOCK_SYNC_V3boolBlock-list synced to server (v3)
SP_KEY_FIXED_BLOCK_MISSING_NAMEboolMissing-name block fix applied
SP_KEY_CANCEL_BLOCK_NUNBER_NOTIFICATIONbool”Cancel block number” prompt state
SP_KEY_DID_REPORTED_BLOCK_NUMBER_FIRST_BATCHboolFirst batch of reported blocks done
SP_KEY_FAKE_AUTHENTICATED_CLIStringTest/placeholder number while unauthenticated
SP_KEY_UNAUTH_CLI_WHILE_TYPINGboolAllow unauth CLI while typing
SP_KEY_VALIDATION_WITHStringWhich validation flow succeeded
SP_LAST_INCOMING_SMS_MSlongTimestamp of last inbound SMS
SP_KEY_LAST_SPAM_SYNClongLast spam-list sync
SP_KEY_VERSION_SPAM_LISTintSpam-list schema version
SP_KEY_SHOULD_SHOW_MISSED_CALL_ON_HISTORY_TITLEboolMissed-call badge on History title
SP_KEY_IS_MISSED_CALL_NOTIFICATION_ENABLEDboolMissed-call notification toggle
SP_KEY_ONE_TIME_IGNORE_MISSED_CALLSboolOne-shot ignore missed calls
AMOUNT_OF_MISSED_CALLS_FOR_HISTORY_BUBBLEintThreshold for missed-call bubble
SP_KEY_CLICKED_DRAW_ABOVE_GO_BTN / SP_KEY_CLICKED_ON_AUTO_START_GO_BTNboolDraw-over / auto-start onboarding clicks
SP_KEY_DID_BATTERY_OPT_INTENT_FAILEDboolBattery-optimisation intent failed
SP_KEY_BACKGROUND_SERVICES_NOT_WORKING / SP_KEY_SUSPICIOUS_BACKGROUND_SERVICES_NOT_WORKINGboolDiagnostics
SP_KEY_FOREGROUND_NOTIFICATION_MODEintForeground-service notification mode
SP_KEY_DID_CLICK_CALL_TTM_BTNboolTTM button clicked
SP_KEY_LAST_ROW_ID_WHATSAPP / SP_KEY_LAST_ROW_ID_WHATSAPP_W4BintWhatsApp contact sync cursor
SP_KEY_IS_TOKI_ENABLEDbool”Toki” (temporary number) enabled
MultiSimCallDialog_USER_CHOOSE_AS_DEFAULTboolMulti-SIM dialog result
SP_KEY_SYSTEM_EVENT_SENTbool(see 5.3.1)
5.3.3 SMS / external SMS / notification readerh6
KeyTypeInferred meaning
SP_KEY_DONT_ASK_ABOUT_SENDING_SMSboolSuppress “allow sending SMS” prompt
SP_SMS_PERMISSION_PROMPT_SHOWboolSMS permission prompt shown
sp_new_external_sms_in_serverboolNew external SMS pushed to server
SP_KEY_NEW_EXTERNAL_SMS_IN_SERVERboolConstant form of the above
trash_sms_numberString (JSONArray)Numbers whose inbox messages were deleted/hidden
SP_IS_SMS_WINDOW_ENABLE_BY_USERboolUser enabled the SMS window
isSmsValidEndedboolPhone-number (SMS-capable) validation finished
sendSmsEndedboolSMS send flow finished
5.3.4 Contacts, photos, address book, socialh6
KeyTypeInferred meaning
SP_KEY_AMOUNT_OF_CONTACTSintContact count
AMOUNT_OF_UI_CONTACTSintPage size for the contacts UI
AMOUNT_OF_UI_HISTORY_LOGSintPage size for the history UI
CELL_SIZE_FOR_FAVORITES_V3intFavourites grid cell size (v3)
SP_MAIN_VIEW_SORTING_MODEintContacts list sort mode
contactLangNeedUpdateboolContact language needs re-sync
SP_KEY_CURRENT_APP_LANGUAGE_v4StringSelected app language
SP_KEY_TOP_CONTACTS_LANGUAGEStringLanguage for Top-Contacts section
SP_KEY_PHONE_NUMBERS_CACHE_V4String (JSON)Cached phone numbers (v4)
SP_KEY_NEED_UPDATE_PROFILE_PICboolProfile picture needs updating
updatePicsByForceDoneboolForced photo sync completed
removeOldPhotosboolLegacy photo cleanup done
picfileStringPath to the current profile picture file
SP_KEY_PICTURE_SOURCEStringWhere the picked photo came from
SP_ALLOWED_TO_START_GIVING_PICS_BY_FORCEboolForce-photo hand-off allowed
SP_KEY_PRE_EYECON_AMOUNT_OF_PHOTOSintPhotos owned before Eyecon
SP_KEY_IS_GIVE_PRE_EYECON_USAGEboolPre-Eyecon usage flag
SP_INIT_FRESP_PICS_COUNTERintFresh-picture response counter
SP_TELL_SERVER_CONTACT_HAS_NO_PHOTOboolTold server a contact has no photo (constant in a7.a)
SP_KEY_LAST_TIME_REQUESTED_CARDSlongLast card/photo request
SP_KEY_SIM_CARDS_LIST_JA_CACHE_v2String (JSON)Cached SIM card list (v2)
SP_KEY_CONTACT_QUICK_GUIDE_JSON_V19String (JSON)Contact quick-guide content (v19)
SP_KEY_USER_EMAILStringSupport e-mail (from onboarding)
sp_eyecon_game_page_sizeint”Eyecon game” list page size
sp_social_installed_apps_v2String (JSON)Installed social apps (v2)
authenticatedCli / tempAuthenticatedCliStringAuthenticated / temporary caller number
tempUserNameStringTemporary display name
usernameserverStringServer-provided display name
userCountryISOStringISO country code
facebookId / facebookTokenStringFacebook identity for social contacts
SP_NEW_FACEBOOK_SOCIAL_CONTACT_ID / SP_NEW_FACEBOOK_SOCIAL_CONTACT_CLIString/intFacebook contact id / number
SP_NEW_SOCIAL_DIALOG_TYPEintSocial dialog variant
socialSrcStringSocial source tag
SP_KEY_DID_CLICK_SHARE_SOCIAL_BUBBLEboolShare-social coaching bubble click
SP_KEY_FID__LIST__FOR_NOT_SHOW_DIALOGString (JSON)Facebook ids for which to suppress the dialog
disappearingPhotosMAPV3String (JSON)Map of disappearing-photo state (v3)
5.3.5 Dynamic Area (“cards”), subjects, statisticsh6
KeyTypeInferred meaning
SP_KEY_DA_LAST_TAGSString (JSON)Last DA tag set received
SP_KEY_DA_LAST_BUNDLE_IDStringLast DA bundle id
SP_KEY_DA_NEED_PEEK_ANIMATION_V4boolPeek animation needed (v4)
SP_KEY_IS_DA_ENABLE_FOR_MAIN_SCREEN_V3boolDA enabled on main screen (v3)
EyeconDynamicArea.isUsedboolHas the user ever seen Dynamic Area
SP_KEY_LAST_TIME_UPDATE_STATS_v3longLast statistics update (v3)
da_contact_call_timelast_calllongLast “call” timestamp per contact
da_contact_call_timelast_unanswered_outgoing_calllongLast unanswered outgoing call
da_contact_call_timemissed_calllongLast missed call
da_new_photo_last_first_cardlongTimestamp of last first “new photo” card
da_permission_dismissed_<perm>boolDA permission prompt dismissed per role (see 5.3.14)
SP_KEY_ADD_.../SP_KEY_BLOCK_SPAM_CALLS—see 5.3.2
5.3.6 Premium / billing / store / themesh6
KeyTypeInferred meaning
SP_KEY_LAST_DATE_FOR_FREE_PREMIUMlongLast free-premium grant
SP_KEY_LAST_DATE_FOR_VIDEO_PREMIUMlongLast video-premium grant
SP_KEY_LAST_DATE_FOR_VIRAL_PREMIUMlongLast viral-premium grant
SP_KEY_LAST_FREE_PREMIUM_TYPEintType of last free premium
SP_KEY_LAST_PREMIUM_UPDATE_CACHEString (JSON)Premium state cache
SP_KEY_PREMIUM_OWN_CACHEString (JSON)Owned items cache
SP_KEY_PREMIUM_PURCHASES_INFO_CACHEString (JSON)Purchase info cache
SP_KEY_INAPP_PRODUCTS_CACHEString (JSON)In-app product catalogue cache
SP_KEY_STORE_OWN_ITEMSString (JSON)Store owned items
SP_KEY_STORE_PRODUCTS_IDS_JSONString (JSON)Store product ids
SP_KEY_SUPPORT_CACHE_OWN_CACHEString (JSON)Support-flow owned cache
SP_KEY_SHOW_PREMIUM_PRESENTboolShow the premium gift
SP_KEY_SHOW_TIME_LIMIT_BANNERboolShow time-limited premium banner
SP_KEY_WAITING_FOR_CUSTOM_THEME_PENDING_PURCHASEboolPending custom-theme purchase
SP_KEY_SELECTED_THEME_INFOString (JSON)Selected theme
SP_KEY_SELECTED_THEME_E2String (JSON)Selected theme (E2 experiment)
SP_KEY_SELECTED_THEME_BY_SETTINGS_DEFAULTboolTheme default came from settings
SP_KEY_IN_APP_UPDATE_COUNT_4.2026.04.23.1145intIn-app-update impressions for this build
SP_KEY_IN_APP_UPDATE_LAST_SHOWNlongLast in-app-update prompt
SP_KEY_MY_VIRAL_IDStringViral/referral id
SP_KEY_INVITER_IDStringInviter id
5.3.7 Calls contesth6
KeyTypeInferred meaning
SP_KEY_CONTESTS_JAString (JSON)Contests JSON
SP_KEY_CONTEST_INFOString (JSON)Current contest info
SP_KEY_CONTEST_NEED_UPDATE_IN_MY_SETTINGSboolContest data needs refresh
SP_KEY_CONTEST_NEED_UPDATE_IN_SERVERboolServer-side contest refresh
SP_KEY_CALLING_CONTEST_DAILY_COUNTintContested calls today
<name>_singular_aggr_counterintAggregated singular counter per name (p671y4.C21845b)
KeyTypeInferred meaning
SP_AFTERCALL_TYPEintAfter-call screen type
SP_AFTER_CALL_LOGSString (JSON)After-call log JSON
SP_KEY_AFTERCALL_LOGS_START_DATElongAfter-call logs window start
SP_KEY_SINGULAR_AFTERCALL_EVENT_HISTORY_JSONString (JSON)After-call event history
SP_KEY_LAST_HISTORY_LOGlongLast history-log row
SP_KEY_DID_LOVE_SURVEY_SHOWNbool”Do you love us” survey shown
SP_KEY_DONT_ASK_ME_AGAIN_TRACKERString”Don’t ask again” tracker JSON
SP_KEY_LEGAL_MESSAGE_SHOWN_V2boolLegal/privacy message shown (v2)
SP_KEY_DID_EYECON_CONSENT_APPROVEDboolAnalytics consent approved
5.3.9 Toki / Talk-To-Me (TTM) / WhatsApp / Telegram / quotesh6
KeyTypeInferred meaning
SP_KEY_LAST_INIT_TTM_JSONString (JSON)TTM init state
SP_KEY_PENDING_TTM_MESSAGESint/StringPending TTM messages
SP_KEY_TTM_TOKENS_AMOUNT_JSONSString (JSON)TTM token balances
SP_KEY_AMOUNT_OF_FULL_TOKI_SESSIONintFull Toki sessions used
SP_IS_WHATSAPP_WINDOW_ENABLE_BY_USERboolWhatsApp window enabled by user
IS_TELEGRAM_WINDOW_ENABLE_BY_USERboolTelegram window enabled
SP_WHATSAPP_BLURRY_SCREEN_DISPLAY_COUNTintWhatsApp blurred-screen counter
SP_KEY_START_DYNAMIC_ANIMATIONboolDynamic animation started
sp_key_start_id_plus_trail_time_mslongIdPlus “trail” popup start time
SP_START_TRAIL_POPUP_SHOWNboolTrail popup shown
SP_KEY_IS_CURRENT_QUOTES_LANGUAGE_SUPPORT_TTSboolQuotes language has TTS
SP_KEY_SELECTED_QUOTES_LANGUAGE_v1StringSelected quotes language
QuotesJson_v4 / NextQuotesUrl_v4String (JSON)Quotes cache + next URL
quotes indexintQuotes pagination index
5.3.10 AI assistant (auto-reply)h6
KeyType
SP_KEY_AI_ASSISTANT_INITIAL_MESSAGE_FOR_DEFAULTString
SP_KEY_AI_ASSISTANT_INITIAL_MESSAGE_FOR_MISSED_CALLString
SP_KEY_AI_ASSISTANT_INITIAL_MESSAGE_FOR_DECLINED_CALLString
SP_KEY_AI_ASSISTANT_INITIAL_MESSAGE_FOR_SPAM_CALLString
SP_KEY_AI_ASSISTANT_INITIAL_MESSAGE_FOR_UNKNOWN_CALLERString
SP_KEY_IS_AI_ASSISTANT_DEFAULT_AUTO_USE_ENABLEbool
SP_KEY_IS_AI_ASSISTANT_MISSED_CALL_AUTO_USE_ENABLEbool
SP_KEY_IS_AI_ASSISTANT_DECLINED_AUTO_USE_ENABLEbool
SP_KEY_IS_AI_ASSISTANT_SPAM_CALL_AUTO_USE_ENABLEbool
SP_KEY_IS_AI_ASSISTANT_UNKNOWN_CALLER_AUTO_USE_ENABLEbool
5.3.11 Onboarding / permissions / settingsh6
KeyTypeInferred meaning
SP_KEY_NEED_SHOW_ON_BOARDINGboolShow onboarding
SP_KEY_ON_BOARDING_SHORTCUT_SHOWboolOnboarding shortcut prompt shown
SP_IS_OPEN_MENU_E2boolE2 “open menu” experiment
SP_IS_PERMISSION_ASKED.v1boolPermission prompt already asked
last_permission_notification_timelongLast permission notification
SP_KEY_REG_PERMISSIONS_SUMMERYString (JSON)Permissions summary
permission_status_json / permissionDialogTimingStringPermission state/timing
SP_KEY_MY_SETTINGS_LOCAL_JSONString (JSON)Local settings snapshot
SP_KEY_MY_SETTINGS_SERVER_JSONString (JSON)Server settings snapshot
settings_versionintSettings schema version
SP_KEY_DONT_RECREATE_ACTIVITY_FOR_DARK_MODEboolDark-mode recreation guard
is_theme_changed_for_eventboolTheme changed -> re-raise event
SP_KEY_EYECON_REMOTE_CONFIG_JO_v2String (JSON)Remote-config JSON (v2)
5.3.12 Ads / remote config / analytics (app-owned)h6
KeyTypeInferred meaning
SP_KEY_AD_LIMITER_LOGSString (JSON)Ad-limiter log
app_open_ad_daily_count / app_open_ad_last_shown_timeint/longApp-open ad counters
interstitial_daily_count / interstitial_last_shown_timeint/longInterstitial counters
interstitial_free_uses_for_add_contactintFree interstitials for add-contact
interstitial_free_uses_for_photo_pickerintFree interstitials for photo picker
interstitial_free_uses_for_reverse_lookupintFree interstitials for reverse lookup
calls_since_last_ad / calls_since_last_app_open_adintAd cooldowns
eia_done / eia_call_counter / eia_page_view_counterbool/intExperiment-in-app counters
pp_bubble_menuPP_V15 / pp_bubble_settingPP_V15 / privacy_police_agreedPP_V15bool/intPaywall / privacy experiments (V15)
Eyecon.SP_KEY_ADVIDStringAdvertising id forwarded to server
eyecon.SP_KEY_IS_BETA_USERboolBeta cohort flag
Eyecon.isNeedAutoStartboolAuto-start still required
eyecon_comparator_bugboolComparator bug workaround
Eyecon-targetSdkVersionintTarget SDK the build was tuned for
fg_notification_enable_by_af_rateintForeground-notification AF rate
fb_notification_EYECON_TYPES_v2String (JSON)Facebook-notification categories seen
fb_notification_timestamp_<id> / fb_notification_timestamp_five_starslongPer-notification timestamps
fb_notification_cash_v2_<x>StringCash-reward notification state
EyeInAppMessage-<id>boolPer in-app-message dismissal state
EyeInAppMessageLastTimeShownlongLast in-app message shown
eyecon.TOPIC_SUBSCRIPTION-<topic>boolPer-topic push subscription
Eyecon.Intent.UsedboolDeep-link “already used” marker
SP_KEY_LAST_EXIST_EVENT_TIMElong(see 5.3.1)
5.3.13 Spam dialogs, security, misch6
KeyTypeInferred meaning
pref_key_report_spam_time_ms / pref_key_share_spam_time_mslongThrottle timestamps
is_from_reverse_lookupboolFlow origin marker
is_select_custom_backgroundboolCustom background selected
is_mac_updatedboolMAC/ID update marker
unregisteredboolRegistration state
LAST_CLEARED_TIMElongLast cache clear
LAST_COPIED_NUMBERStringLast number copied by the user
SP_KEY_EYECON2_NOTES / SP_KEY_EYECON2_REMINDERSString (JSON)Voice-note / reminder lists
sp_call_reminder_list / SP_CALL_REMINDER_LISTString (JSON)Call-reminder list (two keys, same purpose)
SP_BUSY_MSG_TEXT-EYECONStringBusy-message text
cipher_keyStringBlowfish key suffix for contact-photo encryption (see §8)
clientId / deviceName / osName / appVersionStringDevice fingerprint for API calls
exceptions_history / stacktraceStringCrash/log capture buffer
hasInternetConnection / no_internet / isCanceled / CB_ERROR / error_code_nummixedTransient callback payload keys
bubbleOptionsMain / bubbleOptionsInCallPopupString (JSON)Bubble options
do_product_detail_loadboolProduct-detail load trigger
shortcut_supported_event_sendboolShortcut event marker
reveres_lookup_event_sourceStringReverse-lookup event source
5.3.14 Dynamic key families (prefix + runtime suffix)h6
PatternBuilt inSuffix values
da_permission_dismissed_p033b6.EnumC3480mcaller_id, default_dialer, default_dialer_with_caller_id_priority, autostart, battery_optimization, draw_above, notification_reader
<name>_singular_aggr_counterp671y4.C21845bcontact / event name
fb_notification_timestamp_p672y5.C21851fnotification id
fb_notification_cash_v2_ir.AbstractC17448bvariant id
EyeInAppMessage-ad/in-app codecampaign id
eyecon.TOPIC_SUBSCRIPTION-push codetopic
after_call_af.RunnableC0127cafter-call state (lowercase)
SP_KEY_IN_APP_UPDATE_COUNT_<versionCode>in-app update codebuild version

6. DataStore / Proto DataStoreh4

No app-owned DataStore. All androidx.datastore usage belongs to bundled SDKs:

OwnerDataStore nameKind
as.C3363x1 (Google DataTransport scheduler)rTPhFaTmdRPreferences DataStore
p438nj.C18824g0 (Firebase Sessions)AbstractC18851u.f47916aPreferences DataStore
p483pj.C19364j (Firebase Sessions, 2nd instance)AbstractC18851u.f47917bPreferences DataStore
p000.C0000a / p483pj.C19368nFirebase session datastorePreferences DataStore
p028b1.C3401b (Amplitude)getDir("amplitude-kotlin-<instance>")file-backed dir (not DataStore)

No Proto DataStore (.proto serializer) is used by the app; the bundled .proto files (client_analytics.proto, messaging_event*.proto, logs/proto/firebase/inappmessaging/campaign_analytics.proto) belong to SDKs.


7. File-based storageh4

7.1 Internal app files (/data/data/com.eyecon.global/files/)h5
PathPurposeContents
.Eyecon/.nomediaHides files/.Eyecon from the media scannerAbstractC0039a.f201a
.Eyecon/temp_photo.jpgTemporary profile photo during pick/cropAbstractC0039a.f204b
Eyecon/ContactsPhotos/Cached contact photographs (C18517d.m34710a)Blowfish-encrypted blobs
Eyecon/RecordedNotes/Voice-note audio files (no-external-storage fallback)AMR audio
Eyecon/RecordedCalls/Call-recording audio files (fallback)AMR audio
notes/Backup staging: voice notesaudio
calls/Backup staging: call recordingsaudio
call_logs/Backup staging: call-log exporttext/CSV
address_book/Backup staging: address-book exporttext/CSV
ttm.zipBackup staging: TTM voicemail audio archivezip
eye_log.txtRolling app log (AbstractC0085x, p410lo.C18416a)logs, device info, app version, locale, install date
eye_log.zipZipped log bundle for support uploadlogs
image_provider/FileProvider share path (res/xml/image_share_filepaths.xml)shared images
com.crashlytics.settings.jsonCrashlytics (3rd party)json
*.eyecon.propertiesPer-process WebView/timer config (main., secondary., temp.)key=value
7.2 Cache (/data/data/com.eyecon.global/cache/)h5
PathContents
exceptions_and_logs.dataBuffered crash/exception log (AbstractC0085x:529)
instrument/Facebook SDK (3rd party)
lottie_network_cacheLottie animation cache
7.3 External public storageh5
PathPurpose
Environment.getExternalStoragePublicDirectory(DIRECTORY_DOCUMENTS)/Eyecon (MyApplication.f12172q)Base folder when external storage is mounted
.../Documents/Eyecon/ContactsPhotosContact photos
.../Documents/Eyecon/RecordedNotesVoice-note recordings
.../Documents/Eyecon/RecordedCallsCall recordings
MediaStore external_primary / external + relative_path = Documents/Eyecon/<type>API >=29 Scoped-storage registration of AMR recordings (AbstractC16098a.m31461d)

WARNING (high severity): recordings and contact photos are written to shared public external storage (Documents/Eyecon/...). Any app with READ_EXTERNAL_STORAGE (or MediaStore access on <=28) can read them. allowBackup="false" is declared in the manifest but only affects ADB/cloud backup, not this.

7.4 Databases directoryh5
PathPurpose
databases/contactdbMain DB
databases/eyecon_restore_temp_databaseRestore staging DB
databases/eyecon_temp_dbSwap-out during restore
databases/com.google.android.datatransport.eventsFCM event queue
databases/androidx.work.workdbWorkManager
databases/metrics-dbMoloco metrics
databases/com.amplitude.api*Amplitude legacy
databases/com.im_10.8.7.dbInMobi
databases/<name>-journal, -wal, -shmSQLite side-files
7.5 Other filesh5
Path / namePurpose
params.jsonWebView / AMP session params (ir.AbstractC17448b, p672y5.C21856k)
item_info.jsonDynamic-Area card item cache (p034b7.RunnableC3497d, p674y7.RunnableC21879g)
PersistedInstallation.<id>.jsonFirebase Installations
ttad_dirMintegral (Mbridge)
com.eyecon.global.fileproviderFileProvider authority; res/xml/file_paths.xml exposes the entire external/, external-files/, cache/, external-cache/ and files/ trees (path="." on every root)
7.6 Remote file storage (not local, but exfiltration-relevant)h5

Google Drive appDataFolder backup, files named:

eyecon_backup_for_database <- entire contactdb (all 21 tables incl. shared_preference_v2)
eyecon_backup_for_address_book <- address book
eyecon_backup_for_call_logs <- call logs
eyecon_backup_for_calls_records <- call recordings
eyecon_backup_for_notes_records <- voice notes
eyecon_backup_for_ttm_audios <- voicemail audio

Source: p282f5.AbstractC16422f.f40666B; staged locally as pending_temp_eyecon_backup_for_* (p282f5.C16442z).


8. Local personal / sensitive data inventoryh4

Data classWhere exactlySeverity
Full contact list (names, phone numbers/CLIs, contact ids, sim ids, social ids, labels, photo URIs, notes)contactdb.contacts (48 cols: cli, cli_in_server, name, facebook_id, default_cis, account_type, contact_photo_uri, storage_photo_path, custom_phone_number_label, extra_values_json, …)Critical
Contact photographs (encrypted blobs + decrypted cache files)contactdb.names_photos_cache.data (BLOB); files/Eyecon/ContactsPhotos/; Documents/Eyecon/ContactsPhotos/Critical
Call history (numbers, participants, duration, direction, timestamps, account id)contactdb.history (cli, cli_in_server, name, participants, account_id, duration, last_action_date)Critical
SMS message bodies + sender + sender name + message idcontactdb.sms_table (body, sender, sender_name, message_id); u7.b logs "Insert new ExternalSms: body: ... sender: ..."Critical
Voicemail / TTM transcripts + originating numbercontactdb.ttm (text, cli, cis)Critical
Call recordings & voice notes (audio files)Documents/Eyecon/RecordedCalls/, Documents/Eyecon/RecordedNotes/ (+ internal fallback); index in contactdb.recorded_notes (text_note, file_name, record_duration)Critical
Notification contents (sender, message text, sender photo, group name)contactdb.db_table_notification_reader (essage, name, name_group, photo, cis)High
Spam / block lists (numbers + names)contactdb.spam_list, contactdb.block_listHigh
Reverse number-lookup results (number -> name)contactdb.db_table_reverse_lookup_idMedium
Facebook social graph for contactscontactdb.social (social_id, social_name, social_type, social_source)High
All preference values — incl. authenticatedCli, tempAuthenticatedCli, tempUserName, userCountryISO, facebookId, facebookToken, SP_KEY_USER_EMAIL, SP_FCM_TOKEN_EYECON, SP_KEY_PHONE_NUMBERS_CACHE_V4 (JSON of numbers), SP_KEY_SERVER_UPDATED_WITH_ADVERTISING_ID, Eyecon.SP_KEY_ADVID, SP_KEY_SINGULAR_CUSTOM_USER_ID, SP_KEY_MY_VIRAL_ID, SP_KEY_INVITER_ID, trash_sms_numbercontactdb.shared_preference_v2 (and legacy shared_preference BLOB)Critical
Blowfish key material for contact-photo encryptionSP key cipher_key = Random().nextInt(1000); effective key = "Ewu" (R.string.cipher_key) + cipher_key + "sTQHGK" -> only 1000 possible keysHigh (crypto weakness)
Java-deserialised legacy preference mapcontactdb.shared_preference.data read via ObjectInputStream.readObject()High (deserialisation sink)
Full contact DB uploaded to Google DriveDrive appDataFolder file eyecon_backup_for_databaseHigh
Exposed file tree via FileProvidercom.eyecon.global.fileprovider with path="." on every root in res/xml/file_paths.xmlMedium
Device/app fingerprint in logsfiles/eye_log.txt, cache/exceptions_and_logs.dataMedium

Other security-relevant behaviours observed alongside the data layer

  • MyApplication.uncaughtException uploads a log bundle; on SP-load failure the app calls clearApplicationUserData() then System.exit(0).
  • eo.C16303b kills all other processes (Process.killProcess) on preference-load failure.
  • allowBackup="false" and fullBackupContent="false" are declared in the manifest (good), but external-storage recordings are unaffected.
  • Multiprocess: :notification_process, :restart_app.

9. Confidence / Unknownsh4

ItemConfidenceNote
contactdb name, version 57, and all 21 CREATE TABLE statementsHighRead directly from p008a7.C0041b.onCreate, with column constants resolved from p008a7.AbstractC0039a.
Migration chain 1 -> 57HighRead directly from onUpgrade; ordering of the if (i3 <= N) / if (i3 < N) guards reproduced verbatim. The exact intermediate column order of a v13 DB is only inferable because two rebuilds of contacts share a code path.
has_thumbnail_photo not being a real columnMedium-HighConstant declared in a7.a but never referenced by any DDL/DML in the tree.
Room DB is dead codeHighEyeconRoomDB is never referenced; AbstractC18543s is an image util class; InterfaceC18541q is empty. Caveat: if JADX mis-merged a class, a “real” entity may exist under another name — a smali-level check would confirm.
eyecon_restore_temp_database schema == contactdb v57HighConstructed with the same helper class and version constant (p282f5.C16434r:267).
Custom SharedPreferences persisted in contactdbHighConfirmed by eo.C16303b SQL and by the DB wipe on SQLiteFullException.
SP key list completenessMedium-High (~250 keys)Enumerated from string literals passed to the app SP accessors (m10347i/m10348j, m3207*, m3208*, get*/put*) across the whole tree, plus the complete SP_/sp_ literal set. Keys are missed where a key is assembled purely from runtime-computed strings with no literal prefix. Intent extras sharing a name with a pref (e.g. INTENT_KEY_*) were excluded.
Inferred meanings of SP keysLow-MediumKeys are named descriptively by the vendor; meanings are inferred from the key name plus the call-site context. No schema/metadata file ships with the app.
Raw-SQL inventoryHigh for app packages (a7,w6,f7,q7,u7,eo,e5,e6,x7,w9,y5,k5)Some obfuscated packages are shared between app and bundled SDKs; classes were classified as app-owned by their references to com.eyecon.global.
DBContacts (177 KB / 3549 lines) internal SQLLowDBContacts almost exclusively uses ContentValues + insert/update/delete/replace against contacts, history, fresh_pics; no literal SQL was found, so per-method SQL is inferred from the ContentValues keys rather than read.
Third-party DB schemasHigh for schema, Low for active useMbridge’s download_record DDL string exists but the helper that opens the DB appears stripped; may be dead code.
DataTransport DB version 7Highec.C16178l.f39900d = 7, instantiated with that version in ec.C16171e.
External-storage locationsMedium-HighBoth Documents/Eyecon/... and files/Eyecon/... forms appear, selected by AbstractC0042b0.m178a0() (storage-availability check; decompiled body not fully inspected).
Blowfish key weaknessHighRandom().nextInt(RoomDatabase.MAX_BIND_PARAMETER_CNT /* 1000 */) stored in SP as cipher_key; combined with the static R.string.cipher_key = "Ewu" and salt "sTQHGK".
Java deserialisation of the legacy SP blobHighObjectInputStream.readObject() at eo.C16303b.m31718g / m31714C. Exploitability depends on whether an attacker can substitute contactdb, which requires root/ADB backup-restore (blocked by allowBackup=false) or local app compromise.
No prebuilt .db / .sqlite in assets/Highassets/ contains only ad/SDK JS, fonts, dic/ and dexopt profiles.
Exact recorded_notes <-> on-disk file mappingMediumfile_name column + AbstractC16098a.m31460c(0|1) directory selection; the per-file naming scheme was not fully resolved.
Room @Entity of Moloco (events) column namesMediumColumn names inferred from Kotlin constructor parameter names (id, name, timestamp, eventType, nullable parent id, tags) since field names were renamed.

Generated by static analysis of the decompiled tree. No source files were modified.

Part V - Features and flowsh2

Twenty-nine feature modules and nine flows reconstructed end to end, from the UI entry point through the network call to the database write. This is the part that answers “what does this app actually do”, which is a different question from what it could do.

Read this section with the corrections in mind. Several flows were traced through code that a first pass had misattributed, and those are flagged inline.

MICO / Eyecon (com.eyecon.global) - Feature Catalogue & End-to-End Flow Reconstructionh3

Target: com.eyecon.global (Eyecon / MICO caller-ID, spam blocking, contacts, SMS, second phone number, call recording, AI assistant) Build: versionCode 749, versionName 4.2026.04.23.1145 · minSdk 23 · targetSdk 35 · compileSdk 36 Sources: C:\Users\btc_e\Downloads\mico-analysis\src\sources (+ ..\resources) Companion documents: BACKEND-API.md (every HTTP endpoint), DATA-LAYER.md (every DB/table/pref key).

Method. 100% static reading of the JADX decompilation of the shipped APK. No traffic was generated, no server was contacted, nothing was executed. Where a fact is not literally visible in the code it is labelled INFERRED.


0. How to read this documenth4

0.1 Observation / Inference conventionh5

Every non-trivial claim is tagged:

TagMeaning
OBSERVEDA literal string, a call site, a constant, a SQL statement, or a decompiled statement that exists verbatim in the code.
INFERREDMy reasoned conclusion from one or more observations. Confidence stated as high / medium / low.

Where JADX failed to decompile a method it leaves the raw register listing in a comment block (notably p646x5/C21639n.java, androidx/fragment/app/RunnableC2598c.java, p645x7-style synthetic runnables). Those listings are themselves primary evidence and are quoted here as JADX listing:.

0.2 Obfuscation recovery methodh5

The readable com\eyecon\global\ tree holds only ~207 thin wrappers. The real logic lives in short obfuscated packages (a7, w6, f7, q7, x5, …), which JADX re-emitted on disk under pNNN<name> directories (e.g. p008a7 = a7, p620w6 = w6, p284f7 = f7, p494q7 = q7, p646x5 = x5). Identity was recovered two ways:

  1. The /* JADX INFO: renamed from: <orig> */ comment above every class/member.
  2. The inline /* JADX DEBUG: Method not inlined, still used in: [...] */ comments, which name the original class of every call site.
  3. Semantic reading (log-tag strings such as "CallerID get name", "Server", "Authenticator", "EyeconBilling", "DriveHandler", "TalkiServer").
0.3 Resolved obfuscation package -> real identity maph5

(This is the key deliverable of the deobfuscation pass. “role” is my recovered name.)

On-disk dirOriginal pkgKey classes and recovered role
p008a7a7AbstractC0039a = Constants (all DB column names, API base URL f168L1, SP keys). C0041b = DatabaseHelper (contactdb, v57, 21 tables). AbstractC0042b0 = DeviceUtils (IMEI/MAC/SIM). AbstractC0062l0 = UiUtils/LocaleHelper. AbstractC0077t = NotificationUtils. AbstractC0085x = FileUtils/BitmapUtils/APK-signature collector (m467r0). RunnableC0070p0, RunnableC0074r0, RunnableC0081v = core bootstrap runnables / UDP reader thread.
p620w6w6C21273a = NetworkHelper (HTTP core, TAG NetworkHelper). C21280h = Server facade (all API entry points). RunnableC21275c = ApiRequestDB queue drain + misc (w6.c). RunnableC21276d = misc runnable (mysettings/get_store_products/virality/TTM). RunnableC21274b = TTM init/ack. C21280h.m38434g = caller-ID pic? URL builder.
p284f7f7SharedPreferencesC16497y = custom SharedPreferences impl. SharedPreferencesEditorC16496x = its Editor. C16464d0 = ServerMySettings (premium entitlement cache). C16489q = EmulatorDetector. C16491s/C16470g0 = contact model + binary-searchable list. C16458a0 = column-name value object. C16476j0 = quotes client (api.paperquotes.com). C16487? / C16477k etc. = misc.
p494q7q7EnumC19662h1 = ValidationTypes (mo_sms/so_sms/so_call/so_flash/huawei/fb/social_login/auto). EnumC19659g1 = JoinSources (JOIN, GET_ACTION, MO_TIMEOUT, MO_NOTIFICATION, MO_SENT_SMS, MO_START, GENERATE_CLI, IS_CODE_VALID, PHONE_AND_COUNTRY_CHECK, SO_TIMEOUT, GET_TRANSPORT, VALIDATION_TOKEN, FACEBOOK_LOGIN/GOOGLE_LOGIN). EnumC19653e1 = JoinErrors. C19673l0 = RegistrationActivity callback (q7.l0). C19677n = PhoneNumberValidationActivity callback (q7.n). C19665i1 = q7.i1 join-session state.
p262e7e7AbstractC16130b = AsyncCallbackBase (e7.b): m31516i()=onSuccess, m31515h()=onError, m31518o()=setResult, m32061b()=getResult, m32062c()=getIntResult, m32063d()=getExtras.
p365jdjdRunnableC17614z = jd.z join.jsp + cli_auth/gettransport.
p516r5r5RunnableC19875s = r5.s cli_auth/checkcode + block-contact dialog. C19854e = ContactProvider reader. C19862i = Contact model. C19864j = ContactDataLoader. C19856f = CallLogEntry. EnumC19861h0 = ActionTag enum. RunnableC19866k = contact-resume sync. RunnableC19874r = misc (data wipe, updateSgn, username push, ShareMediaJob schedule).
p647x6x6C21649b = ApiRequestDB facade (x6.b) - durable outbound request queue. C21651d/C21652e/C21653f = queued request kinds (FormPost / RawBodyPost / FormGet).
p648x7x7Spam UI+model: C21656c = ReportSpamDialog (x7.c), C21657d = SpamEntry (x7.d), C21670q = SpamDB (x7.q), EnumC21668o = SPAM/BLOCK enum (x7.o), C21661h = block/spam RecyclerView adapter (x7.h), C21675v = dialog binder (x7.v), C21654a = Report&Share dialog (x7.a).
p623w9w9RunnableC21301b = spam_list DB loader + ApiRequestDB writer (w9.b).
p646x5x5AI SMS assistant: InterfaceC21626a = Retrofit API (x5.a, POST respond-sms), C21639n = reply coroutine (x5.n), C21640o = start-flow coroutine (x5.o), C21641p = MessageAssistantRepository (x5.p), C21644s = MessageAssistantStorage (x5.s), AbstractC21633h = AutoUseOptionsHandler (x5.h), AbstractC21647v = start-flow enqueue (x5.v), C21634i/C21635j/C21636k = SMS message model + Assistant/Caller sender singletons (x5.i/j/k), C21631f = Moshi/Retrofit client factory (x5.f).
p636wowoC21474f = Retrofit container (wo.f) - builds http://numberlookup.instabridge.com/.
p554t5t5C20343m = PersonDataLoader (t5.m), C20337g = PersonNameCache (t5.g), RunnableC20341k = load runnable (t5.k), EnumC20336f = cache type (t5.f), C20340j = pic callback.
p397l5l5C18302h = CallerIdDataManager (l5.h), C18299e = Contact.
p541s5s5RunnableC20065s = removecontacts.jsp push (s5.s), RunnableC20051e = misc.
p572u4u4RunnableC20597o = picapproval.jsp, start-ttm, mysettings write, ad callbacks.
p575u7u7AbstractC20698s = SmsDB (u7.s), RunnableC20680b = ExternalSmsDB (u7.b).
p260e5e5AbstractC16098a = audio-file storage (e5.a), C16102d = AudioRecordsDataManager (e5.d), C16104f = RecordedNote model, RunnableC16111m = mega runnable (after-restore call-log rebuild, join variants).
p261e6e6RunnableC16126b = misc (e6.b): DB atomic swap during restore, permission dialogs, waitForServerUpdate. C16128d = StatisticsCalculator (e6.d).
p282f5f5Backup/Restore: AbstractC16422f = DriveHandler base (f5.f), C16426j = BackupRestoreDialog (f5.j), C16434r = DriveHandler restore (f5.r), C16442z = DriveHandler upload (f5.z), RunnableC16439w/RunnableC16429m = per-artifact worker threads.
p303g5g5C16707i = EyeconBilling (g5.i), C16708j = PurchasedProductInfo (g5.j).
p358j5j5C17487c = FreePremium/ViralPremium (j5.c), C17496k = PaidPremium (j5.k), C17485b = viral-id fetchers.
p687ylylC21952k = Talki UDP transport (yl.k) - 15-byte-header datagram protocol with FEC.
p633wlwlC21450m = TalkiSession (wl.m) - signalling JSON + audio capture/playback.
p340i7i7RunnableC17180l0 = i7.l0 misc (UDP connect, social removal).
p703z7z7C22122v0 = TokiFragment / Toki session UI (z7.v0).
p381k7k7C17791c = SyncAdapter (k7.c) - onPerformSync body is EMPTY (dead code).
p607vkvkRunnableC21093i = getupdatedcontactsinfo.jsp + social_auth/validatetoken + share + notifyaddcontact.jsp (vk.i). RunnableC21082e0 = social_auth/use_cli_validation, resetupdatedstatus.jsp, getoptionalpics.jsp, spam list. RunnableC21088g0 = sethistory.jsp.
p599v7v7RunnableC20908f = getnames.jsp (photo-picker), spamreport POST, AMR AudioRecord factory.
p448o4o4InterfaceC19056i = AdvertisingId provider (o4.i).
p418m5m5C18502p = DeviceInfo (m5.p: IMEI, SIM list). C18491e = call-state machine (m5.e). C18498l = misc.
p444o0o0C19066s = misc (o0.s).
p449o5o5AB-test / variant resolution.
p399l7l7C18312b = PhoneNumberUtil (l7.b): normalise, +-prefix, validity, country detection.
p673y6y6C21872a = query-parameter pair (y6.a).
p420m7m7C18533l = OkHttpClient, C18540p = ImageLoader (m7.p, m34736e/m34732a), AbstractC18543s = BitmapUtils.
p466oqoqKtor client (oq.c0, oq.e0, oq.u2…).
p032b5b5AbstractC3464e = Analytics/EventReporter (b5.e), C3465f = EventBuilder (b5.f).
p360j7j7C17534t/C17515a = PermissionRequestDialog (j7.t).
p720zqzqAbstractC22312g = PermissionUtil/Roles + random Toki port chooser (zq.g).
p236d5d5AbstractC15804d = RemoteConfig (d5.d) with ~200 compiled-in default keys; C15803c = RemoteConfig singleton.
p621w7w7C21295o = SocialMediaResolver (w7.o) - Facebook/Instagram/TikTok media scraping.
p612vsvsCrackle RTB mediation adapter (third-party, mixed in).
com.facebook.appevents.AbstractC9039happ code, not FacebookThe app-wide static utility (original short name h): identity getters, SP getters, crash handler, isEyeconUser() guard used in front of nearly every network call.
androidx.fragment.app.RunnableC2598capp code, not AndroidXgetnames.jsp caller-ID name/spam resolver (androidx.fragment.app.RunnableC2598c).

1. MODULE CATALOGUE (29 modules)h4

Format per module: Feature -> Key classes -> Delegates to (obfuscated) -> Endpoints -> DB tables -> Permissions. -- means “none found in the code”.

Note: the on-disk tree also contains a 30th package, com/eyecon/global/Sms/ (IndexableGridView, MySmsReceiver, SmsFragment, SmsJobService, SmsWindowActivity), which was not in the 29-module brief. It is the SMS inbox window + the legacy raw-PDU verification hook; it is covered inside Flow A.6 and Flow D, and its DB classes are u7.s (SmsDB) and u7.b (ExternalSmsDB).


1.1 Adsh5
  • Feature: Ad mediation shell. Native/interstitial/banner slots for After-Call, in-call, SMS window, contact reminder, notification reader, photo picker, feature dialogs; morning ad pre-warm.
  • Key classes: Ads/MorningAdRefreshJob (WorkManager job, remote-gated morning_ads_refresh_job), Ads/CustomMediaView, Ads/Google/FullscreenAds/FullscreenNativeAdsActivity.
  • Delegates to: p614w0 (ad-mediation orchestrator, GAM/AdMob unit selection), p572u4 (u4 ad loader queue), p572u4.EnumC20594l = layout type, p429n4 (Glide/ImageViewCompat), p380k6 (lottie).
  • Endpoints: — (all ad-unit ids come from Remote Config *_admob_*_ad_unit_id, *_google_combine_unit_id, Facebook placement ids; several default to the literal "disabled_by_remote"). Third-party ad SDK hosts only.
  • DB tables: — (may write table_api_requests indirectly if a queued request is used).
  • Permissions: INTERNET, ACCESS_NETWORK_STATE; POST_NOTIFICATIONS for the FGS.
  • Notes (OBSERVED): AdMob app id ca-app-pub-2959190743815944~6092014645; Crackle key in R.string.crackle_key.
1.2 AdsIB (“Ads In-Banner”)h5
  • Feature: A reusable native-ad template view (NAV_ad, headline, body/price, media, CTA) used inside scrolling lists (Menifa feed, home feed).
  • Key classes: AdsIB/TemplateView.
  • Delegates to: p572u4.EnumC20594l (layout variants), p429n4 (view-binding attrs).
  • Endpoints / DB / Permissions: —
  • Notes (OBSERVED): AdMob native unit /21825178605/GAM_IB_Aftercall_030925 and banner ca-app-pub-2959190743815944/3218439299, refresh ac_ib_refresh_time_ms = 30000.
1.3 AfterCallh5
  • Feature: The post-call screen. Shows caller identity, name suggestions, spam report / block / share actions, “AI assistant” CTA, ad slots, follow-up reminders, statistics.
  • Key classes: AfterCall/BaseAfterCallActivity (lockscreen-capable, turnScreenOn), CompactAfterCallActivity, DefaultAfterCallActivity, AfterCallWithNoAutoFinish, SplashInterstitialActivity, AftercallAdRefreshWorker.
  • Delegates to: p623w9.RunnableC21301b (native-ad view sizing, w9.b), p541s5.RunnableC20056j, p232d / AbstractC20000aM36883b (repeat-call dedupe), p397l5.C18302h (caller-ID data), p648x7.C21656c (report-spam dialog).
  • Endpoints: api.eyecon-app.com/app/sethistory.jsp (via p607vk.RunnableC21088g0, vk.g0), eventreport.jsp, getnames.jsp, pic?cli=.
  • DB tables: history, contacts, names_photos_cache, table_share_media.
  • Permissions: READ_CONTACTS, READ_CALL_LOG, POST_NOTIFICATIONS, SYSTEM_ALERT_WINDOW (lock-screen variant).
  • Remote gates: isAfterCallEnabled, after_call_visible_time, IncomingCallPrefetchMode ("when call answered" / "when ringing"), ac_disabled_when_repeat_calls, ac_disabled_when_min_call_duration_by_seconds, enableReminderForMissCall.
1.4 Analyticsh5
  • Feature: In-house event reporting, Firebase Remote Config consumption, and an opt-in call-log upload worker.
  • Key classes: Analytics/EyeconAnalyticsWorker (periodic WorkManager), Analytics/RemoteConfig/RemoteConfigUpdater, CountrySpecificConfig, RemoteConfigDebugTestReceiver (exported broadcast eyecon_remote_config_test).
  • Delegates to: p236d5.AbstractC15804d (d5.d, RemoteConfig singleton C15803c), p032b5.AbstractC3464e (b5.e, event reporter), p466oq (Ktor) + p620w6.C21273a.m38417a for the multipart upload.
  • Endpoints: eventreport.jsp (POST, JSON {name,type,attributes,timestamp} via p607vk.RunnableC21129z), api.eyecon-app.com/app/grps/sgn (RSA-encrypted APK-signature report), <analytics_worker_url> (Remote-Config-controlled multipart ZIP upload of the call log), http://cloudfront-cdn-all.eyecon-app.com/configs/ab-test.txt (cleartext A/B config), Firebase Remote Config / Crashlytics / Amplitude / Firebase Performance.
  • DB tables: reads CallLog.Calls (system provider); shared_preference_v2.
  • Permissions: READ_CALL_LOG (hard-gated - the worker returns retry() without it), INTERNET.
  • Notes (OBSERVED): the call-log worker is off by default - analytics_worker_url default is the literal string "disabled_by_remote" and the worker cancels itself in that case.
1.5 AudioRecordingh5
  • Feature: Call-recording and voice-note playback/list UI.
  • Key classes: AudioRecording/RecordingsFragment.
  • Delegates to: p260e5.C16102d (e5.d, AudioRecordsDataManager), p260e5.AbstractC16098a (e5.a, file/MediaStore placement), p260e5.C16104f (RecordedNote model).
  • Endpoints: — (recordings are local only; see Flow E).
  • DB tables: recorded_notes; reads MediaStore.Files on external/external_primary.
  • Permissions: RECORD_AUDIO, READ_EXTERNAL_STORAGE, WRITE_EXTERNAL_STORAGE.
  • Remote gates: is_recording_calls_enabled_v2, call_recording_mode (default 0), recording_calls_method_mode (default -1).
1.6 Backuph5
  • Feature: Google Drive appDataFolder backup + restore of the whole app database, recordings, notes, call-log archive and address book.
  • Key classes: Backup/BackupActivity, Backup/ProgressTracker, Backup/BackupOrRestoreException.
  • Delegates to: p282f5.AbstractC16422f (f5.f DriveHandler base), p282f5.C16426j (f5.j dialog), p282f5.C16434r (f5.r restore), p282f5.C16442z (f5.z upload), p282f5.C16418b0 (f5.b0 Drive REST wrapper), p261e6.RunnableC16126b case 4 (atomic DB swap).
  • Endpoints: GET api.eyecon-app.com/app/mysettings (to read the last-backup descriptor) and POST api.eyecon-app.com/app/mysettings (type=backup_info, key=<epoch-ms>, value={st_ts, account}) via p620w6.C21280h.m38440m -> p572u4.RunnableC20597o case 7. Google Drive REST (appDataFolder).
  • DB tables: whole contactdb file is copied; plus dirs files/calls, files/notes, files/call_logs, files/address_book.
  • Permissions: Google Sign-In scope https://www.googleapis.com/auth/drive.appdata, READ/WRITE_EXTERNAL_STORAGE, GET_ACCOUNTS.
  • Remote gate: backup_enable.
1.7 Billing / Premiumh5
  • Feature: Google Play Billing (v7.1.1) integration, premium tiers, free/viral premium, paywall WebView.
  • Key classes: Billing/Premium/AbstractActivityC5847a (base purchasing activity), PremiumPurchasingDefaultActivity, PremiumPurchasingPaywallUiActivity, GoldPremiumPurchasingActivity, PremiumUserActivity, FreePremiumUserActivity, GetFreePremiumActivity, Billing/Premium/WebViewPaywall/*, Billing/Store/StoreActivity.
  • Delegates to: p303g5.C16707i (g5.i, EyeconBilling), p303g5.C16708j (g5.j, PurchasedProductInfo), p358j5.C17496k (j5.k, PaidPremium server cache), p358j5.C17487c (j5.c, FreePremium/viral), p284f7.C16464d0 (f7.d0, ServerMySettings), p489q1 (Google Play BillingClient wrapper), p620w6.RunnableC21276d case 6 (get_store_products).
  • Endpoints: GET api.eyecon-app.com/app/get_store_products, GET api.eyecon-app.com/app/mysettings, POST api.eyecon-app.com/app/mysettings, GET virality/premium_status, GET virality/viral_id, GET virality/viral_id_check.
  • DB tables: shared_preference_v2 (via the custom SP), statistics_questions.
  • Permissions: com.android.vending.BILLING, BILL, com.android.vending.CHECK_LICENSE, INTERNET.
  • Remote gates: premium_products, premium_products_v2, premium_products_ids_list, premium_options_type, premium_ad_screen, premium_sub_sku, paywall_ui, registration_premium_location, golden_users_products, no_ad_*_product_id.
1.8 Callh5
  • Feature: Telephony plumbing: caller-ID foreground service, PHONE_STATE / NEW_OUTGOING_CALL receiver, “Talk To Me” (TTM) voicemail UI, SIM-card widgets.
  • Key classes: Call/CallService (foreground, specialUse FGS: “Used to tunnel incoming calls from a broadcast-receiver and determine if the caller ID should be shown to the user”), Call/CallServiceRestarterReceiver, Call/MyPhoneReceiver, Call/HistoryLogActivity, Call/TalkToMe/p204UI/*, Call/SimCardView, Call/EyeSimCardView.
  • Delegates to: p418m5.C18491e (m5.e call-state machine), im.RunnableC17334k (im.k state dispatcher), p379k5.RunnableC17750b (TTM DB k5.b), p620w6 (TTM endpoints), p405lg / p404lg.AbstractC18372n (notification channels).
  • Endpoints: voice2text.eyecon-app.com/{init-ttm,start-ttm,ack,getText,ttm-call-forwarding-result,ttm-get-tokens-amount,no-ttm}.
  • DB tables: ttm, history, contacts.
  • Permissions: READ_PHONE_STATE, READ_CALL_LOG, WRITE_CALL_LOG, PROCESS_OUTGOING_CALLS, RECORD_AUDIO, MANAGE_OWN_CALLS, ANSWER_PHONE_CALLS, FOREGROUND_SERVICE, POST_NOTIFICATIONS.
1.9 CanTalkh5
  • Feature: “Can I talk?” - an SMS-based availability handshake with a caller.
  • Key classes: CanTalk/CanTalkActivity (cantalk_q / cantalk_y / cantalk_n states, noHistory=true, singleInstance).
  • Delegates to: p474p5.AsyncTaskC19296a (p5.a0 network AsyncTask), p516r5 contact lookup.
  • Endpoints: cantalk.jsp (recvcli, q), cantalksaw.jsp (opened when the recipient opens the question), cantalkreceived.jsp, getcantalkstatus.jsp (via p474p5.RunnableC19300e).
  • DB tables: contacts (can_talk_status, can_talk_timestamp, is_can_talk_enable).
  • Permissions: SEND_SMS, READ_SMS, RECEIVE_SMS, READ_CONTACTS.
  • Notes (OBSERVED): contacts table columns can_talk_status / can_talk_timestamp (v2->3) and is_can_talk_enable (v22->23); getupdatedcontactsinfo.jsp propagates these back from other clients.
1.10 ContactReminderh5
  • Feature: “Contact them” reminder popup after a missed call / incoming call.
  • Key classes: ContactReminder/ContactReminderActivity, ContactReminder/ContactReminderWorker.
  • Delegates to: p491q4 (scheduling), p516r5 contacts, p358j5 premium.
  • Endpoints: — (local only; ad slots only).
  • DB tables: contacts, history.
  • Permissions: POST_NOTIFICATIONS, SYSTEM_ALERT_WINDOW, READ_CONTACTS.
  • Remote gates: enableReminderForMissCall, missedCallReminderDelayInMinutes ("60"), showAdsInContactReminder*.
1.11 Contactsh5
  • Feature: Address-book mirroring, local contact DB (DBContacts), contact chooser, contact creation/edit intent targets.
  • Key classes: Contacts/Database/DBContacts (the real data-access facade, ~3.6k lines), Contacts/ContactsChooserActivity, Contacts/NewContactActivity (handles INSERT/INSERT_OR_EDIT for contacts and text/x-vcard).
  • Delegates to: p516r5.C19854e (r5.e ContactsContract reader), p397l5.C18302h (l5.h CallerIdDataManager), p554t5.C20343m (t5.m PersonDataLoader), p607vk.RunnableC21093i / p620w6.C21280h (upload), p620w6.C21280h.m38434g (pic fetch).
  • Endpoints: addcontacts.jsp, removecontacts.jsp, getupdatedcontactsinfo.jsp, resetupdatedstatus.jsp, picapproval.jsp, getcontactslang.jsp, getoptionalpics.jsp, pic?cli=.
  • DB tables: contacts, history, social, fresh_pics, names_photos_cache, block_list, recorded_notes, spam_list, shared_preference_v2; reads ContactsContract.{Phone,Data,RawContacts}.
  • Permissions: READ_CONTACTS, WRITE_CONTACTS, GET_ACCOUNTS, AUTHENTICATE_ACCOUNTS (<=22), CAMERA, READ/WRITE_EXTERNAL_STORAGE.
  • See Flow F.
1.12 DefaultDialerh5
  • Feature: Default-dialer / call-screening integration: the full in-call UI, InCallService, CallScreeningService, and the AI SMS assistant.
  • Key classes: DefaultDialer/CallActivity (~151 KB, the in-call UI), CallStateService (InCallService, IN_CALL_SERVICE_UI=true, INCLUDE_SELF_MANAGED_CALLS=true), EyeconCallScreeningService, EyeDialerAvatarImageView, assistant/*.
  • Delegates to: p554t5.C20343m (person data + photo), p397l5.C18302h (caller-ID state), p418m5.C18491e (call state machine), p646x5.* (AI assistant), p430n5 (keypad), p648x7.C21656c (report spam).
  • Endpoints: pic?cli=, getnames.jsp, http://numberlookup.instabridge.com/respond-sms (cleartext), getoptionalpics.jsp.
  • DB tables: contacts, history, names_photos_cache, block_list, spam_list, recorded_notes.
  • Permissions: ANSWER_PHONE_CALLS, MANAGE_OWN_CALLS, CALL_PHONE, READ_CONTACTS, READ_PHONE_STATE, RECORD_AUDIO, READ_SMS, SEND_SMS, RECEIVE_SMS, BIND_INCALL_SERVICE / BIND_SCREENING_SERVICE (signature-level, system-granted), FOREGROUND_SERVICE_PHONE_CALL.
  • See Flows B, C, D.
1.13 DynamicAreah5
  • Feature: Server-driven “cards” / dynamic-area content injected into screens (onboarding tips, feature promos, surveys).
  • Key classes: DynamicArea/BaseCardViewLayout, CardsException, SwipeRefreshLayout.
  • Delegates to: p672y5.C21851f (y5.f DB), p672y5.C21856k (y5.k statistics DB), p284f7.RunnableC16473i (f7.i DynamicAreaDataLoader), p007a6 (RecyclerView plumbing).
  • Endpoints: cards.eyecon-app.com (Remote Config dynamic_area_url) - no direct call site found in this build; fetch is throttled by cards_update_threshold (default 120 min) in p516r5.RunnableC19875s default branch.
  • DB tables: dynamic_area_tickets, dynamic_area_subjects, statistics_questions.
  • Permissions: INTERNET; ad units if a card slot renders.
  • Notes (OBSERVED): dynamic_area_subjects.schedule_expression defaults to the literal cron string '* * * * *'.
1.14 IdPlush5
  • Feature: Notification reader (WhatsApp/Telegram message previews) + “IdPlus” entitlement screen + Telegram window overlay.
  • Key classes: IdPlus/NotificationReader/NotificationReaderService (BIND_NOTIFICATION_LISTENER_SERVICE, runs in :notification_process, disabled_filter_types=silent), NotificationBroadcastReceiver (internal eyecon.action_notification_posted), NotificationReaderWindowActivity, IdPlus/Telegram/TelegramWindowActivity, IdPlus/IdPlusFragment.
  • Delegates to: p252ds (notification extraction), p359j6.C17513b (Telegram), p314h6.C16927j / C16920c (thread pools), p260e5 (recorded-note rows for message attachments).
  • Endpoints: getnames.jsp (source=PhotoPicker), pic?cli=.
  • DB tables: db_table_notification_reader, contacts, recorded_notes.
  • Permissions: notification-listener grant (user-granted), SYSTEM_ALERT_WINDOW, READ_CONTACTS, POST_NOTIFICATIONS.
  • Remote gates: ask_notification_reader_permission_in_registration, showAdsInNotificationReader*.
  • Notes (OBSERVED): on WhatsApp notification removal the service parses the title and can synthesise a fake EXTRA_STATE_RINGING broadcast with TYPE_CALL_WHATSAPP=true (i.e. treat an outgoing WhatsApp call notification as a call event).
1.15 InCallCallerIdh5
  • Feature: The “full-screen incoming-call caller ID overlay” (EyeconChromiumException is a WebView renderer crash wrapper).
  • Key classes: InCallCallerId/EyeconChromiumException.
  • Delegates to: androidx.webkit Chromium render-process handling; the overlay itself is rendered inside CallActivity + incall_caller_id_scene_new.xml.
  • Endpoints: pic?cli=, getnames.jsp (via the shared person loader).
  • DB tables: contacts, names_photos_cache.
  • Permissions: SYSTEM_ALERT_WINDOW, ANSWER_PHONE_CALLS.
  • Remote gates: caller_id_mode_v2 (default 3), show_caller_id_before_data, enable_callerid_show_battery_opt, IncomingCallPrefetchMode.
1.16 MainScreenh5
  • Feature: The app shell: launcher/dialer activity, tabbed contact list (Favorites / For You / History), search, menus.
  • Key classes: MainScreen/MainActivity (also registered as the ACTION_CALL / ACTION_DIAL / RECENT_CALLS handler, and as the launcher via com.eyecon.global.pink|blue|green activity-aliases), MainScreen/MainFragment, Communication/EyeSearchActivity, Communication/ContactListInfoArea, Communication/ContactGridTextArea, Communication/Favorites/FavoritesFragment, Communication/ForYou/ForYouFragment, Communication/History/HistoryFragment.
  • Delegates to: p516r5 contacts, p397l5.C18302h, p646x5 (assistant entry points), p703z7 (Toki entry), p621w7 (social share), p575u7 (SMS window).
  • Endpoints: getnames.jsp, pic?cli=, getfreshpicsjson.jsp, getoptionalpics.jsp, messages/get, share.
  • DB tables: contacts, history, fresh_pics, table_share_media, db_table_reverse_lookup_id.
  • Permissions: READ_CONTACTS, READ_CALL_LOG, READ_SMS, SEND_SMS, CALL_PHONE, POST_NOTIFICATIONS.
  • Remote gates: com_start_page, com_favorites_default_style, com_for_you_default_style, com_history_default_style, com_title_*.
1.17 Menifah5
  • Feature: Contact detail page (the “Menifa” card view): name, photo, actions (call/SMS/WhatsApp/Toki/notes/calendar), spam report / block / share, name suggestions, reverse lookup, photo picker.
  • Key classes: Menifa/MenifaFragment (~7.2k lines).
  • Delegates to: p554t5.C20343m (person load), p648x7.C21656c (report dialog) and C21654a (share dialog), p648x7.C21657d (spam entry), p599v7 (getnames.jsp photo-picker name harvest), p621w7 (share endpoint), p703z7 (Toki), p260e5 (record a note).
  • Endpoints: getnames.jsp (source=PhotoPicker, is_search=true), share, picapproval.jsp, pic?cli=, getoptionalpics.jsp, spamreport.
  • DB tables: contacts, history, names_photos_cache, fresh_pics, block_list, recorded_notes, social.
  • Permissions: READ/WRITE_CONTACTS, CAMERA, READ_EXTERNAL_STORAGE, READ/SEND_SMS, RECORD_AUDIO, CALL_PHONE.
  • Notes (OBSERVED): MenifaFragment.mo721c0 maps spam_type -> isSpam / isSuspiciousSpam using x7.d.m38861e(t) (=1 or 3) and x7.d.m38862f(t) (=2).
1.18 MoreMenuAndSettingsh5
  • Feature: Settings hub, profile editor, about, third-party licenses, theme selection entry points.
  • Key classes: MoreMenuAndSettings/SettingActivity (deep-link eyecon_settings://), MoreSettingsFragment, MyProfileActivity, UploadProfileWorker, AboutActivity, LicensesActivity, LicenseItem, C5848a.
  • Delegates to: p398l6 (language picker), p360j7 (permission dialog), p282f5 (backup entry), p303g5 (premium), p620w6 (upload), p674y7 (themes), p574u6 / p438nj.
  • Endpoints: uploadpic.jsp (multipart, msisdn = SharedPreferences["clientId"]), updateuserdetails.jsp (username=...), mysettings.
  • DB tables: shared_preference_v2, contacts.
  • Permissions: CAMERA, READ/WRITE_EXTERNAL_STORAGE, WRITE_SETTINGS, REQUEST_IGNORE_BATTERY_OPTIMIZATIONS, INTERNET.
  • Notes (OBSERVED): MoreMenuAndSettings/LicenseItem.java -> p598v6 (third-party licence text store).
1.19 Networkh5
  • Feature: Durable outbound HTTP request queue.
  • Key classes: Network/ApiRequests/ApiRequestWorker (WorkManager), plus the whole queue in p647x6 / p620w6.
  • Delegates to: p647x6.C21649b (x6.b), p620w6.RunnableC21275c case 6 (insert) and case 12 (drain).
  • Endpoints: any URL enqueued; drained by C21273a.m38423g/m38425i.
  • DB tables: table_api_requests (url, json_key, server_retry_count, db_column_uuid).
  • Permissions: INTERNET, ACCESS_NETWORK_STATE.
  • Notes (OBSERVED): the serialised JSON contains urlStr, typeRequest (1=raw-body, 2=form-POST, 3=form-GET), isCameFromReg, requestMethod, jsonExpectedResult, endPoint, dataName, contentType, requestJsonBody, addAutoHeaders, gzipEnable, timeout, responseCode[], params[], headerValuesMap, headerResult. Retry = UPDATE ... server_retry_count = server_retry_count + 1.
1.20 Othersh5
  • Feature: Application object, bootstrap, lifecycle, crash handling, generic views, misc tasks.
  • Key classes: Others/MyApplication, Others/Objects/EyeconInitProvider (initOrder=101), EyeconRoomDB (dead), Others/Receivers/{BootStartReceive,OnPackageReplaceReceiver,OnDataClearedReceiver,MainProcessReceiver}, Others/Tasks/{DailyWorker,ForegroundLauncherService,JobsService,NotificationsWorker}, Others/Views/* (~35 custom views).
  • Delegates to: p008a7 (bootstrap runnable RunnableC0074r0), p315h7.C16945i (thread pools), p032b5.AbstractC3464e (crash analytics), p620w6, p623w9, p648x7 (DailyWorker does the spam-list sync).
  • Endpoints: spam (list_version), notifyaddcontact.jsp via bm.RunnableC3701s3, eventreport.jsp.
  • DB tables: all (via DailyWorker + DBContacts).
  • Permissions: RECEIVE_BOOT_COMPLETED, WAKE_LOCK, KILL_BACKGROUND_PROCESSES, REORDER_TASKS, FOREGROUND_SERVICE, POST_NOTIFICATIONS.
  • Notes (OBSERVED): EyeconRoomDB is declared but never instantiated (dead code, see DATA-LAYER.md section 3.1). MyApplication also installs itself as the process-wide UncaughtExceptionHandler and overrides startActivity/startService/startForegroundService (the latter is a no-op when POST_NOTIFICATIONS is missing).
1.21 Permissionsh5
  • Feature: The consolidated runtime-permission + Android-role requester used everywhere.
  • Key classes: Permissions/ExtendedPermissionPromptActivity (launched from RegistrationActivity.m10525M0 when permission_prompt_text == "long").
  • Delegates to: p360j7.C17534t (j7.t PermissionRequestDialog) and C17515a (permission+role combined dialog), p720zq.AbstractC22312g (zq.g checks + role requests).
  • Endpoints: — (per-request telemetry to eventreport.jsp via AbstractC3464e.m3662m("permissions_and_roles_dialog_show", ...)).
  • DB tables: shared_preference_v2 (SP_IS_PERMISSION_ASKED.v1).
  • Permissions: everything in the manifest; role android.app.role.CALL_SCREENING, android.app.role.DIALER, android.app.role.SMS.
  • Remote gates: addRecommendedTextInPermissionDialog, allow_asking_role_multiple_times_in_a_row.
1.22 PhoneAccounth5
  • Feature: An AccountManager account type “Eyecon” with a SyncAdapter and a contacts RawContacts structure - but the sync is a no-op.
  • Key classes: PhoneAccount/SyncService, PhoneAccount/EyeconAccountAuthService, PhoneAccount/AccountWithDataSet.
  • Delegates to: p381k7.C17791c (k7.c, AbstractThreadedSyncAdapter), p321hd.C16961a (Parcelable CREATOR), p381k7.C17789a.
  • Endpoints: none (OBSERVED: C17791c.onPerformSync(...) has an empty body).
  • DB tables: reads ContactsContract.RawContacts with ?limit=1 (existence probe only).
  • Permissions: AUTHENTICATE_ACCOUNTS (<=22), GET_ACCOUNTS, WRITE_SYNC_SETTINGS.
  • XML: res/xml/eyecon_account_sync_adapter.xml, eyecon_contacts_account.xml, eyecon_contacts_account_auth.xml.
1.23 Photosh5
  • Feature: Photo picker, reveal animation, zoom viewer, and the “photo tracking” worker.
  • Key classes: Photos/PhotoPicker/PhotoPickerActivity, Photos/PhotoRevealAnimationView, Photos/ZoomImageView, Photos/PhotosTrackerWorker.
  • Delegates to: p599v7 (media resolution), p621w7 (Facebook/Instagram scraping), p380k6.C17781s, p260e5.C16102d (e5.d, recorded-note rows), p429n4 (Glide).
  • Endpoints: getnames.jsp (source=PhotoPicker, is_search=true), uploadpic.jsp, pic?cli=, Google Images search?hl=en&udm=2&q= (reverse image search, Remote Config google_photos_link_loader), api.paperquotes.com, www.facebook.com / www.instagram.com / www.tiktok.com media.
  • DB tables: names_photos_cache, contacts (storage_photo_path, ab_photo_type, has_photo, fresh_pic_init_url/tag), fresh_pics.
  • Permissions: CAMERA, READ/WRITE_EXTERNAL_STORAGE, INTERNET.
  • Remote gates: isPhotosTrackerEnabled, photo_picker_ads_mode, photo_picker_ad_unit_id, server_data_cache (photo/name expiry, default 604800000 ms).
1.24 Pushh5
  • Feature: FCM/HMS push reception and token registration.
  • Key classes: Push/PushService (com.google.firebase.MESSAGING_EVENT + com.huawei.push.action.MESSAGING_EVENT), PushService.m10449e (token fetch + g_reg_id upload).
  • Delegates to: bm.RunnableC3701s3 (push dispatch), p444o0.C18984l1 (token task), mn.C18622a (failure task).
  • Endpoints: updateuserdetails.jsp (g_reg_id=<FCM token>, apsVersion=...), cli_auth/gettransport (reg_id=<FCM token>), cantalk.jsp, start-ttm, addfbtoken.jsp (Facebook token).
  • DB tables: shared_preference_v2 (SP_FCM_TOKEN_EYECON, facebookToken, facebookId).
  • Permissions: POST_NOTIFICATIONS, INTERNET, com.google.android.c2dm.permission.RECEIVE, com.eyecon.global.permission.C2D_MESSAGE.
1.25 Registrationh5
  • Feature: First-launch onboarding, phone-number + social registration, CLI validation, join.
  • Key classes: Registration/RegistrationActivity (~1.4k lines; a 7-child ViewFlipper), Registration/Onboarding/OnboardingActivity, Onboarding/FakeCallActivity, Registration/PhoneNumberValidationActivity (deep-link eyecon_number_validation://), SMSRetrieverBroadcast, InstallReferrerReceiver, PreparingContactView, EyeProgressBarRegistration, RegisterNotificationWork.
  • Delegates to: p494q7 (callbacks), p365jd.RunnableC17614z (jd.z join.jsp + gettransport), p516r5.RunnableC19875s (r5.s checkcode), p607vk.RunnableC21082e0 (vk.e0 social_auth/use_cli_validation, social_auth/validatetoken), p284f7.C16489q (emulator detection), p262e7.AbstractC16130b (e7.b callback base).
  • Endpoints: join.jsp, cli_auth/gettransport, cli_auth/checkcode, social_auth/use_cli_validation, social_auth/validatetoken, updateuserdetails.jsp, virality/viral_id.
  • DB tables: shared_preference_v2 (clientId, authenticatedCli, SP_UUID, tempUserName, tempAuthenticatedCli, sendSmsEnded, isSmsValidEnded, picfile), contacts (initial scan).
  • Permissions: READ_PHONE_STATE, READ_PHONE_NUMBERS, SEND_SMS, RECEIVE_SMS, READ_SMS, READ_CONTACTS, CAMERA, READ/WRITE_EXTERNAL_STORAGE, INTERNET.
  • See Flow A.
1.26 SocialMediaShareh5
  • Feature: Share a social-media video/card into Eyecon’s feed and to WhatsApp.
  • Key classes: SocialMediaShare/ShareMediaJob (JobScheduler job id 24).
  • Delegates to: p621w7.C21295o (w7.o media resolver), p621w7.C21294n (ShareVideoDB), p621w7.C21293m (w7.m share payload).
  • Endpoints: POST api.eyecon-app.com/app/share with application/json body {cli_list:[...], type, content, provider} (built in p607vk.RunnableC21093i case 24), plus messages/get polling.
  • DB tables: table_share_media.
  • Permissions: INTERNET, READ_EXTERNAL_STORAGE.
  • Remote gates: share_video_list_sync_interval (20), media_share_job_interval (120 min), hoursSinceReceptionMedia (24), is_media_share_cache_enable, social_media_share_sms_msg.
  • Notes (OBSERVED): the app injects a hard-coded desktop Chrome 89 UA and sec-ch-ua headers to fetch Facebook/TikTok media directly (p607vk.RunnableC21093i default branch).
1.27 Spamh5
  • Feature: Spam report + block list UI and DB.
  • Key classes: Spam/SpamAndBlockFragment, and the whole p648x7 + p623w9 implementation.
  • Delegates to: p648x7.C21656c (x7.c dialog), C21675v (x7.v binder), C21670q (x7.q SpamDB), C21657d (x7.d model), p623w9.RunnableC21301b (w9.b list loader/writer), p648x7.C21654a (x7.a share dialog).
  • Endpoints: GET api.eyecon-app.com/app/spam?list_version=<v> (crowd-sourced spam list), POST api.eyecon-app.com/app/spamreport (p599v7.RunnableC20908f, JSON body).
  • DB tables: spam_list, block_list, plus SP_KEY_VERSION_SPAM_LIST in shared_preference_v2 and the native SP file spam_prefs (reportSpam, shareSpam).
  • Permissions: SEND_SMS (share-spam text), INTERNET.
  • Remote gates: spam_list_sync_interval (default -1 = disabled).
  • See Flow C.
1.28 Themesh5
  • Feature: Custom theme editor/preview.
  • Key classes: Themes/CustomThemePreviewActivity, Themes/CustomThemeView, Themes/ThemeException.
  • Delegates to: p674y7.C21884l (y7.l theme store), p303g5 (premium gating), p599v7 (media), p420m7 (bitmaps).
  • Endpoints: — (theme JSON is local SP: SP_KEY_SELECTED_THEME_INFO).
  • DB tables: shared_preference_v2; colours are Android theme attrs (R.attr).
  • Permissions: WRITE_SETTINGS/SYSTEM_ALERT_WINDOW only if the theme touches system colours (INFERRED, medium).
1.29 Tokih5
  • Feature: “Toki” - the push-to-talk / walkie-talkie second-number feature and its UDP media stack.
  • Key classes: Toki/TokiService (foreground service, specialUse; also runs a proximity sensor listener on sensor type 8 to detect the phone being near the ear), Toki/TokiActivity, Toki/TokiContactsChooserActivity, ClearFocusOnBackEditText.
  • Delegates to: p703z7.C22122v0 (z7.v0 TokiFragment/session), p633wl.C21450m (wl.m TalkiSession), p687yl.C21952k (yl.k UDP transport), as.C3349u2 (as.u2 TalkiServer, log-tag "TalkiServer"), p340i7.RunnableC17180l0 (i7.l0, does InetAddress.getByName(...)).
  • Endpoints: plain UDP to ptt.eyecon-app.com on a port chosen at random from Remote Config toki_ports (default [5620,50000-50010]). No HTTP, no TLS.
  • DB tables: contacts (phone_number_in_server), plus the in-memory session registry z7.v0.f60387b.
  • Permissions: RECORD_AUDIO, MODIFY_AUDIO_SETTINGS, INTERNET, FOREGROUND_SERVICE, POST_NOTIFICATIONS, ACCESS_FINE_LOCATION (used for the “isWalkieTalkieEnabled” region gating - INFERRED).
  • See Flow H.

2. END-TO-END FLOW RECONSTRUCTIONh4

Nine flows. Each is split into ### OBSERVED (what the code does) and ### INFERRED (intent).


FLOW A - FIRST LAUNCH / REGISTRATIONh5

Entry points

  • com.eyecon.global.MainScreen.MainActivity (MAIN/LAUNCHER) - the launcher.
  • com.eyecon.global.Registration.RegistrationActivity (singleTask, TransparentBackground) - the 7-page ViewFlipper.
  • com.eyecon.global.Registration.Onboarding.OnboardingActivity - the “How Eyecon works” carousel.
  • com.eyecon.global.Registration.InstallReferrerReceiver (com.android.vending.INSTALL_REFERRER, goAsync()).
  • com.eyecon.global.Others.Objects.EyeconInitProvider (initOrder=101) - runs before any Activity.
A.1 OBSERVED - bootstraph6
  1. MyApplication.attachBaseContext resolves the process name; f12170o = processName.equals("com.eyecon.global") is the “main process” flag that gates ~all initialisation (SP, DB, thread pools). Three processes are declared: :notification_process (NotificationListenerService) and :restart_app.
  2. MyApplication.onCreate: sets f12172q = Documents/Eyecon, installs itself as the default UncaughtExceptionHandler, creates the singleton C0041b f281a = new C0041b(context) (the contactdb helper, v57), loads the custom SharedPreferences (contactdb.shared_preference_v2), allocates two LruCache bitmap caches sized maxMemory/24, and starts new Thread(new RunnableC0074r0(this, 0)).
  3. RegistrationActivity.m10512l0() / m10514o0() read two Remote-Config strings, caching them into SP as SP_KEY_REG_FLOW_MODE_MODE (reg_flow_mode, default "auto") and SP_KEY_REG_PERMISSIONS_PROMPT_MODE (permission_prompt_text, default "baseline").
A.2 OBSERVED - the onboarding pages (ViewFlipper children)h6
ChildContentAnalytics event
0Welcome / languageReg_welcome_pageView with Source = "How Eyecon works" or "First screen" (from OnboardingActivity.m10463C0() = Remote Config is_new_onboarding_enable)
1(skipped in code)-
2Choose registration type (phone / Facebook / Google)Reg_number_pageview with Reg_type_shown = Phone_plus_Social / Only_social / Phone_only and a detail string built from which of eyecon, facebook, google are present
34-digit code verification (EditText x4)Reg_verification_pageView with Reg_type_chosen = Phone/Google/FB
4Profile (display name + photo)Reg_profile_pageView
6Contacts loading (PreparingContactView)reg_contacts_loading_pageview with onboarding_type = short/normal
A.3 OBSERVED - install referrerh6

InstallReferrerReceiver.onReceive -> RunnableC17219d1(..., goAsync()) -> InstallReferrerReceiver.m10455a(app, intent):

  • First forwards the intent to new com.singular.sdk.SingularInstallReceiver().onReceive(...).
  • Reads bundle.getString("referrer"), one-shot guarded by SP boolean "referrer call", then m10456b(referrer, 1).
  • Three recognised shapes:
    1. referrer.startsWith("evid-") and referrer.substring(5).length() == 12 -> viral id stored in SP key SP_KEY_INVITER_ID, then m10457c() logs Invite_successfully_accepted (guarded by sendViralityEventDone).
    2. referrer.startsWith("eyecon_ch_") -> split on _campid_ into (channel, campaign id), analytics Install referrer channel with Channel / Campaign ID.
    3. otherwise parse utm_content from a fake URI and apply rule (1) to it (viralIdFromPromotion).
  • Note the code prints the entire referrer bundle key/value list to logcat (IRR/...), which is a minor log-leak.
A.4 OBSERVED - CLI validation and join.jsph6

Step-by-step, with the exact call chain:

RegistrationActivity.m10516C0(String mode, String code) // code received
-> set the 4 code boxes (unless mode == "manual")
-> read SP "tempUserName", SP "tempAuthenticatedCli"
-> SP: sendSmsEnded = true (only when mode != "manual")
SP: isSmsValidEnded = false
-> C21280h.m38435h(callback = q7.l0(this,3),
cli = l7.b.normalise(tempAuthenticatedCli),
code = code,
transport = EnumC19662h1.SO_SMS,
isSocialAuth = false)
-> w6.h.m38435h -> p315h7.C16945i (Server pool) -> p516r5.RunnableC19875s ("r5.s")
GET https://api.eyecon-app.com/app/cli_auth/checkcode
?cli=<normalised cli>
&code=<4-digit code>
&apsVersion=vc_749_vn_4.2026.04.23.1145_a
&transport=so_sms
&is_already_social_auth=false
(one retry after a 5 s sleep on any throwable)
success requires: HTTP 200 AND Integer.parseInt(body) == 0
on success: construct p284f7.C16489q ("f7.q" = EmulatorDetector),
set f40984c = true,
RunnableC16111m(13, detector, C16770d(25, cli, callback)) // hop to UI thread
on failure: w6.h.m38437j(callback, EnumC19659g1.IS_CODE_VALID, httpCode, body)

Then, on success, q7.l0.mo309m() case 3:

SP: isSmsValidEnded = true
toast R.string.authentication_approved
SP: authenticatedCli = l7.b.normalise(tempAuthenticatedCli)
f12984r = <"cli" extra from the callback> // the number that was validated
f12982q = tempUserName
RegistrationActivity.m10556t0(isSocialAuth=false)

m10556t0 issues the join:

p315h7.C16945i(Server pool) ->
p365jd.RunnableC17614z (case 1 == "jd.z" JOIN) with:
cli = f12984r (normalised unless already social-auth)
cli_cc = AbstractC0062l0.m371z1() // user country code
is_already_social_auth = z10
validationType = q7.h1 (SO_SMS / MO_SMS / SO_FLASH / FB / SOCIAL_LOGIN / null)
callback = q7.l0(this, 4)
GET https://api.eyecon-app.com/app/join.jsp?<query>&cv=vc_749_vn_4.2026.04.23.1145_a

Note the unusual construction: the whole query string is built with lo.a.m34580q(ArrayList) (OkHttp HttpUrl.Builder), then concatenated onto the URL, and the call is made with an empty param list - so join.jsp is a GET whose parameters live entirely in the query string. Response headers are collected into a HashMap.

join.jsp query parameters (OBSERVED, in insertion order):

#ParamValue
1clil7.b.m34444e(...) - normalised phone number (digits + +), or the social CLI as-is
2usernameliteral "New_User_Eyecon"
3devicenameAbstractC0062l0.m357l1() - formatted device name
4user_langAbstractC0062l0.m356k1()
5os"Android " + Build.VERSION.RELEASE + " ; " + Build.VERSION.SDK_INT
6public_idSP SP_UUID (AbstractC9039h.m10625E())
7adv_idadvertising id (o4.i.m35361a())
8mcAbstractC0042b0.m175X() - Wi-Fi MAC address
9viral_idSP SP_KEY_INVITER_ID
10cli_ccAbstractC0062l0.m371z1() - user country code
11svronly when is_already_social_auth; the value of q7.b1.f50539o (server id from social login)
12transportq7.h1.f50618a (so_sms / mo_sms / so_flash / fb / social_login)
13generated_cliSP SP_REJOIN_OLD_GENERATED_CLI, if non-null
14imeim5.p.m34691f() - device IMEI
15os_idAbstractC0042b0.m172U() - Settings.Secure.ANDROID_ID
16cvvc_749_vn_4.2026.04.23.1145_a

join.jsp response handling (OBSERVED):

  • HTTP != 200 -> w6.h.m38437j -> maps the body integer through q7.e1 (EnumC19653e1) -> error enum.
  • HTTP 200 but empty body -> EnumC19653e1.JOIN_RESPOND_CODE_200_WITHOUT_CID (“missing public id”).
  • Response headers: pi -> stored into SP SP_UUID (“Authenticator/updatePublicId”); sd -> isDeviceChanged when the value equals "0"; is_new -> isJoin when the value equals "1".
  • Response body: becomes the callback result and is written to SP key clientId by q7.l0.mo309m() case 4.
  • Side effects on success: CallStateService.m10184H(app), AbstractC3464e.m3667r("") (clear analytics user props), AbstractC16257t1.m31616A(isJoin, joinOptions, isSocial, false).

clientId = the CID (OBSERVED, resolves an open question from BACKEND-FINDINGS.md §9): com.facebook.appevents.AbstractC9039h.m10662s() returns SP["clientId"]. clientId is written in exactly two places:

  • p494q7/C19673l0.java:367 - from the join.jsp response body (m32061b() of the success callback), and
  • p494q7/C19677n.java:201 - from the social_auth/use_cli_validation response body. Both are then propagated to cli_auth/gettransport as reg_id… no - the reg_id there is the FCM token. The clientId is what w6.a sends as the e-auth header on every subsequent request, and what is sent as msisdn on uploadpic.jsp and as cid in the RSA-encrypted grps/sgn body. So: the account credential is a server-issued opaque token from join.jsp, not a device identifier. (INFERRED, high confidence.)
A.5 OBSERVED - cli_auth/gettransport (what the server tells the client to do)h6

Issued by q7.i1 (C19665i1, the JoinSession) via m36102a(number):

GET https://api.eyecon-app.com/app/cli_auth/gettransport
?apsVersion=vc_749_vn_4.2026.04.23.1145_a
&cli=<normalised cli>
&reg_id=<FCM token>
&is_already_social_auth=<bool>
+ the full device-fingerprint set (w6.h.m38430c): installer_name, n_sims, time,
is_sms_sending_available, is_whatsapp_installed, mc (MAC), device_id (ANDROID_ID),
adv_id, imei, client_cc, time_zone, device_manu, device_model
&transport=<q7.h1> (when known)
&last_error=<code> (when a previous attempt failed)

JSON response:

{
"error_status": <int, mapped through q7.e1>,
"subcode": <int>, // only when error_status == 9 (SERVER_ERR_CANT_VALIDATE)
"transport": "mo_sms|so_sms|so_call|so_flash|huawei|fb|social_login|auto",
"details": { // shape depends on transport
// transport == mo_sms:
{ "send_to": "<short number>", "timeout": <sec>, "body": "<sms text>", "code": "<4-digit>" }
// transport == so_flash:
{ "timeout": <sec>, "pattern": "<regex>" }
// otherwise:
{ "timeout": <sec> }
}
}

error_status == 0 (ERR_NONE) -> proceed; error_status == 10 (SERVER_ERR_ALREADY_VALID) -> treat as success without a transport. subcode: 0=ALL_RETRIES_FAILED, 1=COUNTRY_NOT_SUPPORTED, 2=DELETE_ME_OVER_LIMIT, 3=BANNED_BY_TRAFFIC_ANALYZER.

A.6 OBSERVED - SMS phone-number verificationh6

Two distinct mechanisms, both wired to the same callback:

  1. Server-initiated (MO_SMS) - the server sends an SMS from a short number to the device number containing the 4-digit code; the user types it (mode == "manual", sendSmsEnded stays false).
  2. Google/Huawei SMS Retriever API - SMSRetrieverBroadcast listens for com.google.android.gms.auth.api.phone.SMS_RETRIEVED_ACTION and com.huawei.hms.auth.api.phone.SMS_RETRIEVED_ACTION; if EXTRA_STATUS.getStatusCode() == 0 it rebroadcasts the extras under the internal action INTENT_ACTION_AUTH_SMS_RECEIVED. This is the “App-hash”-free auto-read path.
  3. Legacy raw PDU - com.eyecon.global.Sms.MySmsReceiver (highest-priority, BROADCAST_SMS-protected receiver for android.provider.Telephony.SMS_RECEIVED) parses SmsMessage.createFromPdu, extracts the 4-digit code with AbstractC16468f0.m195w(...), and forwards it to CallService with extras smscode, incoming_number, state. CallService.onStartCommand with extras.getInt("state") == 6 launches PhoneNumberValidationActivity with phone_number + smscode; with state == 7 it calls DBContacts.m10117u(6, cli, now, 0L).

PhoneNumberValidationActivity (deep link eyecon_number_validation://) is the mid-flow variant of RegistrationActivity for an already-installed app. Its success handler (p494q7.C19677n case 2) writes clientId from the social_auth/use_cli_validation response body (this is the path where an existing install re-associates a real number with the social account) and clears SP_KEY_NOT_REGISTERED_WITH_REAL_CLI_V2.

social_auth/use_cli_validation (p607vk.RunnableC21082e0 case 23):

GET https://api.eyecon-app.com/app/social_auth/use_cli_validation
?device_id=<ANDROID_ID> &cli=<normalised> &advid=<GAID> &apsVersion=...

social_auth/validatetoken (same file, case 25):

GET https://api.eyecon-app.com/app/social_auth/validatetoken
?device_id=<ANDROID_ID> &sim_cc=&user_cc=&adv_id=&provider=<fb|google>&token=<oauth token>&apsVersion=...

Response must contain a non-empty cli, else C16479l("validatetoken api return with missing info").

A.7 OBSERVED - post-joinh6

q7.l0.mo309m() case 4 (join success) writes, in one transaction: clientId, SP_KEY_VALIDATION_WITH ("Phone number" / "Google" / "Facebook" / "Not set"), deviceName, osName, sp_join_time (epoch ms), SP_JOIN_VERSION = "4.2026.04.23.1145", SP_JOIN_VERSION_CODE = 749, SP_KEY_IS_REJOIN = !isJoin. Then: analytics New join / JoinType = Join|Rejoin; EyeconAnalyticsWorker.m9988b(true) (enqueue the call-log worker); RunnableC21275c(2) -> updateuserdetails.jsp with adv_id; MorningAdRefreshJob.m9917b(); C19845p.m36570a(); cancel all WorkManager jobs tagged notificationWork; if this is a rejoin -> RunnableC21276d(new C3463d(10), 4) = GET virality/viral_id_check; then j5.c.m33583h -> GET virality/viral_id.

RegistrationActivity.m10530R0() -> child 6, PreparingContactView.m10504d(...) starts the address-book scan; when it completes, DBContacts.addressBookUpdated(changed, deleted, photoApproved) -> DBContacts.m10139d0("app_started", ...) (Flow F). m10529Q0("Registration before auto permissions") may interpose the paywall: only if Remote Config registration_premium_location == "get_started" and f12935R0 (isPremiumUser) is non-null and false, via AbstractC19327e.m35649f("paywall_premium_reg_get_started", true, source).

Permissions: m10531S0() builds a q7.t / q7.j PermissionRequestDialog and only shows it when at least one of C17534t.m33654I0(), C17534t.m33646L0() (dialer role), m33644J0/m33645K0/m33648N0 (sms role), C17534t.m33651E0(true), Remote Config ask_read_notification_in_registration + missing notification-listener grant, or m33652F0() is true.

A.8 INFERRED - Flow Ah6
  • The cli_auth/* design is a state machine driven by the server: join.jsp creates the account, gettransport tells the client which verification channel to use (SMS out from Eyecon, SMS in from Eyecon, flash-call, or none because the number is already validated), and checkcode confirms it. The client has no local policy about whether a number is valid. Confidence: high.
  • MO_SMS/SO_SMS = “Mobile Originated / Service Originated SMS” = the direction of the verification SMS. SO_FLASH = a missed-call (“flash”) verification where the user must call a number and the pattern is detected by the carrier. Confidence: high (the so_flash branch reads details.pattern + details.timeout and RegistrationActivity.mo10497u logs "Reg SO flash started" and registers a receiver for EYECON_ACTION_CALL_TO_FLASH_FOUND).
  • username = "New_User_Eyecon" is a placeholder the server replaces with the user’s chosen display name (which is separately pushed via updateuserdetails.jsp?username=<name>). Confidence: medium.
  • Registration collects exactly three things from the user: (1) a phone number, or a social identity; (2) a display name; (3) optionally a profile photo (gallery / camera / Facebook). There is no email collection for phone registration, no DOB, no address. Confidence: high (all other screens are inert).
  • e-auth-k (PgdtSBeR0MumR7fO) being both an APK constant and the compiled-in default of Remote Config circus_key means the server can rotate the key the client presents without shipping an update. Confidence: high (see BACKEND-API.md §3.3).

FLOW B - CALLER-ID ON INCOMING CALLh5
B.1 OBSERVED - call ingress (three parallel paths)h6

(1) CallScreeningService (API 24+, system-driven).

com.eyecon.global.DefaultDialer.EyeconCallScreeningService.onScreenCall(Call.Details)
-> f11874a = details
-> if CallStateService.m10199x() (MICO is the default dialer / self-managed call):
respondToCall(details, allowCall) // immediately allow, no UI
-> else:
callDirection == -1 -> log "canceled, call DIRECTION_UNKNOWN"
details.getHandle() == null -> log "canceled, uri is null"
handle.schemeSpecificPart empty -> log "canceled, cli is empty"
otherwise build Bundle{ state = RINGING|OFFHOOK,
incoming_number = <cli>,
"android.intent.extra.PHONE_NUMBER" = <cli> }
and post a synthetic broadcast:
RINGING -> "com.eyecon.global.action.CALL_SCREENING_NEW_CALL"
OFFHOOK -> "android.intent.action.NEW_OUTGOING_CALL"
to MyPhoneReceiver (the app's own registered instance)
-> in all cases: RunnableC20051e(21, this, details) after a 2000 ms delay
(delayed respondToCall so the platform's own screening timeout is not blown)

Note stopService() is overridden to return false - the service is effectively unkillable by the system.

(2) BroadcastReceiver (MyPhoneReceiver). Declared with priority=2147483647 for: android.intent.action.PHONE_STATE, android.intent.action.NEW_OUTGOING_CALL, com.eyecon.global.action.INTENT_ACTION_START_CALL_WHATSAPP, com.eyecon.global.action.CALL_SCREENING_NEW_CALL. It re-dispatches everything to im.RunnableC17334k (“im.k”) on the w6/main thread pool.

(3) CallService (a specialUse foreground service whose declared purpose is literally “Used to tunnel incoming calls from a broadcast-receiver and determine if the caller ID should be shown to the user”) keeps a persistent low-visibility notification (caller_id_status / caller_id_v2 channels) so the app is not killed between calls. CallServiceRestarterReceiver + an AlarmManager.setAndAllowWhileIdle in onTaskRemoved restart it 5 s later.

B.2 OBSERVED - the call-state machine (im.k case 7 / m5.e = C18491e)h6
C18491e f46634s = single call-state machine singleton
f46641c lastState (-1 = outgoing/pre-dialled, 0 = IDLE, 1 = RINGING, 2 = OFFHOOK)
f46643e inCall, f46633r offhook
f46634P callerIdNotificationEnable (from the intent extra)
f46645g lastCis, f46635t lastRawNumber
f46637v outgoing-call start time
onStartCommand(EYECON_INTENT_ACTION_CALL, extras):
CallService.f11630f = 1
intent.action = "EYECON_INTENT_ACTION_CALL"
extras: "EYECON_INTENT_KEY_ACTION", "INTENT_KEY_CALLER_ID_NOTIFICATION_ENABLE",
"EYECON_INTENT_KEY_IS_SCREEN_ON", "TYPE_CALL_WHATSAPP"
if action != NEW_OUTGOING_CALL:
state = extras["state"]; rawNumber = extras["incoming_number"]
if inCall && rawNumber empty -> rawNumber = f46644f
IDLE(0) -> f46643e=false, f46633r=false, mode=0
OFFHOOK(2)-> f46633r=true, mode=2
RINGING(1)-> f46633r=true, mode=1
cis = l7.b.m34444e(rawNumber)
(if cis == lastCis or user registered -> f418.m5.C18505s.m34701a("flush call"))
else if f46641c != 1:
f46633r = true; f46643e = true
rawNumber = extras["android.intent.extra.PHONE_NUMBER"]
mode = -1; cis = normalise(rawNumber)
then, per state:
mode == 0 (call ended):
if f46641c == 1 -> C18491e.m34674b(3, raw, cis) // "was it spam?"
else if f46642d -> m34674b(1, raw, cis)
else -> m34674b(2, raw, cis) + DBContacts insert (type 2)
mode == 2 (answered, self-managed only):
f46642d = true
AbstractC19327e.m35649f(cis) // mark "in call" for caller-ID
if RemoteConfig IncomingCallPrefetchMode == "when call answered":
BaseAfterCallActivity.m9925o1("Call started") + analytics
C18491e.m34678f(cis, raw, EnumC17759a.c/d) // RING_ANSWERED first / not-first
mode == 1 (ringing) and raw non-empty and != "N/A":
if (lastState is 2 or -1) and cis == lastCis -> reuse lastCis
else:
f46642d = true
if lastState == 0: // first ring of an incoming call
C18302h.m34431c(...) // push cis into the CallerIdDataManager queue
C18491e.m34677e(raw, cis)
if RemoteConfig IncomingCallPrefetchMode == "when ringing":
BaseAfterCallActivity.m9925o1("Call started")
C18491e.m34675c(raw, cis)
C18491e.m34678f(cis, raw, EnumC17759a.e)
else: // ringing again (already answered once)
if raw non-empty && AbstractC16794a.m32645G():
C18491e.m34677e(raw, cis)
blocking lookup on the CallerIdDataManager queue
if the lookup MISSED -> C17781s.f45046m0.m34104z(raw, cis, EnumC17759a.f)
f46650l = true
RunnableC21275c(10) -> trim SP "SP_AFTER_CALL_LOGS" (drop entries older than now - <run start>)
f46641c = mode; f46645g = (mode == 0 ? "N/A" : cis)
CallService.m10051d(intent)
B.3 OBSERVED - number -> name/photo lookuph6

Two layered caches plus one network call. Everything funnels through t5.m = p554t5.C20343m (PersonDataLoader).

C20343m(srcTag, rawNumberOrCis, InterfaceC20338h callback)
fields: f52915o = src tag, f52904d = raw number, f52901a = cis (normalised),
f52909i = size (1 = small, 2 = big, other = null), f52908h = "useAddressBook",
f52907g = boolean[3] completion flags (name-done, pic-done, done)
m37284i() -> new RunnableC20341k(this, 0).run() (on the DBContacts "K" thread pool)
case 0:
if (contact == null):
if (!f52912l /* from address book */):
r5.e.C19854e (ContactsContract reader) -> RunnableC19542e0(24, raw, C20342l)
else:
DBContacts.m10142n(cis, useAddressBook, callback)
else if (!contact.m36643y() /* has photo */):
// two tracks in parallel:
(a) deliver the cached small photo from MyApplication's LruCache by contact_id
(b) if not cached:
w6.h.m38434g(srcTag, size, cis, C20340j) // <-- the pic? call
on error/after timeout -> deliver the cached big photo
else:
new RunnableC20341k(this, 2) // nothing to fetch; just signal name-only

pic?cli= request builder (OBSERVED, w6.h.m38434g):

// p620w6/C21280h.java:317-338
if (!l7.b.m34452q("+" + str2)) { callback.mo590e(null, 812); return; } // not a valid number
String url = "https://api.eyecon-app.com/app/pic"
+ "?cli=" + cis
+ "&is_callerid=true"
+ "&size=" + (size==1 ? "small" : size==2 ? "big" : "null")
+ "&type=0"
+ "&src=" + srcTag
+ "&cancelfresh=0"
+ "&cv=vc_749_vn_4.2026.04.23.1145_a";
m7.p.m34736e(cacheKey, url, cis, size, callback); // ImageLoader

Fetched with java.net.HttpURLConnection wrapped by FirebasePerfUrlConnection.instrument and w6.a.m38427k(conn), which is the function that injects the e-auth-v / e-auth / e-auth-c / e-auth-k headers. So the photo request carries the same account credential as the API. (OBSERVED.) m7.p.m34732a also gates: if the resolved URL contains eyecon-app.com and the user is not an Eyecon user, the callback fires with (null, 1) and no network happens.

Name + spam resolution (OBSERVED, androidx.fragment.app.RunnableC2598c case 6, log-tag "CallerID get name"):

if (!AbstractC9039h.m10665v()) // not an Eyecon user
-> synthesise x7.d(-1 /*unknown*/, raw, cis, name) and return // no network
// Eyecon user:
1. spam_list cache first:
x7.q.m38871a(cis) // "SELECT * FROM spam_list WHERE cli_in_server = '<cis>'"
if hit -> callback(names=[x7.d.name], entry, 0) and return
2. t5.g (PersonNameCache) lookup:
"SELECT data, strftime('%s', modify_time)*1000 AS modify_time
FROM names_photos_cache WHERE data_type = <name> AND cli_in_server = '<cis>'"
-> HashMap with keys "name", "spam_type", "is_spam"
if hit -> callback + return
3. network:
GET https://api.eyecon-app.com/app/getnames.jsp
?cli=<cis>
&lang=<contactLang>[,<user_lang>] // SP "contactLang" + a7.l0.m356k1()
&is_callerid=true
&is_ic=<isIncomingCall bool>
&apsVersion=vc_749_vn_4.2026.04.23.1145_a
&requestApi=okHttp|URLconnection // w6.h.f56469e is a Random().nextBoolean() decided once per process
&source=<srcTag>
response: a JSON **array**; only element [0] is used:
"name" -> display name
"spam_type" -> string, compared against the literal "spam"
"is_spam" -> boolean

Spam decision (OBSERVED, verbatim from the JADX listing at RunnableC2598c.java:662-673):

if (r0 /* is_spam */) {
if (r2 /* spam_type present and != "spam" */) spam_type = 2; // "maybe spam" / suspicious
else if (r6 /* spam_type present */) spam_type = 1; // confirmed spam
else spam_type = 0; // not spam
} else {
if (httpCode == 404 || httpCode == 200) spam_type = -1; // unknown
}
t5.g.m37273d(spam_type, cis, displayName); // writes into names_photos_cache.data_type=<name>
callback(names, new x7.d(spam_type, raw, cis, displayName), 0);

Plus: a name that case-insensitively equals "spam" or "maybe spam" is discarded and replaced by an empty string (the sentinel the backend uses to mean “no name, but this is spam”).

B.4 OBSERVED - what the UI does with ith6
  • MenifaFragment.mo721c0(callback): reads map.get("CB_KEY_SPAM") -> x7.d, reads map.get(AbstractC0039a.f219g /* "name" */), then sets contact.isSuspiciousSpam = x7.d.m38862f(spam_type) (== 2) and contact.isSpam = x7.d.m38861e(spam_type) (== 1 || == 3).
  • ContactReminder / BaseAfterCallActivity render R.string.identified_as_spam, R.string.maybe_spam (“Could be Spam”), R.string.is_it_spam_nonumber (“SPAM?”).
  • t5.e (C20335e, PersonData) writes the name back into names_photos_cache (data_type = EnumC20336f.f52884a) and the spam verdict into contacts.
B.5 INFERRED - Flow Bh6
  • The whole design is fail-open on the network: if getnames.jsp 404s or 200s with an empty name, the client synthesises spam_type = -1 (unknown) rather than “not spam”, and the UI simply shows no name. Blocking is therefore never silently disabled by a network error. Confidence: high.
  • is_callerid=true on getnames.jsp and &is_callerid=true&size=... on pic?cli= are the same server-side switch, so a caller-ID photo request also returns the name/spam verdict. Confidence: medium.
  • The requestApi=okHttp|URLconnection parameter randomised once per process (Random().nextBoolean() in w6.h.<init>) looks like A/B telemetry of the two HTTP stacks rather than a functional choice. Confidence: medium.
  • The “discard the name if it equals spam/maybe spam” rule implies the backend encodes “this number has no name but is a spammer” in the name field for legacy client compatibility. Confidence: medium.
  • spam_type == 3 is treated as “is spam” by the UI but is never produced by getnames.jsp - it comes only from the crowd-sourced spam list. Confidence: high (see Flow C).

FLOW C - SPAM BLOCKINGh5
C.1 OBSERVED - the three spam “flavours”h6

Two distinct stores, plus a third that is folded into one of them:

StoreTablespam_type semanticsEnum
Crowd/AI spam listspam_list1 = SPAM (crowd/AI), 2 = suspicious/“maybe spam”, 3 and 4 = user-authored entries (never overwritten by the server list)x7.o -> SPAM, BLOCK
Block listblock_listis_enable 0/1 + create_timestampsame x7.o enum
Spam on a contactcontactsderived at read time from spam_list; history.is_spam / history.is_suspiciousSpam are the persisted per-call verdicts-

x7.d (the x7.d model, p648x7.C21657d) carries: contact_id, cli_in_server (implements Comparable), cli, name, spam_type, create_timestamp. Helpers:

x7.d.m38861e(int t) -> (t == 1 || t == 3) // "isSpam"
x7.d.m38862f(int t) -> (t == 2) // "isSuspiciousSpam"
C.2 OBSERVED - crowd-sourced spam-list sync (Others/Tasks/DailyWorker)h6
// com/eyecon/global/Others/Tasks/DailyWorker.java:97-105
int interval = RemoteConfig.m31086g("spam_list_sync_interval"); // default -1 (OFF)
if (interval != -1
&& now - SP.getLong("SP_KEY_LAST_SPAM_SYNC", -1) >= TimeUnit.DAYS.toMillis(interval)) {
String version = SP.getString("SP_KEY_VERSION_SPAM_LIST", "");
-> p607vk.RunnableC21082e0(version, callback, 21)
GET https://api.eyecon-app.com/app/spam?list_version=<version>&apsVersion=...
}

Response JSON:

{ "version": "<string>", "spammers": [ { "cli": "...", "name": "..." }, ... ] }

Merge (OBSERVED, DailyWorker.m10355b):

  1. Load the local spam_list (SELECT * FROM spam_list ORDER BY cli_in_server).
  2. Keep only the local entries whose spam_type is 3 or 4 (the user’s own reports); drop everything the server previously supplied.
  3. Binary-search-merge in every entry from spammers that is not already present; sort by cli_in_server.
  4. Rewrite the whole table in one transaction (DELETE FROM spam_list then per-row insert) and store the new version in SP SP_KEY_VERSION_SPAM_LIST.
C.3 OBSERVED - the report/block dialog (SpamAndBlockFragment -> x7)h6
SpamAndBlockFragment
ViewFlipper (2 tabs, EyeTabLayout): tab 0 = R.string.block ("Block"), tab 1 = R.string.spam ("Spam")
ViewPager adapter x7.p (C21669p) holds two RecyclerView.Adapters:
x7.h (EnumC21668o.SPAM) -> list f57968j
x7.h (EnumC21668o.BLOCK) -> list f57969k
Loads: RunnableC21663j(0) // block_list
w9.b case 9 -> "select * from spam_list order by cli_in_server" -> list f57937q
Both adapters render item_block_and_spam: photo, name, number, and a
Block/Unblock button (x7.h.onBindViewHolder).
onResume logs "Spam_and_block_page".
listens for the in-app broadcast "INTENT_SPAM_UPDATED" (C9080e) and reloads.
Empty state: block tab -> R.string.block_number / R.drawable.block_empty_list
spam tab -> R.string.add_number_to_spam / R.drawable.spam_1 / R.drawable.empty_spam

Report dialog x7.c (C21656c) + binder x7.v (C21675v), layout R.layout.report_spam:

  • Mode 1 = report-spam (title R.string.report_spam, CTA R.string.report_as_spam, R.id.EB_is_a_spam), with a CB_block checkbox (“also block”) and an “it was not spam” (R.id.EB_not_spam, hidden in some modes) escape.
  • Mode 3 / 4 = block-number flow from the Spam/Block pages (block_page_dialog / spam_page_dialog).
  • Mode 2 = from a call screen.
  • On confirm it shows x7.a = ReportAndShareSpamDialog with shareSpam, which sends R.string.share_spam_message: “Beware of this number. I blocked it with Eyecon and recommend that you use it too to protect yourself from spam.”
  • Dialog throttling state lives in the native SP file spam_prefs under keys reportSpam / shareSpam, written by y5.v.m39108u(type, key, text, buttonText); reportSpam is a small JSON read back at w6.d case 26 as optInt("spamType", -1).

Network side of a report (OBSERVED, p599v7.RunnableC20908f line ~342):

p647x6.C21649b.enqueue(new x6.e( // queued RawBody POST
body = <JSON>,
"",
"https://api.eyecon-app.com/app/spamreport",
"reportBlock", "url_encoded", "POST"))

i.e. the report is durably queued into table_api_requests and sent later by ApiRequestWorker.

C.4 OBSERVED - blocking a numberh6

p516r5.RunnableC19875s case 0 (log "BLOCK", analytics AbstractC16257t1.m31644m("BLOCK", <cli>)) shows a confirmation dialog R.string.block_are_you_sure with [xx] replaced by the formatted number, then writes block_list and broadcasts a refresh. The dual-use of r5.s (the same class also implements cli_auth/checkcode) is a ProGuard merge artefact, not a design choice.

Auto-block of spam calls is Remote-Config gated: R.string.block_spam_calls_menu (“Block all SPAM calls”) / R.string.block_spam_with_ai, and enabling the AI assistant for spam calls also force-enables spam blocking (R.string.assistant_spam_blocking_enable_msg: “Enabling AI for spam calls will also enable blocking spam calls. Do you agree?”).

C.5 INFERRED - Flow Ch6
  • spam_type 1/2 are server-supplied (crowd reports + backend ML), 3/4 are local-only and survive a list sync by design. The client therefore trusts its own reports more than the server list, and never silently discards them. Confidence: high.
  • The local spam verdict is cached per-(cis) in names_photos_cache with data_type = <name> and invalidated via modify_time + the Remote Config server_data_cache expiry (default 7 days for both photos and names). Confidence: medium (the expiry constant is read but the exact comparison site was not traced).
  • Blocking is implemented at the UI/state level (don’t show caller ID, suppress actions) rather than at the telephony level; MANAGE_OWN_CALLS + CallScreeningService.setDisallowCall(true) is the mechanism that could reject a call outright, and EyeconCallScreeningService currently only ever calls setDisallowCall(false) in this build. Confidence: high that hard call rejection is not implemented via the screening service; medium that it is not implemented at all.

FLOW D - SMS ASSISTANT / AI REPLYh5

Important correction to the brief: the AI SMS assistant does not talk to voice2text.eyecon-app.com. It talks to a different, cleartext-HTTP host: http://numberlookup.instabridge.com/respond-sms. voice2text.eyecon-app.com is the separate “Talk To Me” (TTM) voicemail / speech-to-text service. Both are documented here because they are the two “AI” subsystems.

D.1 OBSERVED - architectureh6
UI (CallActivity / Menifa / AfterCall / MissedCall popup / Notification / SpamCall ...)
| x5.v.m38851a(context, MessageAssistantStartFlowArgs, type) // "start flow"
v
WorkManager OneTimeWork (NetworkType.CONNECTED)
| Data{ "args_bundle" = Moshi(MessageAssistantStartFlowArgs), "type" = "DEFAULT"|... }
v
com.eyecon.global.DefaultDialer.assistant.MessageAssistantStartFlowWorker (CoroutineWorker)
-> p646x5.C21640o ("x5.o") start-flow coroutine
-> p646x5.C21641p ("x5.p") MessageAssistantRepository
-> p636wo.C21474f ("wo.f") Retrofit container
-> p646x5.InterfaceC21626a ("x5.a") @POST("respond-sms")
... and on every incoming SMS:
com.eyecon.global.DefaultDialer.assistant.MessageAssistantBroadcastReceiver
(android.provider.Telephony.SMS_RECEIVED, permission BROADCAST_SMS, exported=true)
-> WorkManager OneTimeWork (NetworkType.CONNECTED)
| Data{ "sms_bundle" = Moshi(IncomingSMSContainerDTO) }
v
com.eyecon.global.DefaultDialer.assistant.MessageAssistantReplyWorker (CoroutineWorker)
-> p646x5.C21639n ("x5.n") reply coroutine
-> same Retrofit API

Retrofit wiring (p636wo/C21474f.java:230-337, OBSERVED):

  • baseUrl = "http://numberlookup.instabridge.com/" - plain HTTP, no TLS.
  • OkHttp timeouts 1 minute connect / read / write, retryOnConnectionFailure(true).
  • Moshi adapters registered for RespondSmsResponse (RespondSmsAdapter), MessageAssistantSource (MessageAssistantSourceAdapter), MessageAssistantSourceKey (al.C0155a enum adapter).
  • Interface x5.a is a Kotlin suspend fun (uo.d Continuation callback style).
D.2 OBSERVED - the ten trigger sourcesh6

MessageAssistantSource is a sealed interface with exactly ten data-class implementations; each carries a single MessageAssistantSourceKey key: MessageAssistantSourceKey:

Source classMessageAssistantSourceKeyStart type string (x5.h.m38835c)SP auto-use flag (x5.h.m38836d)
MessageAssistantSource.IncomingCallFullscreenIncomingCallFullscreenKeyDEFAULTSP_KEY_IS_AI_ASSISTANT_DEFAULT_AUTO_USE_ENABLE
MessageAssistantSource.IncomingCallNotificationIncomingCallNotificationKeyDEFAULTsame
MessageAssistantSource.IncomingCallPopupIncomingCallPopupKeyDEFAULTsame
MessageAssistantSource.AfterCallAfterCallKeyDEFAULTsame
MessageAssistantSource.MissedCallPopupMissedCallPopupKeyDEFAULTsame
MessageAssistantSource.FollowUpFollowUpKeyDEFAULTsame
MessageAssistantSource.CallDeclinedAutoUseCallDeclinedAutoUseKeyDECLINED_CALLSP_KEY_IS_AI_ASSISTANT_DECLINED_CALL_AUTO_USE_ENABLE
MessageAssistantSource.CallMissedAutoUseCallMissedAutoUseKeyMISSED_CALLSP_KEY_IS_AI_ASSISTANT_MISSED_CALL_AUTO_USE_ENABLE
MessageAssistantSource.SpamCallAutoUseSpamCallAutoUseKeySPAM_CALLSP_KEY_IS_AI_ASSISTANT_SPAM_CALL_AUTO_USE_ENABLE
MessageAssistantSource.UnknownCallerAutoUseUnknownCallerAutoUseKeyUNKNOWN_CALLERSP_KEY_IS_AI_ASSISTANT_UNKNOWN_CALLER_AUTO_USE_ENABLE

x5.h.getTypeAutoOrManual(): returns "manual" when the type is empty or "DEFAULT", else "auto". That single string is the type field of every analytics event for this feature.

Auto-use triggers (p646x5/AbstractC21633h):

x5.h.m38838f(type) -> RemoteConfig.m31081b("use_ai_assistant") && SP.getBoolean(autoUseFlagFor(type))
x5.h.m38840h(callStartMs, cli) // onCallDeclined
if (!enabled) return
if (now - callStartMs >= 2 min) return // "call time is not reasonable"
x5.h.m38839g(cli, "DECLINED_CALL", new MessageAssistantSource.CallDeclinedAutoUse())
x5.h.m38841i(callStartMs, cli) // onCallMissed - same 2-minute rule
x5.h.m38842j(cli, name) // onSpamCall
if (enabled) { x5.v.m38851a(ctx, new MessageAssistantStartFlowArgs(cli, name,
new MessageAssistantSource.SpamCallAutoUse()), "SPAM_CALL"); return true; }
return false
D.3 OBSERVED - request DTOs, field by fieldh6

MessageAssistantStartFlowArgs (Moshi, @JsonClass(generateAdapter=true)) - the WorkManager input:

Kotlin fieldJSON nameTypeSource
numbernumberStringC19862i.phone_number / the raw caller id
contactNamecontactNameStringC19862i.private_name
sourcesourceMessageAssistantSource (polymorphic, one of the 10 sealed classes, each serialised as {"key":"<SourceKey enum name>"})call site

RespondSmsBody - the POST body (p587uo-style; field names are exactly the Kotlin property names because the Moshi adapter uses the reflective/@Json names):

#FieldJSON nameTypeValue at start-flowValue at reply
1contactNamecontactNameStringmessageAssistantStartFlowArgs.getContactName() - the real contact nameflow.getNormalizedNumber() - i.e. the normalised phone number string, not a name
2numbernumberStringl7.b normalised digits-only number (C18312b.m34443d(3, number) then Character.isDigit filter)flow.getNormalizedNumber()
3messagesmessagesList<Message>emptyList()last 50 messages of the conversation, newest-last
4localelocaleStringConfiguration.getLocales().get(0).toLanguageTag() (API 24+) else locale.toLanguageTag()same
5messagesInSessionmessagesInSessionInt0total number of messages in the session (before truncation)
6userNameuserNameStringAbstractC9039h.m10663t("") = SP key "username"same

RespondSmsBody.Message (nested data class, Moshi-generated adapter):

FieldJSON nameTypeMeaning
roleroleenum Role { User, System }System if the SMS was sent by this device (found in content://sms/sent), User if it was received (found in content://sms/inbox)
messagemessageStringthe full SMS body text

RespondSmsResponse - the response, a sealed interface with two implementations:

ImplementationJSON shapeMeaning
NewMessage{ "message": "..." }”send this SMS to the caller and keep the conversation alive”
EndConversation{}”stop; the conversation is over” (Moshi adapter RespondSmsAdapter discriminates on presence of message)

Moshi is configured with AbstractC3588f.f3151a (a JsonClass/KotlinJsonAdapter flag set) - the exact serialization mode is not recoverable from the listing, so I note the ambiguity rather than guess.

IncomingSMSDTO / IncomingSMSContainerDTO - the WorkManager input built in the broadcast receiver:

FieldJSONType
originatingAddressoriginatingAddressString (from SmsMessage.getOriginatingAddress())
bodybodyString (from SmsMessage.getMessageBody())
startedFlowstartedFlowMessageAssistantFlow
(container) messagesmessagesList<IncomingSMSDTO>

MessageAssistantFlow - the per-conversation state persisted locally:

FieldTypeMeaning
startMSLongepoch ms when the conversation was started (written as now - 10000, i.e. back-dated 10 s so the SMS history query includes the assistant’s own opening message)
normalizedNumberStringthe other party’s normalised number
lastMessageMSLongepoch ms of the last message; 0 means “only the opening message”
startTypeStringDEFAULT / DECLINED_CALL / MISSED_CALL / SPAM_CALL / UNKNOWN_CALLER
D.4 OBSERVED - start flow (x5.o = p646x5/C21640o.invokeSuspend)h6
if (!AbstractC9039h.m10664u(context)) // READ_SMS + SEND_SMS + RECEIVE_SMS all granted?
{ analytics "ai_assistant_missing_sms_permission" {source}; return }
String digits = C18312b.m34443d(3, args.getNumber()); // country-aware formatting
if (digits == null) return;
String normalized = digits.filter(Character::isDigit);
if (SP_KEY_IS_AI_ASSISTANT_NEED_SHOW_TUTORIAL) -> RemoteConfig "use_ai_assistant" gates availability
MessageAssistantFlow existing = x5.s.m38848a(normalized, storage.m36434B());
if (existing != null) {
long idleMin = RemoteConfig.m31089j("messageAssistantIdleDurationMin");
long since = now - (existing.lastMessageMS == 0 ? existing.startMS : existing.lastMessageMS);
if (since <= TimeUnit.MINUTES.toMillis(idleMin)) {
log "sendStartingMessage canceled, conversion already alive"; return; // no duplicate opener
}
log "sendStartingMessage, removing old conversion";
x5.s.m38850c(existing, storage); // forget the stale conversation
}
String openingText = x5.h.m38833a(startType); // see below
String personalised = x5.h.m38834b(startType, openingText);
RespondSmsBody body = new RespondSmsBody(
args.getContactName(), // <-- real contact name on the FIRST call
normalized,
emptyList(),
deviceLanguageTag,
0,
SP["username"]);
api.a(body, this);

x5.h.m38833a(type) produces the locally generated opening line (sent as Role.System if the request fails, and also sent to the caller if the backend answers NewMessage):

String userName = AbstractC9039h.m10663t("").trim();
String base = userName.isEmpty()
? R.string.eyecon_assistant_initial_message_without_name
: R.string.eyecon_assistant_initial_message_with_name.format(userName);
return m38834b(type, base);

x5.h.m38834b(type, text) looks up SP key SP_KEY_AI_ASSISTANT_INITIAL_MESSAGE_FOR_<TYPE> and returns the user’s customised text if set, else the default. Keys: ..._FOR_DEFAULT, ..._FOR_DECLINED_CALL, ..._FOR_UNKNOWN_CALLER, ..._FOR_MISSED_CALL, ..._FOR_SPAM_CALL. (An unknown type throws Exception("Unknown type, type = ...").)

Response handling:

EndConversation -> analytics "ai_assistant_send_finish_on_start_flow" {number, source}; nothing sent.
NewMessage(msg) -> analytics "ai_assistant_starting_message_sent" {number, source, type=auto|manual}
-> final = x5.h.m38834b(type, msg) // user override still wins
-> x5.p.m38846c(repo, final, normalized, rawNumber, source, type)
on throwable -> analytics "ai_assistant_error" {error, number, messagetype="starting message", source, type}
+ "ai_assistant_starting_fallback_sent"
-> x5.p.m38846c(repo, <local opening text>, ...) // fall back to the canned line

x5.p.m38846c(...) (“startConversation”):

log "MessageAssistant", "startConversation, cli = %s"
x5.s.m38849b(new MessageAssistantFlow(now - 10000, normalized, now, type), storage); // persist
m38847d(text, number, source);
RunnableC21275c(11) -> Toast R.string.assistant_has_sent_a_message
("Eyecon AI SMS Assistant has answered this call for you")

x5.p.m38847d(text, number, source):

SmsManager.getDefault().sendMultipartTextMessage(number, null, divideMessage(text), null, null);
// on exception: analytics "ai_assistant_platform_sending_failed" {error, source}
D.5 OBSERVED - reply flow (x5.n = p646x5/C21639n.invokeSuspend, read from the JADX register listing)h6
if (!AbstractC9039h.m10664u(context)) { analytics "ai_assistant_missing_sms_permission"
{source = new FollowUp()}; return; }
for (IncomingSMSDTO dto : workerInput) {
String normalized = dto.getStartedFlow().getNormalizedNumber();
String startType = dto.getStartedFlow().getStartType();
String autoManual = dto.getStartedFlow().getTypeAutoOrManual();
long startMS = dto.getStartedFlow().getStartMS();
// --- 1. read the SMS conversation straight out of the system provider
Uri sent = Uri.parse("content://sms/sent");
Uri inbox = Uri.parse("content://sms/inbox");
String[] cols = { "_id", "address", "date", "body" };
String sel = "address LIKE ? AND date > ?";
String[] args = { "%" + n5.<7-digit-ish>(normalized) + "%", String.valueOf(startMS) };
Cursor cSent = contentResolver.query(sent, cols, sel, args, "date DESC"); // sender = Assistant
Cursor cInbox = contentResolver.query(inbox, cols, sel, args, "date DESC"); // sender = Caller
// rows are mapped to x5.i(body, x5.j "Assistant" | x5.k "Caller", date)
// --- 2. normalise into RespondSmsBody.Message list
if (rows.size() > 1) sort(rows, u7.q /* 3 */); // chronological
List<Message> messages = rows.takeLast(10).map { new Message(
row.sender == x5.j ? Role.System : Role.User,
row.body) };
// --- 3. de-duplicate: if the newest message text == the SMS we just received, skip appending
if (messages.lastOrNull()?.message != dto.getBody()) {
messages = messages.toMutableList().also { it.add(new Message(Role.User, dto.getBody())) };
}
// --- 4. keep only the last 50
int totalInSession = messages.size;
List<Message> sent50 = messages.takeLast(50);
// --- 5. POST
RespondSmsBody body = new RespondSmsBody(
normalized, // contactName == the NUMBER (bug/quirk, see D.7)
normalized,
sent50,
deviceLanguageTag,
totalInSession,
SP["username"]);
api.a(body, this);
// caught: IllegalArgumentException, retrofit2.HttpException, IOException,
// com.squareup.moshi.JsonDataException
}
// --- 6. handle the answer
if (result instanceof EndConversation) {
x5.s.m38850c(flow, storage); // forget the conversation
analytics "ai_assistant_ended_on_following_up" {number, historysize, type=auto|manual}
} else if (result instanceof NewMessage) {
analytics "ai_assistant_following_up_message_sent" {number, historysize, type}
flow = flow.copy(lastMessageMS = now) // MessageAssistantFlow.copy$default(...,11,null)
x5.s.m38849b(flow, storage);
x5.p.m38847d(msg.getMessage(), normalized, new MessageAssistantSource.FollowUp());
}
// on throwable: x5.p.m38845b(repo, ex.getLocalizedMessage(), "follow up message", normalized,
// new MessageAssistantSource.FollowUp(), startType)

Note the address LIKE '%<n>%' pattern (not =) on both provider queries - the client reconstructs the thread by substring-matching the number, which is why the filter string is built with a %-wrapped value.

D.6 OBSERVED - local conversation storage (x5.s = p646x5.C21644s)h6
  • Backing store: the native Android SP file MessageAssistantStorage (created by p511r0.C19813a(context, 22)), key STARTED_FLOW_CONTACTS, value = a Moshi List<MessageAssistantFlow> JSON.
  • m38848a(number, list) = find a flow whose normalizedNumber matches by l4.c(m38972l().m38996v(number, flow.normalizedNumber)) - i.e. a last-7-digits style comparison, not string equality.
  • m38849b(flow, storage) = remove any entry with the same normalizedNumber, prepend the new flow, cap the list at 50, write JSON.
  • m38850c(flow, storage) = remove only.
  • A static ArrayList f57884b of x5.? observers is notified on every change; each observer re-queries u7.s (SmsDB) on a background thread.
D.7 OBSERVED - the contactName quirk (worth flagging)h6

At start-flow, RespondSmsBody.contactName carries the real contact name. At reply-flow, the JADX listing is unambiguous (r17.<init>(r18, r19, r20, r21, r22, r23) with r18 = r11 and r19 = r11, both = flow.getNormalizedNumber()), so on every follow-up the backend receives the raw normalised phone number in the contactName field instead of a name. Confidence: high (direct reading of the register listing), but flagged because it could equally be a JADX register-allocation artefact - the decompiled Java is unavailable for that method, so I mark it “observed in the register listing, needs a smali check to be certain”.

D.8 OBSERVED - TTM (“Talk To Me”) - the actual voice2text serviceh6

Different subsystem, different host, different transport: HTTPS, JSON over POST/GET.

EndpointMethodBody / paramsPurpose
https://voice2text.eyecon-app.com/init-ttmPOST application/json{ "mycli": "<authenticatedCli>", "purchases": <JsonArray of the user's owned products>, "freeTrialCode": <RemoteConfig "ttm_free_trial_code"> }one-shot, guarded by comparing the body to SP SP_KEY_LAST_INIT_TTM_JSON; response parsed by n5.C18709h (TTM config/tokens)
https://voice2text.eyecon-app.com/start-ttmPOST application/json{ "mycli": "<authenticatedCli>", "acli": "<caller cli>", "calltag": "<call tag / access token, with &access_token=<…> stripped>", "purchases": <JsonArray> }begin a voicemail transcription session; success is any 2xx
https://voice2text.eyecon-app.com/ackPOST application/jsonopaque JSONacknowledge a transcribed message
https://voice2text.eyecon-app.com/getTextGET?mycli=<authenticatedCli>poll for new transcripts; result fed to u7.d
https://voice2text.eyecon-app.com/ttm-call-forwarding-resultGET?mycli=<authenticatedCli>check the test call-forwarding result
https://voice2text.eyecon-app.com/ttm-get-tokens-amountGET?mycli=<authenticatedCli>remaining token balance
https://voice2text.eyecon-app.com/no-ttmPOST application/json{ "acli": "<caller cli>", "mycli": "<authenticatedCli>" }declare “no answer / no message left”

Transcripts land in the ttm table (cis, cli, text, message_id, time_ms, is_played, is_read) via k5.b (TtmDatabase) and are rendered by Call/TalkToMe/p204UI/TtmWindowActivity. Recordings are backed up as ttm.zip -> eyecon_backup_for_ttm_audios (Flow I). p620w6.RunnableC21276d case 5 polls messages/get on the main API too, so TTM has both a dedicated host and a main-API mirror.

D.9 INFERRED - Flow Dh6
  • The assistant is an LLM-backed SMS auto-responder: the app ships the entire text of a conversation (up to 50 messages, full bodies, both directions) plus the recipient’s number and the user’s own display name to a third-party host (instabridge.com) over cleartext HTTP, and then sends the returned text as a real SMS from the user’s own phone. From the recipient’s perspective the message comes from the user’s number. Confidence: high.
  • Because the transport is cleartext HTTP and usesCleartextTraffic="true" is set app-wide, the conversation content is readable/modifiable by any on-path attacker. Confidence: high.
  • contactName being the number on follow-ups suggests either an oversight or a deliberate privacy choice (do not leak the contact’s name to a third party) - the two readings are indistinguishable from static analysis. Confidence: low which.
  • The five auto-use types and their five separate SP toggles, plus the 2-minute “reasonable call time” guard, indicate a deliberate design goal: auto-answer only for calls the user actually declined/missed/spam-blocked, never for arbitrary manual use. Confidence: high.
  • TTM is a voicemail-to-text product: the app records the callee’s missed call, the backend transcribes it, and the client polls getText to display a text version. It is paid for with token packages (purchases) and a freeTrialCode. Confidence: high.

FLOW E - CALL RECORDINGh5
E.1 OBSERVED - captureh6

The app does not use MediaRecorder. The only AudioRecord instantiation in app code is in the Toki push-to-talk stack:

// p599v7/RunnableC20908f.java:507-510 (inside wo/v7 case)
AmrEncoder.init(0);
AudioRecord audioRecord = new AudioRecord(1 /*MIC*/, 8000, 16, 2 /*MONO*/, 16000 /*buffer*/);
audioRecord.startRecording();

Call recording itself is driven through the Telecom In-call service (CallStateService, an InCallService bound with BIND_INCALL_SERVICE and IN_CALL_SERVICE_UI=true) plus the app’s own call-state machine, with Remote Config selecting the mode:

is_recording_calls_enabled_v2 (bool)
call_recording_mode (int, default 0)
recording_calls_method_mode (int, default -1)
free_rec_calls_actions (int, default 5)
SP_KEY_RECORD_CALLS_MODE (int, written as 0 by r5.r case 10 on screen teardown)

CallStateService.m10184H(app) is called from join.jsp success and from CallService.onCreate - i.e. the recorder is armed by the registration/join path, not by the After-Call screen.

E.2 OBSERVED - storageh6

p260e5.AbstractC16098a.m31460c(type):

type == 0 -> "RecordedNotes" // voice notes
type == 1 -> "RecordedCalls" // call recordings
root:
if (AbstractC0042b0.m178a0()) // "external storage" available flag
<Environment.getExternalStoragePublicDirectory(Documents)>/Eyecon/{RecordedNotes|RecordedCalls}
else
getFilesDir()/Eyecon/{RecordedNotes|RecordedCalls}

On API 29+ the files are inserted into MediaStore, not the app-private dir:

// AbstractC16098a.m31461d(name, folder)
ContentValues v;
if (SDK >= 29) {
uri = MediaStore.Files.getContentUri("external_primary");
v.put("relative_path", Environment.DIRECTORY_DOCUMENTS + "/Eyecon/" + folder); // "Documents/Eyecon/RecordedCalls"
} else {
v.put("_data", new File(<Documents/Eyecon>, folder + "/" + name).getAbsolutePath());
uri = MediaStore.Files.getContentUri("external");
}
v.put("mime_type", "audio/amr");
v.put("_display_name", name);

Format is AMR (mime_type = audio/amr; io.kvh.media.amr.AmrEncoder/AmrDecoder in the Toki path). Writing therefore requires READ/WRITE_EXTERNAL_STORAGE below API 29 and, above it, the MediaStore path (still WRITE_EXTERNAL_STORAGE on <=28, scoped storage otherwise).

Filename grammar (OBSERVED, from the discovery regex in e5.d.m31471e):

.+ __ .+ __ [0-9]+ (__s|__c)? (__-1|__1|__2)? __ (ern|erc) \..*

i.e. <name>__<something>__<timestamp>[__s|__c][__-1|__1|__2]__<ern|erc>.<ext>; ern = recorded note, erc = recorded call; the __s/__c suffix and the __-1/__1/__2 segment are parsed by e5.f.C16104f (the model) and drive is_saved / call_direction.

E.3 OBSERVED - indexing and repairh6

p260e5.C16102d (e5.d, TAG AudioRecordsDataManager):

  • m31471e(includeBroken): queries MediaStore for rows under Documents/Eyecon/Recorded{Calls,Notes}/ matching the filename regex; for each it opens the stream and reads one byte to confirm the file is non-empty (rows whose stream throws IOException are dropped).
  • m31472f(): SELECT * FROM recorded_notes ORDER BY <create_timestamp> DESC - the DB index.
  • m31474h(diskList, dbList): sorts both, binary-search matches them, copies the Uri from the disk entry onto the DB entry (so a re-index can hand a previously-file-backed row a MediaStore Uri), repairs name for type-1 rows whose name is empty/"empty name" by substituting cli, and returns the merged list.
  • m31467a(manager) (e5.d.a): requires READ+WRITE_EXTERNAL_STORAGE, then m31475i() (mkdir), m31470d() (timestamp repair), the disk scan, the merge, then insert any disk-only rows into recorded_notes inside one transaction, and for type-1 rows whose name does not contain "__s__" enqueues RunnableC16101c to export them somewhere (the share job).
  • m31470d() (fixTimestampForDB): a one-shot migration for installs with SP_KEY_ORIGNALY_INSTALL_VERSION <= 385, guarded by SP_KEY_FIXED_RC_TIMESTAMP, that rewrites the file_name column (AbstractC0039a.f224h1).
  • m31468b(entry) = delete: deletes the file (or MediaStore row) then DELETE FROM recorded_notes WHERE <create_timestamp> = ?. Returns -2 if the file delete failed.
  • m31469c(entry) = update (is_played, is_saved, text_note, …).
  • First run posts SP_KEY_IS_CALL_RECORDING_ACTIVITY_ENABLE and enqueues RunnableC0070p0(14) to show the recordings UI once.
E.4 OBSERVED - uploadh6

There is no call-recording upload path in this build. The only “voice evidence” prefix in the artifact is https://eyecon-app.com/vrs/evid-<id> (DEX string), which is used by the viral/invite flow (j5.c.m33579b returns https://eyecon-app.com/vrs/evid-<viralId> as a share link), not by recording upload. Recordings leave the device only via:

  • the Google Drive backup (eyecon_backup_for_calls_records -> Drive appDataFolder), and
  • the OS share sheet / MediaStore visibility.
E.5 OBSERVED - retentionh6

No retention/pruning policy was found. e5.d exposes create / read / update / delete and a one-shot timestamp repair, but no age- or size-based purge of recorded_notes or of the RecordedCalls directory in any code path. The only pruning in the audio-adjacent code is RunnableC21275c case 10, which trims the SP SP_AFTER_CALL_LOGS array (after-call UI log, not audio).

E.6 INFERRED - Flow Eh6
  • Recording is opportunistic and best-effort: on modern Android, MediaRecorder-free capture through InCallService only works on OEMs that expose a call-audio source, which is consistent with recording_calls_method_mode = -1 (“auto”) and the vendor-specific permission dance in j7.t (R.string.one_per_audio_rec, R.string.recording_permission_for_toki). Confidence: medium.
  • has_photo/ab_photo_type-style flags are absent for audio, but call_direction (default -1, fixed to 2147483647 in migration v41->42) suggests the recording pipeline knew the direction but had no reliable source for it. Confidence: medium.
  • The absence of retention is a storage-privacy concern: recordings and their transcripts accumulate indefinitely on device and are swept wholesale into Google Drive by the backup flow. Confidence: high.

FLOW F - CONTACT SYNC (addcontacts.jsp, getupdatedcontactsinfo.jsp, resetupdatedstatus.jsp)h5
F.1 OBSERVED - the driverh6
DBContacts (com.eyecon.global.Contacts.Database.DBContacts)
addressBookUpdated(ArrayList<C16470g0> changed, ArrayList<C16470g0> deleted,
ArrayList<C16470g0> photoApproved) // "r5." diff
m10139d0(String tag, ArrayList changed, ArrayList deleted,
ArrayList photoApproved, boolean force) // tag e.g. "app_started", "<x>-resume"

Triggered from:

  • DBContacts init (line ~3640) with tag "app_started";
  • p516r5.RunnableC19866k on Activity resume with tag "<name>-resume" / "<name>-resume2";
  • bm.RunnableC3686q0 case 12 which performs the actual network push.

m10139d0 (OBSERVED, lines 3477-3523):

boolean afterRestore = SP.getBoolean("SP_KEY_IS_AFTER_RESTORE", false);
if (changed.isEmpty() && deleted.isEmpty() && !afterRestore) {
m10115r(); // full re-read
m10143p(false, tag + "2", true, false, null); // full re-upload
return;
}
// after a restore, re-upload *everything* the local DB has:
if (afterRestore) for (C19862i c : contactMap.values()) changed.add(c.m36623d(c.phone_number_in_server));
else for (C16470g0 g : changed) {
C19862i c = contactMap.get(g[contact_id]);
if (c != null) changedOut.add(c.m36623d(g[cli_in_server]));
}
if (!deleted.isEmpty()) new RunnableC20065s(deleted).run(); // -> removecontacts.jsp
if (!changedOut.isEmpty()) new RunnableC3686q0(changedOut, force, 12).run(); // -> addcontacts.jsp
SP: SP_KEY_IS_AFTER_RESTORE = false
for (C16470g0 g : photoApproved)
C21280h.m38431d([g[cli]], g[name], "yes", new String[1]); // -> picapproval.jsp
F.2 OBSERVED - the @z@ payload format (the record encoder)h6

bm/RunnableC3686q0.m3977b(ArrayList<C19862i>) (bm.q0.b) builds the payload:

StringBuilder sb = new StringBuilder();
boolean first = true;
boolean tellServerContactHasNoPhoto = SP.getBoolean("SP_TELL_SERVER_CONTACT_HAS_NO_PHOTO", true);
if (tellServerContactHasNoPhoto) SP.putBoolean("SP_TELL_SERVER_CONTACT_HAS_NO_PHOTO", false); // one-shot
for (C19862i c : contacts) {
if (!first) sb.append("@z@"); // <-- RECORD separator
first = false;
String hasPhoto = (!tellServerContactHasNoPhoto
|| (c.ab_photo_type != 1 && c.ab_photo_type != 3))
&& c.hasPhoto ? "1" : "0";
sb.append(c.phone_number_in_server) // <-- field 0: cli_in_server
.append("@a@").append(c.private_name) // <-- field 1: contact display name
.append("@a@").append(hasPhoto) // <-- field 2: has-photo flag
.append("@a@");
ArrayList<C19859g0> social = c.mSocialManager.f51218a;
if (social.isEmpty()) sb.append("0@a@"); // <-- field 3: social count = 0
else {
sb.append(social.size()).append("@a@"); // <-- field 3: social count
for (C19859g0 s : social)
sb.append(s.socialEnum.f51206b) // <-- field 4: social TYPE (int)
.append("@a@").append(s.socialID) // <-- field 5: social ID (string)
.append("@a@0@a@"); // <-- fields 6,7: padding
}
}
return sb.toString();

Grammar (OBSERVED):

contacts := record ("@z@" record)*
record := cli_in_server "@a@" private_name "@a@" hasPhoto "@a@" socialSection
socialSection := "0" "@a@"
| count "@a@" (socialType "@a@" socialId "@a@" "0" "@a@"){count}

removecontacts.jsp uses the same idea in miniature (bm/RunnableC20065s default branch, OBSERVED):

cli_in_server "@a@" name "@z@" (repeating; de-duplicated through a HashSet)
F.3 OBSERVED - addcontacts.jsp requesth6
w6.h.m38428a(String contactsPayload, boolean init)
if (payload empty) -> log "addContactsList empty addedList"; return
if (!AbstractC9039h.m10665v()) return // must be a registered Eyecon user
ArrayList p:
"contacts" = <the @z@/@a@ blob>
"cli_cc" = TelephonyManager.getSimCountryIso().toLowerCase()
"init" = init ? "1" : "0"
"possible_langs"= a7.l0.m356k1().toLowerCase()
"apsVersion" = vc_749_vn_4.2026.04.23.1145_a
new x6.d(url = base + "addcontacts.jsp", "POST", init, 2 /*FormPost*/, "addcontacts.jsp")
x6.b.enqueue(request) // DURABLE QUEUE -> table_api_requests
// side effect: after enqueueing, decide whether to refresh the language
int records = payload.split("@z@").length;
ArrayList existing = DBContacts.f11724b.m32073e(...);
if (SP.getBoolean("contactLangNeedUpdate", false)
|| records > 20
|| (existing.size() > 0 && (records * 100) / existing.size() > 5)) {
SP.putBoolean("contactLangNeedUpdate", true);
GET base + "getcontactslang.jsp?apsVersion=..."
if (200 && body matches "[a-zA-Z]{2}") {
SP.putBoolean("contactLangNeedUpdate", false);
SP.putString("contactLang", body);
f7.j0.m32051f(); // reload the quotes client in the new language
if (f7.j0.f40917c != body && SP["SP_KEY_SELECTED_QUOTES_LANGUAGE_v1"].isEmpty())
-> server call to re-fetch quotes for that language
}
}

Batching / init semantics (OBSERVED, bm.RunnableC3686q0 case 12):

String payload = m3977b(contacts);
int remaining = SP.getInt("SP_KEY_INIT_ADD_CONTACTS_COUNT", 0);
C21280h.m38428a(payload, remaining > 0 || force);
if (remaining > 0) SP.putInt("SP_KEY_INIT_ADD_CONTACTS_COUNT", remaining - 1);

So init=1 is sent only for the first N uploads of a first-contact-dump burst, where N is decremented from SP. There is no explicit chunk size limit in the code: the whole changed-set is serialized into one string and posted as one request. Practical chunking is therefore whatever m10139d0’s caller passes in. Confidence: high that there is no chunker in w6.h.a; medium on the upstream batching.

F.4 OBSERVED - getupdatedcontactsinfo.jsp (the delta pull)h6

p607vk/RunnableC21093i.m38227a() (vk.i), invoked as case 26:

if (!AbstractC9039h.m10665v()) { run(); return; }
ArrayList p = { ("apsVersion", "vc_749_vn_4.2026.04.23.1145_a") };
HashMap headerResult = new HashMap();
String body = C21273a.m38423g(p, base + "getupdatedcontactsinfo.jsp", "GET",
new int[1], false, "getupdatedcontactsinfo.jsp",
null, /*addAuth*/true, false, headerResult, "");
if (httpCode == 200) w6.h.m38432e(headerResult); // reads header "sms"; if > 0 -> ExternalSmsDB sync + SP sp_new_external_sms_in_server = true
for (String record : body.split("@z@")) {
String[] f = record.split("@a@");
if (f.length < 3) continue;
C16470g0 c = new C16470g0();
c.put("cli_in_server", f[0]);
c.put("is_viewer", Integer(f[1]));
c.put("os_type", Integer(f[2].isEmpty() ? 0 : f[2]));
outContacts.add(c);
if (f.length > 3) {
int socialCount = Integer.valueOf(f[3]);
int i = 4;
for (int k = 4; k < (socialCount * 2) + 4 && f.length > (i = k + 1); k += 4) {
if (!f[i].isEmpty()) {
if (f[k] == null) f[k] = "";
if (f[k].equals("null"))
throw new RuntimeException("Social type value is 'null' - should be integer");
C16470g0 s = new C16470g0();
s.put("social_type", Integer(f[k]));
s.put("social_id", f[i]);
s.put("social_name", "0");
s.put("cli_in_server", f[0]);
s.put("social_source", 1); // 1 = "from server"
outSocials.add(s);
}
}
c.put("is_sub", f[i]);
if (i + 1 < f.length) c.put("is_can_talk_enable", Integer(f[i+1].isEmpty() ? 0 : Integer.valueOf(f[i+1])));
if (i + 2 < f.length) c.put("vn", Integer.parseInt(f[i+2]) or 0);
}
}
run();

Delta record grammar (OBSERVED):

record := cis "@a@" is_viewer "@a@" os_type "@a@" [ socialCount "@a@" (socialType "@a@" socialId){socialCount}
"@a@" is_sub "@a@" is_can_talk_enable "@a@" vn ]
F.5 OBSERVED - resetupdatedstatus.jsph6

p607vk/RunnableC21082e0 case 25 (vk.e0):

ArrayList p = { ("apsVersion", "vc_749_vn_4.2026.04.23.1145_a") };
GET https://api.eyecon-app.com/app/resetupdatedstatus.jsp
boolean[] success = {false};
if (body != null && body.length() > 0) success[0] = (httpCode == 200);
runnable.run();

No body, no callback - it is a fire-and-forget “I have applied the delta” ping.

F.6 OBSERVED - notifyaddcontact.jsph6

as/RunnableC3316o (the shared as.o mega-runnable), case around line 100-122: a GET to base + "notifyaddcontact.jsp" with apsVersion and cv params, fired when a locally-created contact is pushed (the “tell the backend a new contact appeared” ping, distinct from the full addcontacts.jsp upload).

F.7 OBSERVED - external SMS deltah6

w6.h.m38432e(HashMap responseHeaders):

if (!headers.containsKey("sms") && !SP.getBoolean("sp_new_external_sms_in_server", false)) return;
if (Integer.parseInt(headers.get("sms")) > 0) {
u7.d (ExternalSmsDB) -> server thread, RunnableC21276d(new u7.e(...,23), 5)
SP: sp_new_external_sms_in_server = true
}

i.e. the server signals, via a response header on a normal API call, that the user’s SMS inbox changed on another device; the client then pulls it (u7.b.insertWithOnConflict(..., CONFLICT_REPLACE)).

F.8 INFERRED - Flow Fh6
  • Contacts leave the device on every app start and on every Activity resume, as one @z@-delimited blob per changed set. The blob contains, per contact: the E.164-ish server-formatted number, the user’s private contact name, a has-photo bit, and the count + type/ID of every linked social account. Confidence: high.
  • init is a first-upload marker so the backend can distinguish “initial address-book dump” from “incremental change”; it is only sent while SP_KEY_INIT_ADD_CONTACTS_COUNT > 0. Confidence: medium (the SP counter’s writer was not traced; only the decrement was found).
  • SP_TELL_SERVER_CONTACT_HAS_NO_PHOTO is a one-shot privacy switch: on the very first upload the client explicitly tells the backend that contacts without photos are “no photo” (so the backend does not later treat the missing photo as unknown), then flips the flag off so subsequent uploads send the raw has_photo bit. Confidence: high (the flag is read and immediately cleared in the same method).
  • getupdatedcontactsinfo.jsp propagates changes made on the user’s other devices / the web client (viewers, subscribers, CanTalk state, vn version) back into this device. Confidence: high.
  • The uploads are durably queued (table_api_requests) rather than issued inline, so a contact added while offline is uploaded on the next ApiRequestWorker run with no data loss. Confidence: high.

FLOW G - BILLING / PREMIUMh5
G.1 OBSERVED - the Google Play layerh6

Manifest: <meta-data android:name="com.android.play.billingclient.version" android:value="7.1.1"/> plus com.android.billingclient.api.ProxyBillingActivity and ...ProxyBillingActivityV2. p303g5.C16707i (g5.i, TAG EyeconBilling) wraps BillingClient (p489q1.C19537c):

  • CB_KEY_OWNED_PURCHASE (String), CB_KEY_OWNED_PRODUCT - the two SP-ish keys for the locally cached entitlement.
  • f41457k = TimeUnit.DAYS.toMillis(30) - the 30-day cache TTL.
  • m32521b(billing, productInfo, purchase) = acknowledgePurchase: reads purchase.optBoolean("acknowledged", true); if already acknowledged -> no-op; requires purchase.getPurchaseState() == PURCHASED; reads optString("token", optString("purchaseToken")) and calls acknowledgePurchase.
  • m32522c(billing, ?, callback) = getAllPurchases: queryPurchasesAsync("inapp") and queryPurchasesAsync("subs"), joined via a countdown AtomicInteger (i41465g), then a completion runnable.

p303g5.C16708j (g5.j, PurchasedProductInfo) fields (OBSERVED in j5.k): f41470e = productId, f41472g, f41473h (Strings, part of the equality comparison), f41475j = int, plus m32540c() returning the JSON written to SP.

G.2 OBSERVED - the tiersh6

Tiers are fully Remote-Config driven; there are no hard-coded SKU tables. Defaults compiled into d5.d:

// "premium_products"
{ "main_product": {"sku":"yearly_25_jul_2021", "type":"subs","enable":true},
"second_product": {"sku":"monthly_4.99_jul_2021", "type":"subs","enable":true},
"third_product": {"sku":"quarterly_15_aug_2021", "type":"subs","enable":true},
"free_premium": "third_product",
"highlight_product":"main_product" }
// "premium_products_v2"
{ "main_product": {"sku":"golden_user_test_product","type":"subs","enable":true},
"second_product": {"sku":"yearly_25_jul_2021", "type":"subs","enable":true},
"third_product": {"sku":"monthly_4.99_jul_2021", "type":"subs","enable":true},
"fourth_product": {"sku":"quarterly_15_aug_2021", "type":"subs","enable":true},
"free_premium": "fourth_product",
"highlight_product":"second_product" }

Also relevant: premium_products_ids_list = {"subs":[...],"inapp":[...]} (validated by j5.k.m33586b(productId)); golden_users_products = ["golden_user_test_product"]; no_ad_lifetime_product_id = "no_ad_lifetime_5.99", no_ad_yearly_product_id = "no_ad_yearly_2.99", no_ad_monthly_product_id = "no_ad_monthly_1_dollar"; premium_sub_sku = "no_ad_yearly_2.99_free_trial"; premium_options_type = "2 tabs". paywall_ui ("man_with_cards" -> PremiumPurchasingPaywallUiActivity, otherwise PremiumPurchasingDefaultActivity) selects the screen. Term normalisation is in WebviewPaywallActivity.m10025C0: P1D->daily, P1W->weekly, P1M->monthly, P3M->quarterly, P1Y->yearly, P6M->half_yearly.

Premium features (defaults from premium_features):

{ "reverse_lookup": {"is_premium_feature":true,"amount_for_free":2,"dialog_unlock_options":"premium and video"},
"photo_search": {"is_premium_feature":true,"amount_for_free":2,"dialog_unlock_options":"premium and video"},
"bg_colors": {"is_premium_feature":true,"amount_for_free":0,"dialog_unlock_options":"video"} }
G.3 OBSERVED - the two paywall renderersh6
  1. Native: com.eyecon.global.Billing.Premium.AbstractActivityC5847a reads Remote Config premium_products and pulls free_premium / highlight_product keys, then g5.i.m32534m(products, new C17507v(this,0)) to bind the list. m10027E0-style helper on the WebView variant throws PaywallException("product not found product = <id>") if a Play product has no matching paywall entry.
  2. WebView: WebviewPaywallActivity + C5835b (paywall config) + C5834a (a product group) + C5846m (WebViewClient).
    • m10026D0() requires an intent extra named "placement_id" (FacebookMediationAdapter.RTB_PLACEMENT_PARAMETER) and throws RuntimeException("missing placement_id") / RuntimeException("missing paywall") if the Remote Config JSON for that key is absent.
    • C5835b.f11565a is the paywall URL; WebviewPaywallActivity.f11551V = Uri.parse(url).getHost().replace("www.","") and that host is then used as the allow-list domain in C5846m (host.equals(f11551V) or host.endsWith("." + f11551V)), so navigation off the paywall origin is blocked (except an explicit https://m.facebook.com/profile.php carve-out at line 111).
    • After initProductsInfo the WebView is loadUrl(C5835b.f11565a).
    • Runtimerecord: C5846m.m3503v-style sends are RunnableC21275c(11) etc. Purchase completion inside the page is relayed by C5844k, which handles the “pending purchase” toast (R.string.pending_purchace_toast).
  3. Registration interpose: RegistrationActivity.m10529Q0 shows paywall_premium_reg_get_started only when Remote Config registration_premium_location == "get_started" and the local premium flag is not true.
  4. Free premium (no purchase required): j5.c (FreePremium) awards time-limited premium from three sources, each writing SP_KEY_LAST_FREE_PREMIUM_TYPE and SP_KEY_LAST_DATE_FOR_FREE_PREMIUM:
    • viral invite (SP_KEY_LAST_DATE_FOR_VIRAL_PREMIUM, m33585g(amountMs, reason)),
    • rewarded video (m33582f(amountMs) -> type "video"),
    • contest (m33584e(amountMs)). C17487c.m33580c() is the entitlement check (lastDateForFreePremium > now). FreePremiumUserActivity is reachable from an external intent action EYECON_PREMIUM_UPDATE and from the FCM notification Free premium update (R.string.free_premium_noti_title / _msg, with [XXX]-templated inviter name).
G.4 OBSERVED - backend-side entitlement check (this is the important bit)h6

There is a server-side entitlement record, and the client treats it as authoritative over the local Play cache:

// p358j5/C17496k.java ("j5.k" = PaidPremium) static initialiser
SP_KEY_PREMIUM_OWN_CACHE -> Boolean (local Play-derived cache, e.g. true)
SP_KEY_LAST_PREMIUM_UPDATE_CACHE -> Long (epoch ms)
SP_KEY_PREMIUM_PURCHASES_INFO_CACHE -> Json (g5.j PurchasedProductInfo)
if (SP_KEY_PREMIUM_OWN_CACHE == null)
-> wait for the server settings blob (blocks on C16464d0.f40869b listener list)
// p284f7/C16464d0.java ("f7.d0" = ServerMySettings, TAG "MySettingsInServer")
JSONObject f40868a loaded from SP "SP_KEY_MY_SETTINGS_LOCAL_JSON"
if registered and SP "SP_KEY_MY_SETTINGS_SERVER_JSON" == null -> m31984b(null) // pull from server
-> w6.d case 8 -> GET https://api.eyecon-app.com/app/mysettings
(response text handed to a C3502i callback)
m31983a() = write f40868a back to SP "SP_KEY_MY_SETTINGS_LOCAL_JSON"
// p261e6/RunnableC16126b case 19 ("waitForServerUpdate")
long premium_cache_time = f7.d0.f40868a["premium_cache_time"];
if (premium_cache_time == 0) -> "server cache is not exist - nothing to update"; return;
if (now - premium_cache_time >= 30 days) -> "server cache is not valid"; return;
JsonObject premium_cache_product = f7.d0.f40868a["premium_cache_product"];
if (premium_cache_product is not a JsonObject) -> "server cache say not premium user"
else -> "server cache say user has premium"
-> j5.k.m33590d(productInfo, premium_cache_time)
// p358j5/C17496k.m33588e(now, productId) -- push entitlement to the server
if (f7.d0.f40868a["premium_cache_product"] == productId
&& 1 day + premium_cache_time >= now) return; // already fresh
f7.d0.f40868a["premium_cache_product"] = productId;
f7.d0.f40868a["premium_cache_time"] = now;
f7.d0.m31983a(); // triggers w6.d case 8 (GET mysettings)

So the client round-trips its own entitlement into the server-side mysettings blob as premium_cache_product (a JSON product descriptor) + premium_cache_time, and then reads it back to decide premium state when the Play cache is stale or missing. The 30-day (g5.i.f41457k) local TTL and the 1-day server-push suppression window are separate.

Also observed: j5.k.m33589a(localFlag) returns the cached flag only if now - SP_KEY_LAST_PREMIUM_UPDATE_CACHE < 30 days, else falls back to the passed-in local value; m33590d additionally rate-limits its own writes to once per hour.

Other entitlement consumers: j5.c.m33579b(callback) returns the viral landing page https://eyecon-app.com/vrs/evid-<viralId>; w6.d case 2 GET virality/premium_status returns a day count (multiplied by 1000 to get ms) that j5.c.m33585g then credits as viral premium; w6.d case 9 GET virality/viral_id_check and case 6 GET get_store_products round out the set.

G.5 INFERRED - Flow Gh6
  • Premium is subscription-first (type: "subs" on all default tiers) with a lifetime in-app tier kept in config but not in the default set; the actual SKU catalogue lives entirely in the Play Console and is mirrored by Remote Config. Confidence: high.
  • The design is entitlement-by-two-sources with a server tiebreaker: Play Billing is the source of truth at purchase time, the server-side mysettings blob is the tiebreaker when Play is unreachable or the cache is stale, and SP_KEY_PREMIUM_OWN_CACHE is a 30-day local mirror. Confidence: high.
  • Because premium_cache_product is written by the client into a blob the client also reads, and there is no signature on it, a rooted/hooked client could self-grant premium. Confidence: medium (no server-side verification code path was found; acknowledgePurchase is the only Play-side proof, and its result is not sent to the API).
  • The “free premium” track (viral + rewarded video + contest) is the acquisition funnel for the subscription, and is tracked with a single SP_KEY_LAST_FREE_PREMIUM_TYPE so the UI can tell the user why they currently have premium. Confidence: high.

FLOW H - SECOND NUMBER / TOKI / PHONE ACCOUNTh5
H.1 OBSERVED - the UDP service (ptt.eyecon-app.com)h6

Toki is a push-to-talk walkie-talkie. There is no HTTP control plane for the media; signalling and audio ride a single custom UDP protocol.

Host and port (OBSERVED):

// p703z7/C22122v0.java:419 (z7.v0 constructor)
this.f60476s0 = "ptt.eyecon-app.com"; // overridable via
// ...line 1994
this.f60476s0 = arguments.getString("INTENT_KEY_SERVER_ADDRESS", "ptt.eyecon-app.com");
// p720zq/AbstractC22312g.m39737C() ("zq.g" — port picker)
JSONArray ports = RemoteConfig.m31091l("toki_ports", false); // default "[5620,50000-50010]"
for (String entry : ports) {
if (entry.contains("-")) {
String[] ab = entry.split("-");
int lo = parseInt(ab[0]), hi = parseInt(ab[1]);
if (lo == hi || lo > hi) { list.add(lo); continue; } // fixed port
// range -> a random port inside [lo, hi] is collected
} else list.add(Integer.parseInt(entry));
}
// -> returns one random port from the collected list

There is no TLS and no certificate validation - this is plain java.net.DatagramSocket.

Transport (OBSERVED, p687yl.C21952k = yl.k, the “Talki” UDP transport):

DatagramSocket socket = new DatagramSocket(); // ephemeral local port
socket.setSendBufferSize(...);
new C21952k(socket, remoteInetAddress, remotePort);

Every packet has a 15-byte binary header followed by an opaque payload:

byte[0] : (unused / flags)
byte[1] : message type (0 = control/ack, 1 = data, 2 = FEC-repair)
byte[2..5] : session sequence number (int, big-endian) m35480z(data, 2, n)
byte[6] : flags bitfield
0x08 = "control/empty"
0x40 = piggyback acknowledgement
0x80 = connect
0x02 = "this packet is an XOR of the previous one" (set in data[6] in practice)
byte[7..10] : packet (fragment) sequence number int, big-endian m35480z(data, 7, n)
byte[11..14] : acknowledged packet sequence int, big-endian m35480z(data, 11, n)
byte[15...] : payload

Mechanics (all OBSERVED in yl.k):

  • Every packet is sent twice (datagramSocket.send(p); datagramSocket.send(p);).
  • FEC: if a payload packet is retransmitted, the retransmission is XORed byte-for-byte against the previously sent packet (for (i=16..) data2[i] ^= prev[i]) and data[16] = 2 marks it as a repair. The receiver (m39300d) reassembles by XORing a repair against its stored neighbour.
  • ACK piggybacking: m39302f() packs up to N received sequence numbers into the outgoing control packet’s byte[11..14] field before sending.
  • Retransmission: every outstanding packet is registered with a RunnableC21943b timer at (f59241d * 50) + 200 ms and re-sent until acked.
  • Reassembly buffer sizes: DatagramPacket[65536] for control, DatagramPacket[1000] for audio.
  • Audio payload: AudioRecord(MIC, 8000 Hz, 16-bit, MONO, buf 16000) -> io.kvh.media.amr.AmrEncoder.init(0) -> AMR frames in the UDP payload; playback is AudioTrack + AmrDecoder.
  • Threads: network.UDPClient (socket read), client.Recorder, client.Player.

Session establishment (OBSERVED, as.C3349u2 = as.u2, log-tag "TalkiServer"):

byte[] b = new byte[15];
b[1] = 0; // control
b[6] |= 0x80; // CONNECT flag
c21952k.f59285A = new Random().nextInt(255); // session id byte 0
c21952k.f59310w = new Random().nextInt(255) + 1; // first packet seq
m35480z(b, 7, seq);
c21952k.m39303g(new C21942a(datagramPacket(b, 15)));

Signalling messages (OBSERVED, p633wl.C21450m.m38606c + the m3503v(type, json, cb, runnable) sender):

// wl.m.m38606c(myCli, cid, cidChecksum)
{ "cli": myCli, "vn": "4.2026.04.23.1145", "vc": "749",
"e-auth-v": "e1", "e-auth": <clientId>, "e-auth-c": <checksum(CID)> }
Message typeExtra JSON fieldsMeaning
CHANNEL_INVITEinvited_clis: [...]invite a set of contacts into a channel (m38607a)
CHANNEL_JOINchannel_id: Int, invitation_id: Intaccept an invitation (m38609d)
INVITE_REJECTchannel_id, invitation_id, cli, type: Intdecline (m38610e)
TALK_DONEchannel_id, session_idend the session (m38612g)
CHANNEL_BROADCAST_COMMANDsound_fx_id: Int, cli, channel_id, session_id, type: "playSoundFX"UI sound effect (m38613h)

Note that the e-auth triple is re-implemented in JavaScript-land inside the UDP payload, i.e. the same weak account credential that the HTTP layer uses is what authenticates the UDP session. (OBSERVED; the checksum is computed elsewhere - see BACKEND-API.md §3.1 for the algorithm.)

H.2 OBSERVED - number provisioningh6

Toki does not provision a phone number. (INFERRED, high confidence - see below.) There is no HTTP endpoint anywhere in the decompiled tree that asks the server for a second number, and no cli_auth/* or *number* request outside registration. The identity used is the app’s own authenticated CLI (SP["authenticatedCli"]):

// p703z7/C22122v0.java:2419-2436 (TokiFragment.connect)
if (SP.getString("authenticatedCli", null) == null) {
failureReasons.add("sender unauthorized user");
analytics "Toki - connect canceled not eyecon user";
return false; // Toki requires a REGISTERED user
}
this.f60436P = new C21450m(this, app, l7.b.m34444e(authenticatedCli));
c21450m.f38608b(this.f60476s0, AbstractC22312g.m39737C()); // connect to ptt.eyecon-app.com:<random port>

Channel/invitation ids come from push, not from a provisioning API: the notification intent carries INTENT_KEY_CHANNEL_ID and INTENT_KEY_INVITATION_ID, read in z7.v0’s argument parsing (f60434N = arguments.getInt("INTENT_KEY_CHANNEL_ID", -1), f60433M = arguments.getInt("INTENT_KEY_INVITATION_ID", -1)). If no Toki session is alive, z7.v0.m39382O0 falls back to a push notification whose deep link carries the participant JSON.

The nearest thing to “a second number” in the app is C19862i.phone_number_in_server - a virtual/E.164 number that the backend assigns per contact so the app can be addressed without exposing the SIM number, plus AddressBook.DefaultPhoneAccountType / DefaultPhoneAccountName / ...V2 SP keys. That is an addressing scheme inside Eyecon, not an SMS-capable number.

H.3 OBSERVED - the Toki service and sensorsh6

TokiService (foreground, specialUse):

  • Channel toki_service, title “Eyecon”, text R.string.toki_noti_text, tap intent TokiActivity with action INTENT_ACTION_REFOCUS_TOKI.
  • onCreate registers a SensorManager listener for sensor type 8 (TYPE_PROXIMITY) at SENSOR_DELAY_NORMAL, caching maximumRange. onSensorChanged: when the raw value < maximumRange the phone is near the ear -> broadcast com.eyecon.global.isonear with isonear=true (edge-triggered, f13077c latch); when it goes back above -> isonear=false.
  • onStartCommand returns 1 (START_STICKY) only if C22122v0.m39381H0() reports at least one live Toki session; otherwise stopSelf().
  • onBind returns a bare p315h7.BinderC16946j() (no real AIDL surface).
H.4 OBSERVED - the PhoneAccount / contacts sync account is dead codeh6
com/eyecon/global/PhoneAccount/SyncService.java
onCreate() -> if (f12419a == null) f12419a = new p381k7.C17791c(getApplicationContext(), true);
onBind() -> f12419a.getSyncAdapterBinder();
// p381k7/C17791c.java ("k7.c")
public final void onPerformSync(Account, Bundle, String, ContentProviderClient, SyncResult) {
} // <-- EMPTY BODY

So the Eyecon AccountManager account (res/xml/eyecon_contacts_account_auth.xml) is registered and offered to the user, and AccountWithDataSet builds a ContactsContract.RawContacts URI with ?limit=1, but no contact is ever synchronised through it. All contact sync happens through Flow F.

EyeconAccountAuthService (p381k7.C17789a) is the AccountAuthenticator; AccountWithDataSet is a Parcelable triple (accountName, accountType, [null]).

H.5 INFERRED - Flow Hh6
  • “Second number” in the product sense is implemented as Eyecon-internal virtual addressing (phone_number_in_server), not as a provisioned SIM/eSIM number. There is no carrier-SRV or number-port code in the artifact. Confidence: high (negative evidence: no requesting endpoint; positive evidence: phone_number_in_server is present on every contact row and is what cli, getnames.jsp, pic?cli= and join.jsp all use).
  • Toki’s channel/invitation rendezvous is push-driven: FCM carries the channel_id/invitation_id/participants payload, and the actual media session is then negotiated out-of-band over UDP. That means Toki works while the app is foregrounded but the signalling plane still depends on Google’s or Huawei’s push infrastructure, and the media plane is unauthenticated transport with a weak in-payload credential. Confidence: high.
  • The presence of a proximity sensor in TokiService is a push-to-talk UX affordance (screen off / speaker off when the handset is at the ear), not an anti-fraud signal. Confidence: high.

FLOW I - BACKUP / RESTORE (Google Drive appDataFolder)h5
I.1 OBSERVED - entry point and scope requesth6
com.eyecon.global.Backup.BackupActivity (android:screenOrientation="portrait")
onCreate:
String json = intent.getStringExtra("EXTRA_BACKUP_INFO_JSON_STR");
if (empty) json = SP.getString("SP_KEY_LAST_TIME_BACKUP", ""); // local descriptor
if (still empty) { log BackupOrRestoreException("missing EXTRA_BACKUP_INFO_JSON_STR"); finish(); }
f11520U = Gson().fromJson(json, JSONObject) // {st_ts, account}
f11521V = intent.getBooleanExtra("forTest", false)
-> bm.RunnableC3686q0(this, intent.getBooleanExtra("EXTRA_RETRY", false), 1)
- if not retrying -> m10014C0()
- if retrying -> dialog R.string.backup_restore_title / R.string.restore_backup_retry_msg
with "yes" -> retry, "no" -> SP SP_KEY_STARTING_RESTORE_PROCESS=false; finish()
// BackupActivity.m10014C0() -> show f5.j (p282f5.C16426j) as "BackupRestoreDialog", not cancelable

Scope (OBSERVED, f5.j.onActivityCreated):

this.f40707p = GoogleSignIn.getClient(activity,
new GoogleSignInOptions.Builder(GoogleSignInOptions.DEFAULT_SIGN_IN)
.requestScopes(new Scope("https://www.googleapis.com/auth/drive.appdata")) // appDataFolder ONLY
.requestEmail().build());
if (AbstractC16422f.m31913c(activity) != null) m31924v0(); // already signed in -> straight to backup
// onActivityResult(110): if grantedScopes contains drive.appdata -> store account + m31923u0()

The UI shows the connected Google account (R.string.using_drive_account_xx) and the last backup time formatted from f40714w["st_ts"].

I.2 OBSERVED - what is included (six Drive artefacts)h6

p282f5.AbstractC16422f.f40666B (OBSERVED verbatim):

public static final String[] f40666B = {
"eyecon_backup_for_ttm_audios",
"eyecon_backup_for_calls_records",
"eyecon_backup_for_notes_records",
"eyecon_backup_for_database",
"eyecon_backup_for_call_logs",
"eyecon_backup_for_address_book"
};
Drive folder nameLocal sourceWhat it is
eyecon_backup_for_databasecontext.getDatabasePath("contactdb")the entire app SQLite database, byte-for-byte - all 21 tables including contacts (names + numbers + social ids), history, social, sms_table (full SMS bodies), db_table_notification_reader (notification text), recorded_notes, ttm transcripts, spam_list, block_list, shared_preference_v2
eyecon_backup_for_calls_recordsfiles/calls/ (filtered C16100b(1))call recordings (AMR files whose names contain ..erc.)
eyecon_backup_for_notes_recordsfiles/notes/ (filtered C16100b(0))voice notes (..ern.)
eyecon_backup_for_ttm_audiosfiles/ttm.zipTalk-To-Me voicemail audio, zipped, uploaded with Content-Type: application/zip
eyecon_backup_for_call_logsfiles/call_logs/the archived system call log (CallLog.Calls export)
eyecon_backup_for_address_bookfiles/address_book/the raw address-book export

Additionally there is a transient staging folder on the restore path, databases/eyecon_restore_temp_database.

I.3 OBSERVED - upload (f5.z = p282f5.C16442z)h6
  1. Phase 0 (parallel, 6 workers): create/move each of the 6 pending_temp_<name> Drive folders (C16418b0.m31906a(name)).
  2. Phase 1 (parallel, 6 workers): prepare each artefact locally (RunnableC16439w cases 1..5).
  3. Phase 2: m31915d(3) -> spin up 5 named threads: BU_database_thread, BU_address_book_thread, BU_call_log_thread, BU_records_thread, BU_ttm_thread.
  4. Phase 3 (mo31914b, 95 %): upload each artefact into its pending_temp_<name> folder.
  5. Phase 4 (96 %): delete the local staging files.
  6. Phase 5 (97 %): build the descriptor and publish it:
JSONObject d = new JSONObject();
d.put("st_ts", System.currentTimeMillis());
d.put("account", googleAccountEmail);
C21280h.m38440m("backup_info", "backup_info"/*key*/, d.toString(), /*highPriority*/true, callback);
// -> p572u4.RunnableC20597o case 7:
// POST/GET https://api.eyecon-app.com/app/mysettings
// type=backup_info & key=<epoch-ms> & value=<json> & apsVersion=...
SP.putString("SP_KEY_LAST_TIME_BACKUP", d.toString());
  1. Phase 6 (98-100 %): finish.

ttm.zip is built with C16100b/zip filters and uploaded via C16418b0.m31907b(localZip, progressCallback) after being staged at files/ttm.zip.

I.4 OBSERVED - restore (f5.r = p282f5.C16434r)h6
  1. Prepare local dirs: wipe/recreate files/notes, files/calls; set f40747F = getDatabasePath("contactdb"), f40677j = databases/eyecon_restore_temp_database.
  2. GET https://www.googleapis.com/drive/v3/files?spaces=appDataFolder&fields=nextPageToken,files(id,name,size,parents) - paginated listing of the whole appDataFolder.
  3. If the account has no files -> C16419c("Selected account has no files", 1).
  4. Resolve the 5 non-TTM folders in parallel (RunnableC3589a(...,8)), then assert the 3 mandatory ones exist: m31937k("database", "eyecon_backup_for_database"), m31937k("address_book", "eyecon_backup_for_address_book"), m31937k("call_logs", "eyecon_backup_for_call_logs") -> a missing folder throws C16419c("file name = <x>", 2).
  5. Start 5 named threads: BR_database_thread, BR_address_book_thread, BR_call_log_thread, BR_records_thread, BR_ttm_thread.
  6. BR_database_thread (mo31914b): open the restored DB with the 4-arg helper
    new p008a7.C0041b(MyApplication.f12163h, "eyecon_restore_temp_database", null, 57);
    read its history rows newer than the local DB’s last event, rebuild history rows for each, recompute contacts.score via DBContacts.m10109h(lastEvent, previousCallCount, previousScore) + 1.0, write them into the restored DB, then merge the SharedPreferences.
  7. SP merge with an explicit denylist (OBSERVED, C16434r.mo31914b lines 553-624): read the restored DB’s SP (shared_preference_v2, falling back to the legacy serialised shared_preference), then remove these keys before copying the current app’s map over the top:
    SP_FCM_TOKEN_EYECON, SP_KEY_LAST_TIME_UPDATE_STATS_v3, deviceName, osName,
    sp_isCalenderAvailable, contactLangNeedUpdate, contactLang, picfile, appVersion,
    dpPixelRatio, SP_KEY_AUDIO_RECORD_PROPERTIES_JSON_V4, tempAuthenticatedCli,
    sendSmsEnded, SP_KEY_LAST_CALL_RECORD_ID, isSmsValidEnded, SP_UUID,
    SP_REG_AUTO_START_SHOWN, "referrer call", SP_IS_PERMISSION_ASKED.v1,
    sp_install_time, SP_KEY_ORIGNALY_INSTALL_VERSION, sp_join_time, SP_JOIN_VERSION,
    SP_REQUIRED_SERVER_CONTACTS_REFRESH, SP_KEY_INIT_ADD_CONTACTS_COUNT,
    SP_IS_SERVER_UPDATED_WITH_PUBLIC_ID, SP_KEY_SERVER_UPDATED_WITH_ADVERTISING_ID,
    SP_TELL_SERVER_CONTACT_HAS_NO_PHOTO, updatePicsByForceDone,
    SP_ALLOWED_TO_START_GIVING_PICS_BY_FORCE, SP_KEY_LEGAL_MESSAGE_SHOWN_V2,
    AMOUNT_OF_MISSED_CALLS_V2, core_init_done, is_mac_updated,
    AddressBook.DefaultPhoneAccountType / Name / TypeV2 / NameV2,
    imeis_updated, sp_eyecon_game_page_size, HasAccessToHistory
    plus SP_KEY_SELECTED_THEME_BY_SETTINGS_DEFAULT and SP_KEY_SELECTED_THEME_E2. Note what is kept from the old install: clientId, authenticatedCli, premium cache keys, viral id, remote-config caches, the spam-list version. It then force-sets: SP_KEY_IS_AFTER_RESTORE, SP_KEY_IS_AFTER_RESTORE_DA, SP_KEY_IS_AFTER_RESTORE_DA_STATS, SP_KEY_SHOW_BACKUP_RESTORE_SUCCESS_DIALOG, SP_KEY_ONE_TIME_IGNORE_MISSED_CALLS, SP_KEY_DONT_RECREATE_ACTIVITY_FOR_DARK_MODE = true, and SP_KEY_STARTING_RESTORE_PROCESS, SP_KEY_NEED_SHOW_ON_BOARDING = false.
  8. The atomic swap (OBSERVED, p261e6.RunnableC16126b case 4):
File db = getDatabasePath("contactdb");
File temp = new File(db.getParentFile(), "eyecon_restore_temp_database");
File old = new File(db.getParentFile(), "eyecon_temp_db");
try {
if (db.renameTo(old)) { if (!temp.renameTo(db)) old.renameTo(db); }
else { old.renameTo(db); } // revert
delete: databases/eyecon_restore_temp_database,
files/address_book, files/call_logs
wipeTree(files/calls); wipeTree(files/notes);
runnable.run();
} catch (Throwable t) { old.renameTo(db); throw t; } // always revert on failure
  1. RunnableC16111m(7, handler, RunnableC16428l) posts the completion callback 100 ms later; analytics Backup restore success (or Backup restore failed with Has internet).
I.5 OBSERVED - error handlingh6

f5.j.m31922t0(code, throwable) classifies via AbstractC16422f.m31912a(throwable, context, R.string.backup_restore_err_msg) returning {exceptionClass, formattedMessage, statusCodeString, group}, which is a 4-way classification including the literal statuses NETWORK_ERROR and storageQuotaExceeded, and status groups "[23]". The dialog offers “change account” (GoogleSignInClient.signOut() -> onActivityResult(112) -> retry).

I.6 INFERRED - Flow Ih6
  • The backup is a whole-database file copy, not a logical export. That means it captures every table including the full SMS bodies (sms_table), every captured notification body (db_table_notification_reader), every voicemail transcript (ttm), all social-account IDs (social) and the entire preference map - and none of it is encrypted by the app (Drive’s own appDataFolder encryption-at-rest is the only protection, scoped to the user’s Google account). Confidence: high.
  • The restore keeps the device’s identity (clientId, authenticatedCli, SP_UUID, FCM token, advertising id, MAC-updated flag, install time, join version, viral id, theme, premium cache) and replaces the content (contacts, history, recordings, notes, call logs, address book, and most preferences). This is a deliberate “same account, new phone” migration. Confidence: high.
  • The contactdb swap is not crash-safe across a process restart: contactdb is renamed away and the temp file renamed in, and the old copy is deleted only if the swap succeeded. A power loss between the two renames leaves eyecon_temp_db behind; the code does not look for it on next launch. Confidence: medium.
  • Because the restore writes a Java-serialised HashMap into shared_preference (legacy path) and the SP denylist is applied by key name, a backup taken on an older schema could resurrect keys the current build has removed. Confidence: medium.

3. DATA FLOW MAP - what personal data leaves the device, and whereh4

Legend for Transport: TLS = HTTPS, CLR = cleartext HTTP, UDP = plain UDP (no encryption at all), SDK = third-party SDK.

#DestinationTransportPersonal data sentTriggerEvidence
1api.eyecon-app.com/app/join.jspTLSphone number, device name, OS version, public_id, Google advertising id, Wi-Fi MAC address, IMEI, ANDROID_ID, viral inviter id, user & SIM country code, app version, (social server id)registration / rejoinOBSERVED p365jd/RunnableC17614z case 1
2api.eyecon-app.com/app/cli_auth/gettransportTLSphone number, FCM push token, + the entire device-fingerprint set (see row 12), validation transport, last errorregistration, validation retryOBSERVED p365jd/RunnableC17614z default
3api.eyecon-app.com/app/cli_auth/checkcodeTLSphone number, 4-digit verification code, transport, social flagregistrationOBSERVED p516r5/RunnableC19875s case 1
4api.eyecon-app.com/app/social_auth/validatetokenTLSGoogle/Facebook OAuth token, provider, ANDROID_ID, advertising id, SIM & user countrysocial registrationOBSERVED p607vk/RunnableC21093i case 25
5api.eyecon-app.com/app/social_auth/use_cli_validationTLSphone number, ANDROID_ID, advertising idbind a real number to an existing social accountOBSERVED p607vk/RunnableC21082e0 case 23
6api.eyecon-app.com/app/addcontacts.jspTLSevery changed contact: server-formatted number, contact display name, has-photo bit, count + type + id of every linked social account (Facebook/Instagram/TikTok/etc.) + SIM country + init flagapp start, Activity resume, after restoreOBSERVED w6.h.m38428a + bm.q0.b
7api.eyecon-app.com/app/removecontacts.jspTLSnumber + name of deleted contactscontact deletedOBSERVED bm.s5.s
8api.eyecon-app.com/app/getupdatedcontactsinfo.jspTLSnothing outbound (GET); returns other-device changes. A response header sms triggers an SMS-inbox syncserver-sync cycleOBSERVED vk.i case 26
9api.eyecon-app.com/app/resetupdatedstatus.jspTLSnothing (version ping)after applying a deltaOBSERVED vk.e0 case 25
10api.eyecon-app.com/app/pic?cli=&is_callerid=true&size=&type=0&src=&cancelfresh=0&cv=TLS (image)the looked-up phone number + a src tag identifying the calling screen + the account credential in e-auth headersevery caller-ID lookup, person load, photoOBSERVED w6.h.m38434g
11api.eyecon-app.com/app/getnames.jspTLSphone number, language preference, is_callerid, is_ic (is-incoming-call), source, HTTP-stack A/B tagcaller-ID, contact photo pickerOBSERVED RunnableC2598c case 6; v7.f
12api.eyecon-app.com/app/updateuserdetails.jspTLSGoogle advertising id, Wi-Fi MAC address, app version, username (display name), g_reg_id (FCM token), uploadpjoin, name change, token refresh, MAC changeOBSERVED w6.c cases 2/3/4; r5.r case 12; vk.e0 case 24
13api.eyecon-app.com/app/uploadpic.jspTLS (multipart)the user’s own profile photo (JPEG, quality 90) + msisdn = the account clientIdprofile photo changedOBSERVED w6.h.m38441n
14api.eyecon-app.com/app/picapproval.jspTLScomma-joined list of phone numbers whose photos the user approved + isapproved=yesphoto approval in bulkOBSERVED u4.o case 5
15api.eyecon-app.com/app/spamreportTLS (queued)the reported/spammed number + payload JSONuser reports spamOBSERVED v7.f
16api.eyecon-app.com/app/spam?list_version=TLSaccount clientId only; returns the crowd-sourced spammer list ({cli,name})daily, Remote-Config-gatedOBSERVED DailyWorker
17api.eyecon-app.com/app/getoptionalpics.jspTLSclipics = comma-joined phone numberssurvey/photo pickerOBSERVED vk.e0 case 22
18api.eyecon-app.com/app/getfreshpicsjson.jspTLSphone numbersfresh-photo pollOBSERVED RunnableC2598c lines 150/183
19api.eyecon-app.com/app/getnames.jsp / getcontactslang.jspTLSnothing (locale only)language refresh after a big contact uploadOBSERVED w6.h.m38428a
20api.eyecon-app.com/app/sethistory.jspTLSfrom = the other party’s phone number, time (epoch seconds), type (call type int) - per call eventafter each callOBSERVED vk.g0
21api.eyecon-app.com/app/eventreport.jspTLS (queued){name, type, attributes:{...}, timestamp} - every UI action: page views, permission dialogs, registration stage, spam reports, blocks, AI-assistant events, Joins, errors with stack tracescontinuousOBSERVED w6.h.m38436i + vk.i case 24
22api.eyecon-app.com/app/mysettingsTLSserver settings blob; on write: type/key/value (used for backup_info and for the premium entitlement)app start, join, backup, purchaseOBSERVED w6.h.m38440m; w6.d case 8
23api.eyecon-app.com/app/get_store_productsTLSaccount only; returns the store cataloguestore screenOBSERVED w6.d case 6
24api.eyecon-app.com/app/cantalk.jsp / cantalksaw.jsp / cantalkreceived.jsp / getcantalkstatus.jspTLSrecvcli / notifiedcli = phone numbers; q/y/n answerCanTalk featureOBSERVED p5.RunnableC19300e
25api.eyecon-app.com/app/virality/viral_id, /virality/viral_id_check, /virality/premium_statusTLSaccount only; returns the inviter id and the premium-day budgetjoin, rejoin, premiumOBSERVED w6.d cases 2/4/9
26api.eyecon-app.com/app/notifyaddcontact.jspTLSapsVersion, cv (and the new contact context)local contact createdOBSERVED as.o
27api.eyecon-app.com/app/grps/sgnTLS (queued)the APK’s own signing-certificate SHA-256 hashes, RSA-encrypted with the public key in R.string.pk_enc_2, plus cid and a random floatonce after join, Remote-Config gated (sig_report_config.is_enable, cooldown cooldown_days_after_join)OBSERVED r5.r case 11
28api.eyecon-app.com/app/deleteme.jspTLSaccountaccount deletionOBSERVED v6.a
29api.eyecon-app.com/app/messages/getTLSaccount; returns server messagesmessage pollingOBSERVED w6.d case 5
30api.eyecon-app.com/app/shorten-urlTLSan arbitrary URL to shortenshareOBSERVED vk.e0 case 26
31api.eyecon-app.com/app/shareTLS (queued){cli_list:[contact numbers shown on screen], type, content, provider} - the numbers of the contacts whose feed is being sharedsocial-media shareOBSERVED vk.i case 24
32api.eyecon-app.com/app/publish (pub/redirectme.jsp)TLSpublisher attributioninstallDEX strings (no live call site found)
33voice2text.eyecon-app.com/init-ttmTLS{mycli, purchases:[owned SKUs], freeTrialCode}TTM setupOBSERVED w6.b case 0
34voice2text.eyecon-app.com/start-ttmTLS{mycli, acli = the caller's number, calltag, purchases:[owned SKUs]}TTM session startOBSERVED u4.o case 6
35voice2text.eyecon-app.com/getText, /ack, /ttm-call-forwarding-result, /ttm-get-tokens-amount, /no-ttmTLSmycli (and acli for /no-ttm)TTM pollingOBSERVED w6.d cases 1/3/7; w6.b; as.o
36numberlookup.instabridge.com/respond-smsCLRthe full SMS conversation (up to 50 messages, complete bodies, both directions), the other party’s phone number, a contactName field, the device locale, the user’s own display name, message countevery AI-assistant round tripOBSERVED p636wo/C21474f:295, p646x5/InterfaceC21626a
37cloudfront-cdn-all.eyecon-app.com/configs/ab-test.txtCLRnothing (a config fetch)Remote Config loadOBSERVED d5.d / BACKEND-API.md §1
38<analytics_worker_url> (Remote-Config-controlled)TLS (multipart)a ZIP of the device’s full call log: {cli, fake_cli, call_logs:[{phone, date, duration_in_seconds, type, name}]} - number, timestamp, duration, direction, contact nameperiodic worker, off by defaultOBSERVED Analytics/EyeconAnalyticsWorker.m9990d/m9989c
39ptt.eyecon-app.comUDP, no TLSthe app’s own CLI, app version, the e-auth/e-auth-c credential, live microphone audio (AMR) and playback audio, channel/invitation ids, invited contact CLIsToki sessionOBSERVED p687yl/C21952k, p633wl/C21450m, p703z7/C22122v0
40googleapis.com Drive (appDataFolder)TLSthe whole contactdb (all contacts, all SMS bodies, all notification bodies, all voicemail transcripts, all social IDs), all call recordings, all voice notes, the TTM audio zip, the call-log archive, the address book, and the preference mapuser-initiated backupOBSERVED p282f5/*
41api.paperquotes.com/apiv1/quotes/TLSAuthorization: Token <R.string.quotes_api_id> (a hard-coded bearer)after-call quotesOBSERVED f7.j0.m32047b
42translate.google.com/translate_a/singleTLSMY NAME IS <contact name> - contact names to Google Translatename normalisationOBSERVED ic.RunnableC17219d1:590
43us1/eu1.locationiq.com, workers.degoo.com/locationTLSthe caller’s IP (implied) / the device IP for a lat-long gatecaller-area lookupOBSERVED BACKEND-API.md §1
44api2.amplitude.com, api.eu.amplitude.comSDKfull session/event stream, including session replaycontinuousAmplitude SDK
45Firebase (Crashlytics / Remote Config / Performance / FCM / Sessions / In-App Messaging)SDKcrash reports (with the app’s own tagged logs), HTTP timings via FirebasePerfUrlConnection/FirebasePerfOkHttpClient, device identifiers, FCM tokencontinuousOBSERVED in w6.a / m7.p
46firebaseremoteconfigrealtime.googleapis.comSDK(fetch only)-manifest
47Singular (join.eyecon-app.com branded domain, cleartext)SDKinstall attribution, deep linksinstallOBSERVED p671y4/C21845b:72
48Ad networks (AdMob/Google Ads, Meta Audience Network incl. the bundled assets/audience_network.dex, Pangle/ByteDance, InMobi, Fyber/FairBid, Inneractive, Moloco, Mintegral, Vungle/Liftoff, Amazon APS, Singular, Mbridge, Crackle/Mobitech)SDKadvertising id, IP, coarse location, installed-app signals, ad-interaction telemetryad fillmanifest + SDK classes
49the-news-desk.com, s3.amazonaws.com/embed.footylight.com, hb.crackle.co.in, docs.google.com, www.google.comTLS / inline scriptad-tag scripts injected into app WebViews (user_id=[advertisingId], publisher_key, sub_id=[placementId]); the welcome-back survey formad fill / surveyOBSERVED in d5.d defaults mobitech_*_html_script, welcome_back_notification_url
50www.facebook.com / m.facebook.com / graph.facebook.com / www.instagram.com / www.tiktok.comTLSoutbound scraping only (the app fetches profiles/media with a spoofed desktop Chrome UA); the resulting media URL is posted to api.eyecon-app.com/app/sharesocial-media shareOBSERVED vk.i default branch, w7.o

Summary of the highest-sensitivity flows (INFERRED, high confidence):

  1. Complete SMS conversation text to a cleartext third-party host (numberlookup.instabridge.com) whenever the AI assistant is used.
  2. The complete address book (names + numbers + every linked social-account identifier) to api.eyecon-app.com on essentially every app start.
  3. A hardware-level device fingerprint (IMEI + Android ID + Wi-Fi MAC + advertising id) on join.jsp, cli_auth/gettransport, and updateuserdetails.jsp.
  4. The entire app database - including full SMS bodies and captured notification text - to the user’s own Google Drive appDataFolder, and back again on restore.
  5. Live microphone audio to ptt.eyecon-app.com over unencrypted UDP.
  6. A kill-switch-free, remotely-flippable full call-log upload (analytics_worker_url), currently disabled but one Remote Config value away from being enabled, with an analytics_worker_reload_json rewind (from_date) that can re-upload older history.

4. NOTEWORTHY / SURPRISING FINDINGSh4

  1. The account credential is server-issued, not device-derived. clientId - which becomes the e-auth header on every request, the msisdn on uploadpic.jsp, and the cid in the encrypted signature report - is the response body of join.jsp (and of social_auth/use_cli_validation). This resolves the open question in BACKEND-FINDINGS.md §9 item 1. It is therefore opaque, revocable, and resettable only server-side; it is not resettable by the user from Android settings, unlike an advertising ID.
  2. Two unrelated “AI” services, two different hosts, two different transport security postures. The AI SMS assistant goes to numberlookup.instabridge.com (a name that does not appear anywhere else in the app’s branding) over cleartext HTTP; the voicemail transcription goes to voice2text.eyecon-app.com over HTTPS. The assistant’s own module lives under com.eyecon.global.DefaultDialer.assistant and its DTOs are Moshi-generated Kotlin - i.e. it is a relatively recent, separately-developed module grafted into the older m4/-era codebase.
  3. contactName is populated with the phone number, not the name, on every AI-assistant follow-up request. On the first request it carries the real name. Either a bug or a deliberate choice not to leak names to the third party - static analysis cannot tell.
  4. PhoneAccount.SyncService registers a contacts SyncAdapter whose onPerformSync body is completely empty. The app ships a full AccountManager account type + authenticator + RawContacts structure for an Eyecon contacts account and then never uses it. All contact sync is the ad-hoc addcontacts.jsp path. (EyeconRoomDB is similarly dead code - see DATA-LAYER.md §3.1.)
  5. Backups are raw database-file copies. Restoring swaps contactdb at the filesystem level with renameTo, not through SQLite. The backup therefore contains full SMS bodies, captured notification text, voicemail transcripts and every linked social-account identifier - none of it encrypted by the app.
  6. EyeconCallScreeningService.stopService() is overridden to return false, and the screening response is deliberately delayed by 2 seconds so the platform’s own timeout cannot fire. This is aggressive device-ownership behaviour.
  7. AppOpsManager-based SEND_SMS probing for Xiaomi in w6.h.m38430c, plus per-OEM permission-routing dialogs (j7.t), a STICKY CallService restarted by AlarmManager.setAndAllowWhileIdle every 5 s, and a Proximity-sensor foreground service - this app is heavily engineered around Chinese OEM ROMs and aggressive battery management.
  8. The Toki UDP transport sends every packet twice and XORs retransmissions against the previous packet for FEC, with a 15-byte binary header and embedded e-auth credentials, over an unencrypted socket to ptt.eyecon-app.com on a port randomly drawn from Remote Config toki_ports (default [5620, 50000-50010]). Live microphone audio crosses the network in the clear.
  9. grps/sgn uploads the app’s own APK signing-certificate hashes, RSA-encrypted with a public key embedded in res/values/strings.xml (pk_enc_2), on a Remote-Config cooldown after join. This is an anti-repackaging / tamper signal. (Important correction to an earlier reading: the signatures array is the app’s own certificate, not the list of installed apps.)
  10. The AI assistant reads the SMS database through content://sms/sent and content://sms/inbox with address LIKE '%<number>%' and date > <flowStart>, i.e. it reconstructs conversations by substring-matching numbers rather than using thread IDs - which is why a conversation can pick up unrelated messages on short numbers.
  11. The spam model has four spam_type values but only two are server-produced. Types 3/4 are local-only user reports, explicitly preserved across a full list resync (DailyWorker.m10355b drops everything else before merging the server list) - the client trusts its own reports over the crowd list.
  12. Token IDs and other user-visible IDs are handled in obfuscated code but every one of them is recoverable, and the endpoint-name constants (share, spamreport, picapproval.jsp, …) live as readable literals. ProGuard here obfuscated structure, not intent - the semantics of nearly every network call are readable straight out of the DEX string table.

5. CONFIDENCE / UNKNOWNSh4

5.1 Confidence summary per claim areah5
AreaConfidenceWhy
Module -> obfuscated-class mappingHighEvery mapping is backed by an explicit renamed from: JADX comment or by the still used in: [orig.Class.member] inline hints, which name original class names verbatim.
Endpoint inventory and request parametersHighRead directly out of ArrayList<C21872a> construction sites and the URL-concatenation expressions.
@z@ / @a@ payload grammarsHighBoth the encoder (bm.q0.b) and the decoder (vk.i case 26) were read; they round-trip.
DB tables per moduleHighCross-checked against DATA-LAYER.md (schema and raw-SQL inventory).
Spam decision logic (0/1/2/-1)HighRead from the JADX register listing, which is unambiguous control flow.
AI-assistant DTO field names and typesHighEach DTO’s @Metadata(d2 = ...) carries the original Kotlin signature and the Moshi @Json(name=...) values.
AI-assistant request content (which messages, how many, what order)HighRead from the JADX register listing of x5.n.
contactName = number on follow-upsMedium-highThe decompiled Java is unavailable for x5.n; the register listing shows both arguments bound to the same register (r18 = r11, r19 = r11). A smali check would settle it.
Registration state machineHighcli_auth/gettransport, cli_auth/checkcode and join.jsp all read end-to-end, including response-header handling.
Toki UDP protocolHigh for the framing/mechanics, Medium for the semanticsThe byte layout, sequence counters, double-send and XOR-FEC are literal. The meaning of the header fields (type, flag bits) is inferred from the call sites that set them.
Backup artefact setHighThe six folder names are a literal array, and each is matched to a local directory in the upload and restore code.
Call-recording capture mechanismLow-MediumThe storage, indexing and repair are fully readable; the actual AudioRecord/MediaRecorder call that records a phone call is not in the app code (only the Toki mic capture is). See 5.2 item 1.
Data-flow table (rows 1-41)High for app-owned calls, Vendor traffic not enumeratedAd/analytics SDK payloads are documented as classes, not dissected.
5.2 Open questions static analysis could NOT answerh5
  1. How is a phone call actually recorded? There is exactly one AudioRecord instantiation in the whole tree and it belongs to the Toki stack (v7.f, AudioRecord(MIC, 8000, 16, MONO, 16000)). No MediaRecorder anywhere. The most likely explanation is that recording is delegated to the platform/OEM via InCallService + Call.disconnect/Call.respond timing on specific vendor ROMs (the recording_calls_method_mode Remote Config key and free_rec_calls_actions support this), but the actual capture call was not recovered. This is the single biggest gap in this document.
  2. Is there a recording upload path? None found. https://eyecon-app.com/vrs/evid- is the viral-invite landing page, not an evidence upload. If recordings are uploaded, the call site is either in a code path JADX failed to decompile (392 decompilation errors per jadx-export.log) or gated behind an unexercised branch.
  3. What is SP_KEY_AUDIO_RECORD_PROPERTIES_JSON_V4? It appears in the backup denylist and in DATA-LAYER.md’s key list but its writer/reader were not traced. Presumably per-recording codec parameters.
  4. Server-side response schemas. Only request shapes are observable. The join.jsp response headers (pi, sd, is_new) and cli_auth/gettransport’s JSON are described from the client-side parser, which is authoritative for the fields the client reads but says nothing about fields the client ignores.
  5. E-Mark / CID response headers on 401. A device-binding/attestation channel exists (w6.a.m38420d) but its semantics need server-side or runtime evidence. Unchanged from BACKEND-API.md §9.
  6. Whether e-auth-k is enforced server-side at all. It ships in the APK, is the compiled-in default of Remote Config circus_key, and can be changed remotely by reflection (b5.AbstractC3464e.m3668s()). Whether the server honours it, ignores it, or uses it for telemetry is unknown.
  7. cards.eyecon-app.com (dynamic_area_url) had no live call site in this build. The fetch path exists (DynamicAreaDataLoader, y5.f, f7.i, cards_update_threshold throttling) but the URL is assembled from Remote Config and the concrete request was not found.
  8. apps.eyecon-app.com/call-contest/a/report-calls likewise appears only as a Remote Config default (call_contest_report_calls_url) with no traced call site.
  9. SP_KEY_INIT_ADD_CONTACTS_COUNT writer. I found the decrement (in bm.q0 case 12) but not the setter, so the initial burst size N is unknown. Consequently the claim that init=1 marks “the first N uploads” is medium-confidence.
  10. Voice2Text auth. start-ttm sends a calltag with &access_token=<PhotosTrackerWorker.m10433e()> stripped, and init-ttm/ack are called with addAutoHeaders = true so they inherit the e-auth quartet - but it is unclear whether voice2text validates the e-auth credentials or a separate token.
  11. The MoE "padding" fields in the social sub-record. bm.q0.b appends a trailing "0@a@" after every social id, and vk.i’s decoder iterates i += 4 over a count * 2 + 4 window. The exact server-side contract for the two extra fields per social entry is not fully determined (the decoder only consumes two fields per pair and then reads is_sub from wherever i lands).
  12. PKG_CONFIRMING / package-visibility abuse. The <queries> block enumerates ~30 messaging apps, but whether it is used for presence detection (is_whatsapp_installed), for SMS/notification interception, or for social profile scraping varies by call site and was not exhaustively traced.
  13. Whether daily_default_dialer_role_other analytics reveal competitor dialers. DailyWorker records role_owner = the package name of the current default dialer once per day (AbstractC0046d0.m198z()). Whether that is sent to Eyecon’s own telemetry or only to Firebase was not separated.
  14. 392 JADX decompilation errors (jadx-export.log) mean some methods are stubs. Notably p646x5.C21639n.invokeSuspend (the AI reply coroutine) and large parts of androidx/fragment/app/RunnableC2598c are available only as register listings, which is why those two sections are written from listing evidence rather than Java.
5.3 Method notes / caveatsh5
  • This document is read-only static analysis. No code was executed, no request was replayed, no emulator was used.
  • Column-name constants were resolved by reading the static {} initialiser of p008a7.AbstractC0039a (e.g. f216f -> "cli_in_server", f219g -> "name", f177O1 -> "SP_TELL_SERVER_CONTACT_HAS_NO_PHOTO"). Any table reference written as AbstractC0039a.fNNN in the source is named here for readability.
  • Obfuscated method names were renamed back to their renamed from: originals where they matter (e.g. w6.h.m38428a = w6.h.a = addContactsList).
  • Third-party SDK behaviour is described only where the app configures it (Remote Config keys, placement ids, manifest permissions); the SDKs’ own payloads are out of scope except for Amplitude session replay, which is enabled and is called out explicitly.
  • The full RemoteConfig default map in p236d5.AbstractC15804d is the single most valuable artefact for understanding this app: ~200 keys that switch features on and off server-side, including circus_key (the e-auth-k value), analytics_worker_url (the call-log upload), sig_report_config (the signature report), toki_ports, use_ai_assistant, dynamic_area_url, and every premium SKU. Anyone assessing risk should start there.

End of FEATURE-AND-FLOWS.md

Part VI - Native codeh2

The APK carries five ARM native libraries. Three are ByteDance Pangle code pulled in by the advertising stack, and this section covers those. The other two are libbuffer_pgl.so and libfile_lock_pgl.so, which were never analysed and appear nowhere else in the documentation.

Provenance is easy here and worth stating plainly. ByteDance maintains an internal fork of the Android NDK for its hardened libraries, and the symbols referencing that fork are visible in these binaries with a single strings pass. That is what identifies them as vendor code rather than an app protector or anything belonging to eyecon.

The three reversed libraries:

librarysizewhat it does
libnms.so299,528anti-fraud routines, XOR string decryptor, 34+ recovered strings
libpglarmor.so68,600cypher4 cipher, /dev/urandom key path, time-seeded permutation
libtobEmbedPagEncrypt.so8,304software AES-128, no AES-NI, FIPS-197 S-boxes confirmed

Two results here are partial and are labelled as such in the text. The srand(time(NULL)) seed in cypher4 makes the permutation predictable, which is a real weakness, but predicting the permutation is not the same as recovering plaintext. The round function FUN_00104040 is still unsolved. For libtobEmbedPagEncrypt.so the key candidate UK*@3oKpFlVVnads is recovered but the AES mode is not, so the key alone does not decrypt anything.

The Java bridge is thin. C15537aw.m30739b(context, "nms") calls System.loadLibrary, and the library exports a single native method, meta(int, Context, Object). The integer argument is a selector, and mapping the remaining selector values to their routines needs a running device, which this analysis did not have.

This section also carries the Frida recovery script and its supporting data, in full, so the runtime half can be run by someone who does have a device.

libnms.so — Native Analysis (Tier C)h3

Target: native\libnms.so — arm64-v8a, 299,528 bytes SHA-256 prefix: 63DA06A1E2C518C9 Owner: com.pgl.ssdk.ces.a (ByteDance Pangle / 穿山甲 ad SDK, not MICO)

Method: Ghidra 12.1.2 via GhidraMCP bridge (bridge_mcp_ghidra.py → 127.0.0.1:8080), plus direct ELF parsing of the file on disk. Static only — nothing was executed.


1. Load layouth4

Non-standard ELF. Two RX/RW LOAD segments with different file↔vaddr offsets, plus a Ghidra display base of 0x100000:

SegTypefile offsetvaddrfileszmemszflags
1LOAD0x00x00x3F6B00x3F6B0R-X
2LOAD0x3F6B00x436B00x86980x8698RW-
3LOAD0x47D500x4FD500xC080x2184RW-

Real vaddr → file offset: v < 0x3F6B0 maps 1<1>; otherwise v − 0x4000. Ghidra’s displayed addresses are real + 0x100000. Getting this wrong produces out-of-range reads — it cost two failed scans.

Ghidra reported segments: .text 00114810–0013f40b (175,003 bytes of code), .rodata 0010b390–00110ff8, .data.rel.ro 001436b0–0014b7f7, .bss 00150960–00151ed3.


2. Exported surface — 7 symbols onlyh4

_FINI_2 _FINI_0 _INIT_0 _FINI_1 JNI_OnLoad __DT_INIT _DT_INIT

JNI_OnLoad at 00115614 is the only functional export. Every JNI method is bound dynamically via RegisterNatives, so no method name appears in .dynsym/.dynstr.


3. Imports — 40 total, and what they proveh4

GroupImportsInference
Device properties__system_property_find / _read / _getAndroid build/device property harvesting
Proc/file probingstat, lstat, fopen, fread, fgets, sscanf, fdopen, feof, fclose, fseek, ftellReads and parses /proc/* text — TracerPid, maps, status
Timingclock_gettime, time, log10, pow, getpagesizeTiming-attack detection, page-size (emulator/VM) fingerprinting
Threadingpthread_selfThread identity
String/memmemcmp, strstr, strnlen, strlen, memmove, memset, memcpy, malloc, calloc, freePattern matching, allocation
Stdio/parsevsnprintf, __vsprintf_chk, __strlen_chk, atoi, strtollFormatting and numeric parsing
Hardening__stack_chk_fail, __cxa_finalize, __cxa_atexit, __errnoStack canaries, static destructors

Absent by significance:

  • No cryptography. No AES, SHA, HMAC, MD5, or any cipher primitive.
  • No networking. No socket, connect, send, recv, getaddrinfo.

So despite sitting in a Pangle “crypt” neighbourhood, libnms.so performs no encryption and no network I/O of its own.


4. Toolchain provenance — explicith4

.comment section, both strings verbatim:

Android (9352603, based on r450784d1) clang version 14.0.7
(https://android.googlesource.com/toolchain/llvm-project 4c603efb0cca…)
clang version 9.0.0 (gitlab@git.byted.org:security/android-ndk-llvm.git
9f3a96b2ca7550528ffab623094b18501af7f4f3)

The second is ByteDance’s internal security fork of the Android NDK LLVM toolchain (git.byted.org:security/). Combined with the import profile, this identifies the library as an environment-integrity / anti-fraud component, not a crypto library.


5. Anti-disassemblyh4

Ghidra’s decompiler of JNI_OnLoad emits:

/* WARNING: Control flow encountered bad instruction data */
/* WARNING: Removing unreachable block (ram,0x001156d4) */
/* WARNING: Could not recover jumptable at 0x0013d634. Too many branches */

Deliberate opaque-predicate / indirect-jump obfuscation. JNI_OnLoad uses AArch64 PIC addressing (adrp/add rewritten in decompiler output as &label - (value >> 2)), a __stack_chk_fail canary, and a one-time init flag in .bss.


6. String obfuscation — repeating-key XORh4

FUN_00115174 is the decryptor, called from JNI_OnLoad as its very first action:

void FUN_00115174(long src, long dst, byte *key) {
byte bVar1 = *key;
if (bVar1 != 0) {
long lVar2 = 0; int iVar3 = 1;
do {
*(byte *)(dst + lVar2) = *(byte *)(src + lVar2) ^ bVar1;
lVar2 = iVar3; iVar3 = iVar3 + 1;
bVar1 = key[lVar2];
} while (bVar1 != 0);
}
}

JNI_OnLoad invokes it as FUN_00115174(&UNK_0010b474, 0x150a18, &UNK_0010b47c) — source in .rodata, destination in .bss, key in .rodata.

Verified by direct decryption. Key at 0x10b47c is 01 93 87 62 (4 bytes, non-printable). Ciphertext at 0x10b474 is 34 a1 b2 54. Result:

"5256"

FUN_00115f94 was confirmed to be libc strtol, used only to resolve the .bss global 0x150a18 for the PIC computation.

Call-site counth5

FUN_00115174 has 30+ call sites (Ghidra truncated at its page limit). Distribution:

  • _INIT_0 — 1
  • FUN_0011689c — 2
  • FUN_001185c0 — 1
  • FUN_0011ed38 — 2
  • FUN_0011efc4 — 3
  • FUN_0011f3c8 — 5
  • FUN_0011f88c — 8 (bulk table unpacker)
  • plus ~9 more sites

FUN_0011f88c alone unpacks eight separate strings in one function.


7. Full static string recoveryh4

The method-table scan below was superseded — a better static path was found.

.data.rel.ro scanning for JNINativeMethod triples returned zero because the slots are filled by R_AARCH64_RELATIVE relocations at load time (they read as 0 in the file). Parsing .rela.dyn (offset 0xAD0, 1,760 entries, 1,746 of type R_AARCH64_RELATIVE=1027) recovers the addends, and each call site’s DAT_addr + -0xe39520 resolves to addend − 0xE39520. From there the repeating-key XOR runs offline against the file.

7.1 FUN_0011f88c — 24 su binary probes (root detection)h5

Decrypted at .rodata 0xC718–0xC950, keys at 0xCBD8–0xCBA0. Pattern is a 32-byte stride of {flag, src, dst, key} in .data.rel.ro; each is guarded by a one-time .bss flag.

/system/bin/sutemp /system/xbin/bstk/su /system/xbin/ku.sud
/system/bin/sudo /system/bin/.su /system/xbin/bdsu
/system/bin/bdsu /system/xbin/daemonsu /system/bin/shuamesu
/system/xbin/bbksu /dev/su /data/su
/cache/su /system/xbin/su /system/usr/we-need-root/su
/system/sd/xbin/su /system/bin/failsafe/su /system/bin/.ext/su
/system/bin/su /su/bin/su /sbin/su
/data/local/xbin/su /data/local/bin/su /data/local/su

The function then probes each via a syscall wrapper (FUN_0013d5d8, syscall number 0x119, AT_FDCWD = -100) and, on a hit, calls FUN_00116130, iterating up to 0x17 (23) entries.

7.2 FUN_0011f3c8 — Magisk mount detectionh5
"r" fopen mode
"/proc/self/mounts" target file
"magisk" needle
"core/mirror" needle
"core/img" needle

Reads /proc/self/mounts line-by-line via fgets, strstr for any of the three needles — the standard Magisk mount fingerprints. Returns 1 on any match.

7.3 FUN_0011efc4 — SELinux execution-context checkh5
"/proc/self/attr/prev" file path
"r" fopen mode
"u:r:zygote:s0" expected first line

Gated on FUN_00120a4c() > 0x16 (API level > 22). Reads the first line of /proc/self/attr/prev and compares it to u:r:zygote:s0. A mismatch means the process did not fork normally from the zygote — i.e. it was injected or started from another context.

7.4 FUN_0011ed38 — VMOS emulator detectionh5
"persist.vmos.root.enable" system property
"1" expected value

__system_property_get then a strict strlen == 1 && value[0] == '1' test. vmos is VMOS Virtual Mobile System (an Android clone/emulator). Root-enabled VMOS ⇒ treated as a virtual device.

7.5 FUN_0011689c — memory-map analysish5
"r" fopen mode
"%lx-%lx %4s " sscanf format

%lx-%lx %4s is exactly the /proc/self/maps line grammar (start-end perms name). Built via openat (syscall 0x121 = 289) on a path assembled by FUN_0011c35c from two encrypted fragments, then per line: matches a caller-supplied needle, sscanfs start/end/perms, keeps the minimum start address and maximum end address, and only accepts mappings whose permission field begins with 'r'. Returns 1 when min < max.

This is the injected-module / code-patching probe — the technique used to spot libfrida-agent.so, Xposed, or tampered text segments in the address space.

7.6 JNI_OnLoadh5
"5256"

A build/config identifier, written to .bss 0x150A18.

7.7 Only plaintext survivorh5

com.pgl.ssdk.ces.a at 0x10B3E2 — the registration class, unencrypted.


8. Conclusionsh4

Established with high confidence:

  1. libnms.so is a ByteDance Pangle anti-fraud / environment-integrity library, built with ByteDance’s internal security NDK fork (git.byted.org:security/android-ndk-llvm.git).
  2. It performs no cryptography and no network I/O — 40 imports, none of either kind.
  3. It is a five-technique tamper-detection suite, all of whose strings were recovered:
    • 24-path su binary scan (root)
    • /proc/self/mounts Magisk fingerprinting (magisk, core/mirror, core/img)
    • /proc/self/attr/prev SELinux context check against u:r:zygote:s0 (injection)
    • persist.vmos.root.enable == "1" (VMOS virtual device)
    • /proc/self/maps parse + readable-mapping range analysis (injected modules / patching)
  4. All strings are runtime-decrypted into .bss by a repeating-key XOR (FUN_00115174, 30+ call sites) fed by R_AARCH64_RELATIVE addends — fully reversible statically, as demonstrated, without executing anything.
  5. Deliberate obfuscation: one functional export, dynamic RegisterNatives, opaque-predicate control flow, one-time-init .bss flags.

Not established:

  • Whether these signals reach ByteDance, Pangle, or MICO. The library makes no network calls, so the verdict must be handed back to its Java caller.
  • The RegisterNatives method table itself — still absent from the file, so still needs the dynamic path in §9.

To close the last gap (dynamic, requires a device):

  • Hook FUN_00115174 with Frida, logging src/dst/key per call.
  • Dump .bss (0x50960–0x51ED4 real / 0x150960–0x151ED3 Ghidra) after init.
  • Breakpoint RegisterNatives, dump the method-table argument.

Blocker on this host: no adb, no emulator, and no frida CLI — there is no device running MICO to hook, so the dynamic step cannot run here.


9. Java↔native wiring — the dynamic step turned out to be unnecessaryh4

Traced statically from the decompiled tree. This supersedes the “needs a device” conclusion.

9.1 The loaderh5

libnms.so is not in any literal System.loadLibrary("…") call. It is loaded dynamically:

// com/pgl/ssdk/ces/C15544b.java:171
C15537aw.m30739b(context, "nms");
// → com/pgl/ssdk/C15537aw.java:68
System.loadLibrary(str); // str == "nms"

It is also registered for native crash monitoring:

// com/bytedance/sdk/openadsdk/ApmHelper.java:154
monitorCrashInitSDK.config().setSoList(
new String[]{"libnms.so", "libtobEmbedPagEncrypt.so", "tt_ugen_layout.so"});
9.2 The entire native API surface — one methodh5
// com/pgl/ssdk/ces/C15543a.java:8 (real class: com.pgl.ssdk.ces.a)
public static native Object meta(int i3, Context context, Object obj);
public static Object njss(int i3, Object obj) { // safe wrapper, swallows all Throwables
try {
C15544b b = C15544b.m30762h();
if (b != null) return b.m30764a(i3, obj);
return null;
} catch (Throwable unused) { return null; }
}

meta is the only native declaration in the whole com.pgl.ssdk package. A single polymorphic entry point: an int selector, a Context, and an opaque object. All detection routines are dispatched by the int inside native code.

This is the decisive finding: the RegisterNatives table contains one entry, and it is already fully recovered here statically. The dynamic step would have produced a method name we already have.

9.3 Call sites and what is fed into the collectorh5

C15544b invokes meta() with ~19 distinct selectors:

SelectorArgumentMeaning
101"1"version/protocol marker
102this.f37985hinternal handle
114Integer (field f37986i)mode flag
105StringBuilder resultassembled string
152, 153C15516ab.m30635i/m30630d(ctx)device identifiers
106–110C15516ab.m30632f/m30631e/m30628c/m30629d/m30627c(ctx)further device fingerprint values
115this.f37987jinternal handle
103, 104, 111Stringper-check inputs
165set.contains("boot")SIM present
166set.contains("mnc")Mobile Network Code
167set.contains("mcc")Mobile Country Code
168set.contains("gaid")Google Advertising ID
159C15516ab.m30670a(...)device string
224new Object[]{str, byte[]}only selector returning a value (String cast at line 363)

Selectors 165–168 are the significant ones: SIM operator identifiers and the advertising ID are pushed straight into the native anti-fraud collector. The gaid is ByteDance’s own, obtained via the reflection trick that discards isLimitAdTrackingEnabled() (see cluster-C6a §e1).

9.4 Complete chainh5
MICO
└─> Pangle / ByteDance ad SDK
└─> com.pgl.ssdk.ces.C15544b (Java collector)
└─> C15543a.meta(selector, ctx, payload) ×19
└─> libnms.so → com.pgl.ssdk.ces.a (JNI_OnLoad, RegisterNatives)
├─ FUN_0011f88c 24 su-path root scan
├─ FUN_0011f3c8 Magisk mount fingerprints
├─ FUN_0011efc4 SELinux zygote-context check
├─ FUN_0011ed38 VMOS virtual-device check
└─ FUN_0011689c /proc/self/maps injection scan
<─ result returned to Java (no native networking)
└─> Pangle's own network stack transmits to ByteDance

The library makes no network calls of its own, so the verdict necessarily travels back through Java and out via Pangle’s separate HTTP stack. That final transmission lives in Java and is already present in the decompiled tree.

9.5 Verdict on the dynamic steph5

Not worth an emulator. The RegisterNatives table holds exactly one method, already recovered. What remains genuinely unavailable statically is the meaning of each selector — the mapping from 101/152/224… to the native routines. That mapping lives in native code and would need dynamic tracing, but it is diagnostic labelling, not new capability.

native/libnms-dump.js is retained in case that mapping is ever wanted; it would report the single meta entry plus any remaining decrypt sites.


10. Session / tooling notesh4

  • Ghidra 12.1.2 at C:\RE\RE\01-Disassemblers\ghidra_12.1.2_PUBLIC_20260605\ghidra_12.1.2_PUBLIC
  • GhidraMCP extension installed at ~\.ghidra\.ghidra_12.1.2_PUBLIC\Extensions\GhidraMCP\ (declares ghidraVersion=11.3.2 against Ghidra 12.1.2 — loads and works anyway)
  • Bridge: ~\.config\opencode\opencode.jsonc → ghidramcp\bridge_mcp_ghidra.py, DEFAULT_GHIDRA_SERVER = http://127.0.0.1:8080/
  • The bridge exposes decompile, list, xref, search, rename and comment tools but not raw byte reads or reliable disassemble_function (it returned empty), which is why byte-level work was done directly on the file.
  • Auto-analysis must be run explicitly after import; without it Ghidra defines only PLT stubs.

libpglarmor.so — Native Analysis (Tier C, part 3)h3

Target: native\libpglarmor.so — arm64-v8a, 68,600 bytes Owner: com.bytedance.sdk.component.pglcrypt.PglCryptUtils Role: cypher4 — the current Pangle app-log cipher (see NATIVE-libtobEmbedPagEncrypt.md §5) Toolchain: clang 14.0.7 plus clang 9.0.0 (gitlab@git.byted.org:security/android-ndk-llvm.git) — ByteDance’s internal security NDK, same provenance as libnms.so.

Method: Ghidra 12.1.2 via GhidraMCP, plus direct ELF parsing. Nothing was executed.


1. Load layouth4

Segfile offvaddrfileszflags
10x00x00x13…R-X
20x…0x…RW-

Ghidra segments: .text 001032bc–0010f847 (50,571 bytes), .rodata 00102070–00102c7a (3,090 bytes), .data.rel.ro 00113930–00111402f (1,711 bytes), .data 00118280–001183d7.


2. Exports — 3 only, and no _INIT_0h4

_FINI_1 001032bc
_FINI_0 001032d4
Java_..._PglCryptUtils_bc 0010353c

Note the absence of _INIT_0, which libnms.so has. There is no one-time initialiser; the key is read fresh on each operation.


3. Imports — 13, and this is the whole cipherh4

__cxa_finalize __cxa_atexit free __stack_chk_fail calloc memset
time srand rand
__open_2 __errno __read_chk close

DT_NEEDED: libc.so, LIBC, liblog.so, libz.so, libm.so, libdl.so. (libz.so is linked but no zlib symbol is imported — linked and unused, or reached via dlsym.)

No cryptography imports whatsoever. No EVP_*, no BoringSSL, no OpenSSL, no AES-NI. cypher4 is a from-scratch custom cipher, exactly like the libnms family.


4. JNI entry point — a two-opcode dispatcherh4

undefined8 Java_..._PglCryptUtils_bc(undefined8 env, undefined8 cls, int op, undefined8 obj) {
if (op == 0x3f3) return FUN_0010e6d4(env, obj); // 1011
if (op == 0x3f2) return FUN_0010e5b0(env, obj); // 1010
return 0;
}

Structurally identical to meta(int, Context, Object) in libnms.so — an int-selected polymorphic entry point rather than named JNI methods.


5. Key material — recovered from /dev/urandomh4

FUN_0010e7f8(long out, long len):

FUN_0010330c(&DAT_00102988, &DAT_001183d8, &DAT_00102998); // XOR-decrypt the path into .bss
while (__fd = __open_2(&DAT_001183d8, 0), __fd == -1) { // O_RDONLY, retry on EINTR
if (*(int*)__errno() != 4) return -1;
}
__read_chk(__fd, out, len, 0xffffffffffffffff); // read exactly len bytes
close(__fd);

FUN_0010330c is the same repeating-key XOR decryptor found in libnms.so. The path is XOR-obfuscated at .rodata 0x2988 with the key at 0x2998. Decrypted:

/dev/urandom

OBSERVED: the cipher key is genuinely random. This part is sound.


6. The cipher — 64-bit block XOR/transform with a predictable output permutationh4

FUN_001036f8(uint len, byte *in, byte *out) is the crypto core. It runs a 64-bit-block transform loop:

out[i..i+7] = keystate[i] ^ in[i]; // 8-byte keystream XOR
FUN_00104040(&sbox_state, out, out, (uVar11 % 5) * 4 + 0x10); // round transform
FUN_0010333c(&keystate, out, 8); // advance key state

.rodata holds bit-manipulation constants consistent with a bitwise-round function: signed masks -48/-56, -21/-16/-11/-4, and two de Bruijn-style power-of-two tables (0x100, 0x200, 0x400, 0x800, 0x2000, 0x1f00, 0x4000, 0x8000) at 0x2070–0x2100.

6.1 The weaknessh5

After the block loop, the output is permuted using srand(time(NULL)) + 12 × rand():

tVar9 = time(NULL);
srand((uint)tVar9);
if (len > 0xfe) uVar7 = 0xff;
for (k = 0; k < 12; k++) {
iVar6 = rand();
uVar11 = iVar6 % len; // ← predictable index
local_80[k] = out[uVar11 & 0xff]; // ← byte selected at that index
}
FUN_0010ec48(local_90, 0, 0xb); // shuffle the 12 indices

time(NULL) is observable — HTTP Date headers, or simply the victim’s local clock. An attacker within a small window of the true timestamp can replay srand(t); rand() twelve times and recover the exact index selection.

6.2 Calibrated severity — this is NOT a full breakh5

Be precise about what this does and does not give an attacker:

  • Does: reveal which 12 byte positions of the output were touched, and remove essentially all entropy from the index-selection step. That is a real reduction in the work needed and a genuine design flaw — a cryptographic primitive must not depend on wall-clock time.
  • Does not: yield plaintext. The permuted bytes are out[index] ^ out[other], so the values written still depend on unknown ciphertext, and the underlying 64-bit block cipher is keyed from /dev/urandom (§5). An attacker still has to break that cipher.

Net: a real but partial weakness — predictable-PRNG output permutation in a live, version-negotiated telemetry cipher. Not “cypher4 is broken”; “cypher4 has one unnecessary and predictable layer.”


7. Conclusionsh4

Established:

  1. cypher4 — the cipher currently used for Pangle app-log payloads — is a custom, from-scratch cipher, not a standard primitive. No crypto library is linked.
  2. Its key is read from /dev/urandom (path recovered by decrypting the XOR-obfuscated string) — sound randomness.
  3. It operates on 64-bit blocks with byte-XOR keystream and bit-manipulation round transforms.
  4. Its output permutation is derived from srand(time(NULL)) + 12 rand() calls — predictable to anyone who knows the timestamp window.
  5. Single JNI export bc with opcodes 1010/1011; no _INIT_0, key read per operation.
  6. Same toolchain provenance as libnms.so — ByteDance’s security NDK fork.

Not established:

  • The exact round function of FUN_00104040 — would need focused Ghidra work on the S-box state.
  • Whether bc(1010) and bc(1011) are encrypt and decrypt, or two variants.
  • The meaning of error codes 501–506 surfaced by PangleEncryptUtilsType4.getCryptFailedReason.

8. Cross-referencesh4

DocumentRelationship
NATIVE-libnms.mdSibling ByteDance security library; same XOR-decryptor technique, same NDK fork, but anti-fraud rather than crypto
NATIVE-libtobEmbedPagEncrypt.mdThe fallback cipher — confirmed AES-128; this library is cypher4, the current one
cluster-C5-media-social-extra.mdPangle host, consent flow, ad mediation context

libtobEmbedPagEncrypt.so — Native Analysis (Tier C, part 2)h3

Target: native\libtobEmbedPagEncrypt.so — arm64-v8a, 8,304 bytes Owner: com.bytedance.sdk.component.embedapplog.PangleEncryptUtils Toolchain: clang 11.0.5, LLD 11.0.5

Method: direct ELF parsing of the file on disk. Small enough to reverse without Ghidra. Nothing was executed.


1. Load layouth4

Segfile offvaddrfileszmemszflags
10x00x00x13E00x13E0R-X
20x13E00x53E00x2500x250RW-
30x16300x96300x2000x214RW-

Again a two-segment layout with differing offsets (v→file is identity in seg1, v−0x4000 in seg2, v−0x8000 in seg3).


2. Exported symbols — the symbol table is not strippedh4

sbox0 0x9630 size 256 OBJECT
sbox1 0x9730 size 256 OBJECT
ss_encrypted_size 0x0AD0 size 116 FUNC
initKey 0x1018 size 88 FUNC
ss_decrypt 0x0EA0 size 376 FUNC
ss_encrypt 0x0B44 size 456 FUNC
...PangleEncryptUtils_ttDecrypt 0x11C8 size 304 FUNC
...PangleEncryptUtils_ttEncrypt 0x1070 size 344 FUNC

.text is 0xA94–0x12F8 = 2,132 bytes. These seven symbols account for 1,960 of them.


3. The cipher — AES, confirmedh4

The entire .data section is 512 bytes, exactly two 256-byte tables. Both were verified as permutations of 0–255 and compared byte-for-byte against FIPS-197:

sbox0 — the AES forward S-boxh5
63 7c 77 7b f2 6b 6f c5 30 01 67 2b fe d7 ab 76 ca 82 c9 7d fa 59 47 f0 ad d4 a2 af 9c a4 72 c0
b7 fd 93 26 36 3f f7 cc 34 a5 e5 f1 71 d8 31 15 04 c7 23 c3 18 96 05 9a 07 12 80 e2 eb 27 b2 75
09 83 2c 1a 1b 6e 5a a0 52 3b d6 b3 29 e3 2f 84 53 d1 00 ed 20 fc b1 5b 6a cb be 39 4a 4c 58 cf
… (identical to the FIPS-197 table)
sbox1 — the AES inverse S-boxh5
52 09 6a d5 30 36 a5 38 bf 40 a3 9e 81 f3 d7 fb 7c e3 39 82 9b 2f ff 87 34 8e 43 44 c4 de e9 cb
54 7b 94 32 a6 c2 23 3d ee 4c 95 0b 42 fa c3 4e 08 2e a1 66 28 d9 24 b2 76 5b a2 49 6d 8b d1 25
… (identical to the FIPS-197 table)

This is a from-scratch software AES. No AES-NI, no OpenSSL, no BoringSSL, and no crypto imports whatsoever — the two tables plus initKey/ss_encrypt/ss_decrypt are the whole implementation.

.rodata (48 bytes)h5
0x8E0 "TeaLog: after ntohl: %u\0"
0x900 "UK*@3oKpFlVVnads" ← 16 bytes
  • “TeaLog” is not the TEA cipher. It is the log tag for ByteDance’s embedapplog (app-log / telemetry) subsystem.
  • "UK*@3oKpFlVVnads" is a 128-bit key candidate — exactly AES-128 key length, sitting in read-only data immediately after the log format string.
  • ntohl in the log tag implies host/network byte-order handling somewhere in the path.

4. Java wiringh4

com/bytedance/sdk/component/embedapplog/PangleEncryptUtils.java
static { System.loadLibrary("tobEmbedPagEncrypt"); }
public static byte[] encrypt(byte[] b, int n) { return ttEncrypt(b, n); } // swallows Throwable
public static byte[] decrypt(byte[] b, int n) { return ttDecrypt(b, n); }
private static native byte[] ttEncrypt(byte[] b, int n);
private static native byte[] ttDecrypt(byte[] b, int n);

The int is the buffer length, not a mode selector — confirmed at the call sites:

PangleEncryptManager.java
PangleEncryptUtils.decrypt(bArr, bArr.length);
PangleEncryptUtils.encrypt(bArr, bArr.length);

Both Java entry points swallow every Throwable and return null on failure, so the native path fails silently rather than throwing.


5. The important finding: this is the fallback, not the live cipherh4

PangleEncryptManager exposes two schemes:

PathImplementationLibrary
defaultPangleEncryptUtils (native)libtobEmbedPagEncrypt.so — the AES above
Type4PglCryptUtils.getInstance().cypher4*()libpglarmor.so

PangleEncryptUtilsType4 produces an envelope carrying an explicit version tag:

jSONObject3.put(PglCryptUtils.KEY_MESSAGE, pairCypher4EncryptWithNoWrapBase64.second);
jSONObject3.put("cypher", 4);

So ByteDance runs versioned cipher negotiation for Pangle app-log payloads, cypher = 4 being current, with this library’s AES as the older/fallback implementation. Error codes 501–506 are mapped to human-readable reasons via getCryptFailedReason.

Consequence for effort allocation: the AES I just reversed protects the legacy path. The cipher that actually runs today is cypher4, in libpglarmor.so. That is the better next target — 68,600 bytes, one named JNI export (Java_com_bytedance_sdk_component_pglcrypt_PglCryptUtils_bc), i.e. an equally thin Java façade over a real native implementation.


6. Not establishedh4

  • AES mode (ECB / CBC / CTR). ss_encrypted_size computes a padded output length, which implies PKCS#7 padding and therefore CBC or padded ECB — but that is inference, not proof. Settling it requires decompiling initKey and ss_encrypt in Ghidra.
  • Whether "UK*@3oKpFlVVnads" is genuinely the AES key or an unrelated constant.
  • Round count / key schedule details.

All three are answerable by importing the 8 KB library into Ghidra — a much lighter job than libnms.so, and Ghidra is already running with the MCP bridge live.

Native working filesh3

The Frida recovery script used to dump libnms.so at runtime, the PHDR table it walks, and the workspace notes for the native work.

libnms-dump.jsh4

/*
* libnms.so — runtime string + JNI table recovery
* Target : com.eyecon.global (MICO), ByteDance Pangle anti-fraud library
* Arch : arm64-v8a
*
* Offsets are MODULE-RELATIVE real vaddrs. Ghidra displays these with a +0x100000
* image base, so Ghidra's 0x00115174 == module offset 0x15174 here.
*
* FUN_00115174 offset 0x15174 repeating-key XOR string decryptor
* JNI_OnLoad offset 0x15614 sole functional export
* FUN_0013d5d8 offset 0x3D5D8 syscall wrapper used for su-path probing
* .bss 0x50960 .. 0x51ED4 (real) decrypted strings land here
*
* Derivation (from Ghidra's segment map, cross-checked against the ELF section table):
* .text Ghidra 0x00114810-0x0013F40B -> real 0x14810-0x3F40B file off == real vaddr
* .bss Ghidra 0x00150960-0x00151ED3 -> real 0x50960-0x51ED3
*
* Run:
* frida -U -f com.eyecon.global -l libnms-dump.js --no-pause
* or attach to a running process:
* frida -U -n com.eyecon.global -l libnms-dump.js
*/
'use strict';
const LIB = 'libnms.so';
const OFF_DEC = 0x15174; // FUN_00115174 decryptor
const OFF_JOL = 0x15614; // JNI_OnLoad
const OFF_SYS = 0x3D5D8; // FUN_0013d5d8 syscall wrapper (su-path probe)
const BSS_LO = 0x50960; // .bss start
const BSS_HI = 0x51ED4; // .bss end
const seen = Object.create(null);
let hits = 0;
function hex(p, n) {
return p.readByteArray(n).map(b => ('0' + b.toString(16)).slice(-2)).join(' ');
}
function readCString(addr, max) {
const buf = new Uint8Array(max);
for (let i = 0; i < max; i++) {
const b = addr.add(i).readU8();
if (b === 0) return buf.slice(0, i);
buf[i] = b;
}
return buf;
}
/* Reproduce FUN_00115174 exactly:
* dst[i] = src[i] ^ key[i] for i in 0..strlen(key)-1
* The loop is bounded by the KEY length, so output length == key length. */
function decrypt(srcAddr, keyAddr) {
const key = readCString(keyAddr, 128);
if (key.length === 0) return '<empty key>';
const src = new Uint8Array(key.length);
for (let i = 0; i < key.length; i++) src[i] = srcAddr.add(i).readU8();
const out = new Uint8Array(key.length);
for (let i = 0; i < key.length; i++) out[i] = src[i] ^ key[i];
return String.fromCharCode.apply(null, out);
}
/* Printable rendering; escapes control bytes so binary blobs stay visible. */
function render(s) {
return s.replace(/[^\x20-\x7e]/g, c => '\\x' + c.charCodeAt(0).toString(16).padStart(2, '0'));
}
function hookDecryptor(mod) {
const target = mod.base.add(OFF_DEC);
console.log('[+] hooking decryptor @ ' + target + ' (base ' + mod.base + ')');
Interceptor.attach(target, {
onEnter(args) {
this.src = args[0];
this.dst = args[1];
this.key = args[2];
this.plain = decrypt(this.src, this.key);
this.keyLen = readCString(this.key, 128).length;
},
onLeave() {
hits++;
const tag = 'src=' + this.src + ' dst=' + this.dst + ' klen=' + this.keyLen;
if (seen[tag]) { return; }
seen[tag] = 1;
console.log('[' + String(hits).padStart(3) + '] ' + tag);
console.log(' |' + render(this.plain) + '|');
}
});
}
/* After JNI_OnLoad returns, every one-time-init decryptor has run.
* Sweep .bss for NUL-terminated printable runs that we have not already logged. */
function dumpBss(mod) {
const lo = mod.base.add(BSS_LO);
const size = BSS_HI - BSS_LO;
console.log('\n===== .bss sweep ' + lo + ' .. +0x' + size.toString(16) + ' =====');
try {
const bytes = new Uint8Array(lo.readByteArray(size));
let start = -1;
const flush = (end) => {
if (start < 0) return;
const len = end - start;
if (len >= 3) {
let s = '';
for (let i = start; i < end; i++) s += String.fromCharCode(bytes[i]);
const off = (BSS_LO + start).toString(16);
console.log(' +0x' + off + ' [' + len + '] |' + render(s) + '|');
}
start = -1;
};
for (let i = 0; i < size; i++) {
const c = bytes[i];
const printable = c >= 0x20 && c < 0x7f;
if (printable) { if (start < 0) start = i; }
else { flush(i); }
}
flush(size);
} catch (e) {
console.log('[-] bss sweep failed: ' + e);
}
}
/* RegisterNatives is reached through the JNI function table, so we hook the
* JNINativeInterface slot rather than a named import. FindClass + RegisterNatives
* indexes (JNIEnv is a pointer to a table of 232 function pointers on Android):
* FindClass = index 6, RegisterNatives = index 215 */
function hookRegisterNatives(envPtr) {
try {
const table = envPtr.readPointer(); // *JNIEnv -> JNINativeInterface*
const fn = table.add(215 * Process.pointerSize).readPointer();
console.log('[+] RegisterNatives @ ' + fn);
Interceptor.attach(fn, {
onEnter(args) {
const methods = args[2];
const count = args[3].toInt32();
console.log('[RegisterNatives] class=' + args[1] + ' count=' + count);
for (let i = 0; i < count; i++) {
const e = methods.add(i * 3 * Process.pointerSize);
const name = e.readPointer();
const sig = e.add(Process.pointerSize).readPointer();
const fnp = e.add(2 * Process.pointerSize).readPointer();
let nm = '<err>', sg = '<err>';
try { nm = name.readCString(); } catch (_) {}
try { sg = sig.readCString(); } catch (_) {}
console.log(' [' + i + '] ' + nm + sg + ' -> ' + fnp +
' (module+0x' + fnp.sub(mod_base()).toString(16) + ')');
}
}
});
} catch (e) {
console.log('[-] RegisterNatives hook failed: ' + e);
}
}
let _mod = null;
function mod_base() { return _mod ? _mod.base : ptr(0); }
function install() {
const mod = Process.findModuleByName(LIB);
if (mod === null) {
console.log('[-] ' + LIB + ' not loaded yet; retrying on dlopen...');
const dl = Process.findModuleByName('libdl.so');
if (dl !== null) {
Interceptor.attach(dl.getExportByName('android_dlopen_ext'), {
onLeave(retval) {
const m = Process.findModuleByName(LIB);
if (m !== null && m.base.equals(mod ? mod.base : ptr(0x0))) {
setImmediate(function () { go(m); });
}
}
});
}
return;
}
go(mod);
}
function go(mod) {
if (_mod !== null && _mod.base.equals(mod.base)) return; // already installed
_mod = mod;
console.log('=========================================================');
console.log(' ' + LIB + ' base=' + mod.base + ' size=0x' + mod.size.toString(16));
console.log('=========================================================');
hookDecryptor(mod);
const jol = mod.base.add(OFF_JOL);
console.log('[+] hooking JNI_OnLoad @ ' + jol);
Interceptor.attach(jol, {
onEnter(args) {
console.log('[JNI_OnLoad] JavaVM* = ' + args[0]);
try {
const vm = args[0];
const getEnv = vm.readPointer().add(6 * Process.pointerSize).readPointer();
const envOut = Memory.alloc(Process.pointerSize);
getEnv(vm, envOut, 0x00010006 /* JNI_VERSION_1_6 */);
hookRegisterNatives(envOut.readPointer());
} catch (e) {
console.log('[-] GetEnv/RegisterNatives hook failed: ' + e);
}
},
onLeave(retval) {
console.log('[JNI_OnLoad] returned ' + retval);
setTimeout(function () { dumpBss(mod); }, 1500);
}
});
}
/* Some .so files load before our script attaches; install immediately if so. */
setImmediate(install);
/* Also re-check periodically in case the library loads much later. */
let ticks = 0;
const timer = setInterval(function () {
if (_mod !== null) { clearInterval(timer); return; }
if (++ticks > 120) { clearInterval(timer); return; } // ~60 s
install();
}, 500);

libnms-phdr.csvh4

"Idx","Type","Off","Va","Fsz","Msz","Flags"
"0","6","64","64","504","504","4"
"1","1","0","0","259760","259760","5"
"2","1","259760","276144","34456","34456","6"
"3","1","294224","326992","3080","8580","6"
"4","2","292888","309272","480","480","6"
"5","1685382482","259760","276144","34456","35152","4"
"6","1685382480","69628","69628","2796","2796","4"
"7","1685382481","0","0","0","0","6"
"8","4","568","568","188","188","4"

native READMEh4

libnms.so — runtime recovery kith4

Everything here is for the dynamic step of docs/NATIVE-libnms.md. The static pass already recovered 5 detection routines and 34 strings; what remains is the RegisterNatives method table and the handful of decrypt call sites not reachable from the six functions decompiled.

Filesh5
FilePurpose
libnms.soThe library, arm64-v8a, SHA-256 prefix 63DA06A1E2C518C9
libnms-dump.jsFrida script — hooks the decryptor, JNI_OnLoad, and RegisterNatives
libtobEmbedPagEncrypt.soNext target if libnms.so is exhausted (8 KB, fully reversible)
libpglarmor.so, libbuffer_pgl.so, libfile_lock_pgl.soLower priority, extracted for completeness
libnms-phdr.csvELF program headers parsed during analysis
What the script doesh5
  1. Waits for libnms.so — installs immediately if already loaded, otherwise hooks android_dlopen_ext and retries every 500 ms for 60 s.
  2. Hooks FUN_00115174 (offset 0x15174) — the repeating-key XOR decryptor. It reads src/key in x0/x2, reproduces the cipher in JS, and logs each distinct (src, dst, keylen) triple with the plaintext.
  3. Hooks JNI_OnLoad (offset 0x15614) — calls GetEnv(VM, &env, JNI_VERSION_1_6), then attaches to the RegisterNatives slot in the JNINativeInterface table (index 215) so it can dump {name, signature, fnPtr} for every dynamically registered method, converting each fnPtr to a module-relative offset.
  4. Sweeps .bss (0x50960–0x51ED3) 1.5 s after JNI_OnLoad returns, printing every printable run ≥ 3 bytes — this catches strings decrypted by the call sites not yet mapped.
Offsets — and how they were derivedh5

Ghidra displays addresses with a 0x100000 image base. Strip it to get module-relative offsets:

Ghidrareal/module offsetsymbol
0x001151740x15174FUN_00115174 decryptor
0x001156140x15614JNI_OnLoad
0x0013d5d80x3D5D8FUN_0013d5d8 syscall wrapper
0x001509600x50960.bss start
0x00151ED30x51ED4.bss end

Verified against the ELF section table: .text file offset 0x14810 size 0x2ABFC, .rodata offset 0xB390 size 0x5C69, .data.rel.ro addr 0x436B0, .bss addr 0x50960 size 0x1574. For this library file offset == real vaddr in segment 1, which is why all the static decryption in NATIVE-libnms.md could be done directly against the file.

Caution: this ELF has two LOAD segments with different file/vaddr offsets (seg1 offset 0x0 / vaddr 0x0; seg2 offset 0x3F6B0 / vaddr 0x436B0). Getting this wrong produces out-of-range reads rather than wrong data.

Prerequisites (not present on the analysis host)h5

The analysis machine has no adb, no emulator, and no frida CLI, so this could not be run locally. It needs:

adb devices # a running arm64 device or emulator
pip install frida-tools # frida + frida-ps + frida-trace on the host
adb push frida-server /data/local/tmp
adb shell "su -c 'chmod 755 /data/local/tmp/frida-server'"
adb shell "su -c '/data/local/tmp/frida-server &'" # or: adb forward
frida-ps -Uai | grep com.eyecon.global
Runh5

Spawn and instrument from launch, so JNI_OnLoad is caught:

frida -U -f com.eyecon.global -l libnms-dump.js --no-pause

Or attach to an already-running process:

frida -U -n com.eyecon.global -l libnms-dump.js

Save output:

frida -U -f com.eyecon.global -l libnms-dump.js --no-pause > libnms-runtime.txt 2>&1
Expected output shapeh5
=========================================================
libnms.so base=0x7a... size=0x49208
=========================================================
[+] hooking decryptor @ 0x7a...174
[+] hooking JNI_OnLoad @ 0x7a...614
[JNI_OnLoad] JavaVM* = 0x7a...
[+] RegisterNatives @ 0x7a...
[RegisterNatives] class=0x7f... count=N
[0] someNativeMethod(Landroid/content/Context;)J -> 0x7a... (module+0x1b0f0)
...
[ 1] src=... dst=... klen=18
|/system/bin/sutemp|
...
===== .bss sweep ... =====
+0x509f0 [18] |/system/bin/sutemp|
Cross-check against the static passh5

These 34 strings were already recovered statically and must reappear — if any do not, the hook offsets are wrong:

  • 24 su paths from FUN_0011f88c (/system/bin/sutemp, /system/xbin/bstk/su, …)
  • r, /proc/self/mounts, magisk, core/mirror, core/img from FUN_0011f3c8
  • /proc/self/attr/prev, u:r:zygote:s0 from FUN_0011efc4
  • persist.vmos.root.enable, 1 from FUN_0011ed38
  • %lx-%lx %4s from FUN_0011689c
  • 5256 from JNI_OnLoad

Anything beyond that list is new — most likely the RegisterNatives method names, which do not exist in the file in any form.

Notesh5
  • Syntax-checked with node --check; it has not been executed.
  • readCString is called once per hook invocation and is bounded to 128 bytes.
  • The script never writes to the target process — Interceptor.attach only observes.

Part VII - First-party codeh2

Every one of the 535 first-party packages, read and written up. This is the bulk of the document by volume, which is expected, since it is the part with no prior art to lean on.

The clusters are organised by subsystem rather than by package name, so the numbering does not correspond to anything in the APK. C1 is the core platform: HTTP, storage, constants, the emulator detector. C2 is registration and billing, and it is the largest. C3 handles the dialer and the SMS side of the app. C4 is the main UI and contacts. C5 is media and social. C6a and C6b are what is left over, including the four packages that never classified.

When you are grepping for a specific behaviour and the obfuscated name does not match anything you expect, start with the identity map in Part I. It maps every p620w6-style package to its original name.

Cluster C1 — “core platform” (MICO / com.eyecon.global v4.2026.04.23.1145)h3

Scope: the 16 ProGuard-obfuscated packages in this cluster (16 packages, 350 .java files, 7.63 MB) that form the foundation layer every other cluster depends on.

Companion documents (referenced, not duplicated):

  • docs/BACKEND-API.md — endpoints, headers, host inventory
  • docs/DATA-LAYER.md — full contactdb schema, migrations, key lists
  • docs/MANIFEST-AND-SECURITY.md — manifest/permission surface

0. Method, conventions, and how to read this fileh4

  • Sources tree: C:\Users\btc_e\Downloads\mico-analysis\src\sources
  • Resources tree: C:\Users\btc_e\Downloads\mico-analysis\src\resources (AndroidManifest.xml, res/values/strings.xml, res/xml/network_security_config.xml)
  • Disk dir → real package: JADX writes obfuscated packages to disk as pNNN<name> (e.g. p008a7 = real package a7). Every entry below states the real identity recovered from /* JADX INFO: renamed from: <pkg>.<cls> */.

Evidence convention used throughout:

TagMeaning
OBSERVEDDirectly read in the decompiled source, resource file or manifest. Line/file references given.
INFERREDReasoned from observed code + standard platform/library semantics. Confidence stated.
(third-party)Not Eyecon-authored. Obfuscated only because R8 flattened all packages, not because it was hidden.

Important caveat about class naming. Many files in this cluster are not distinct classes. ProGuard merged independent Kotlin lambdas / anonymous classes into one synthetic class that dispatches on a f…a integer field (a switch over cases). Where that happens this document says so explicitly and treats each case as a separate code site. Several files (a7.p0, a4.c, a4.d, e5.m, a7.r0) contain 10–30 unrelated code sites each.


1. Verification and correction of the briefing’s pre-stated factsh4

#Briefing claimVerdictEvidence
1p620w6\C21273a (real w6.a) = HTTP core, TAG "NetworkHelper"CONFIRMEDp620w6/C21273a.java:40 private static final String TAG = "NetworkHelper";
2rc_key = "PgdtSBeR0MumR7fO"CONFIRMEDC21273a.java:42 private static final String rc_key = "PgdtSBeR0MumR7fO"; — note the modifier really is private static final
3m38417a = Ktor pathCONFIRMED, and correctedIt is OkHttp 4.x, not Ktor: m38417a builds new C19233f0(...) (oq.f0 = Request), gets httpClient = new C19227c0(new C19225b0()) (oq.c0 = OkHttpClient, oq.b0) and executes via FirebasePerfOkHttpClient.execute(...) (C21273a.java:47-77)
4m38423g = HttpURLConnection pathCONFIRMEDC21273a.java:149-209; opens URL.openConnection() wrapped in FirebasePerfUrlConnection.instrument(...)
5m38427k applies e-auth-v, e-auth, e-auth-c, e-auth-kCONFIRMEDC21273a.java:456-468. Also sets them on the OkHttp path via m38417a (C21273a.java:59-62). Both paths use the live (possibly RC-overwritten) rc_key
6m38421e = checksum = sum of Character.getNumericValue of the FIRST char of each dash-separated CID segment, returned as a decimal stringCONFIRMED, verbatimC21273a.java:164-176
7m38420d = 401 / E-Mark / CID handlingCONFIRMEDC21273a.java:113-138
8p620w6\C21280h (real w6.h) = “Server” facade with addcontacts.jsp, join.jsp, uploadpic.jsp, getcontactslang.jsp, pic?cli=CONFIRMEDC21280h.java:99-108 (uploadpic.jsp, join.jsp), :123-124 (addcontacts.jsp, getcontactslang.jsp), :106 TAG = "Server" (thread pool name at :100)
9p008a7\AbstractC0039a (real a7.a) holds https://api.eyecon-app.com/app/ in f168L1 plus ~150 DB column constantsCONFIRMEDa7/C0041b… → AbstractC0039a.java:602 f168L1 = "https://api.eyecon-app.com/app/";. Exactly 148 C16458a0 column constants + 4 String constants
10p284f7\* (real f7.*) = custom SharedPreferences + emulator detectorCONFIRMED, refinedf7.y (prefs) + f7.x (editor) + f7.q (emulator fingerprint data). The emulator logic lives in two other places — see §8.3 and §19.6
11App prefs are backed by a shared_preference_v2 table in contactdb, NOT Android SharedPreferencesCONFIRMED, and refineda7.b (C0041b) → super(context, "contactdb", null, 57) (C0041b.java:30); C0041b.java:236-243 creates shared_preference_v2 ( key TEXT, data_type INTEGER, data_part INTEGER, data TEXT DEFAULT NULL ). Refinement: a legacy Android SharedPreferences file is still read as a migration source and re-saved (a4.d.m95a(), C16496x writes to the DB) — see §7.3
12clientId lives in that prefs store and is the join.jsp response body; read via com\facebook\appevents\AbstractC9039h.m10662s() → p358j5\AbstractC17503r.m33607k("clientId",""); presence check m10665v()CONFIRMED (cross-cluster)See §20.1 for the exact call chain into f7.y.m32082b
13p032b5\AbstractC3464e.m3668s() overwrites the static final rc_key by reflection from RC key circus_keyCONFIRMEDp032b5/AbstractC3464e.java:375-386 — getDeclaredField("rc_key"), setAccessible(true), set(...) on the w6.a singleton
14…and the Eyecon RC channel is fetched over CLEARTEXT HTTP from cloudfront-cdn-all.eyecon-app.comCONFIRMEDd5.d.AbstractC15804d:300 map.put("eyecon_remote_config_url", "http://cloudfront-cdn-all.eyecon-app.com/configs/ab-test.txt"); fetch executed by a4.c case 17 → w6.a.m38423g(...)
15(implied) — the cleartext channel can therefore set circus_keyCORRECTED — it cannotd5.d.m31091l(key,false), which m3668s() calls, resolves in this order: main process → FirebaseRemoteConfig; secondary process → prefs key FB_RC_<key>; then the compiled-in default map. It never consults d5.c (C15803c, the Eyecon A/B store that the cleartext file feeds). See §19.4 for the full proof and §19.7 for what the cleartext channel can reach. This corrects BACKEND-API.md §3.3

2. Package indexh4

Disk dirReal pkgFilesKBOwning moduleFirst-party?One-line responsibility
p008a7a750510.5OthersyesGlobal constants (Constants), SQLiteOpenHelper for contactdb, view/dialog/WebView/toast/notification helpers, app-instance UUID, app-wide startup lambdas
p284f7f742214.0OthersyesCustom SharedPreferences implementation + Editor, interned DB column-name wrapper, ContentValues builder, emulator-fingerprint tables, backup-payload crypto, quotes loader, locale override
p260e5e527222.7OthersyesCall-recorder data layer (RecordedNote) + the full emulator detector
p420m7m736198.9OthersyesContact-photo storage (Blowfish-encrypted bitmap cache), Google-Images scraper (JS-injection WebViews), quotes/inspiration, bitmap utilities, online image loader
p340i7i739130.5OthersyesApp-owned custom-View / Drawable / RecyclerView-adapter widget library
p674y7y71583.2OthersyesCustom themes & dynamic-area (“Menifa” cards): theme model, font enum, apply/preview flow
p274eoeo459.5OthersyesDB-backed persistence for shared_preference_v2 (read/write/migrate) + RxJava/Tasks adapter lambdas
p005a4a48155.6OthersmixedPrefs load/migration engine, cleartext RC HTTP fetch, analytics dual-write, Digital Turbine Ignite attribution auth component
p647x6x6522.0NetworkyesApiRequestObj persistence/retry queue (table_api_requests) wrapping the three w6.a transports
p305g7g710.7OthersyesAbstract BroadcastReceiver that defers work to goAsync() on the shared pool
gfgf1362.1Others(Google Play Core)In-App Update (com.google.android.play.core.appupdate.*) — service binding, listener registry, install status
p394ll1626.7Others(Glide)Glide image-loading internals (BitmapDrawableResource, ByteBufferInputStream, DataCacheWriter, MIME set)
p466oqoq47186.8Analytics(OkHttp 4.x)OkHttp Kotlin client/builder/Request/ResponseBody/ConnectionSpec/CertificatePinner — this is the transport w6.a uses
p506qoqo38248.6AnalyticsmixedKotlin stdlib collections + a large app-level utility object (qo.r) holding crypto-verification, Ignite version resolution and the “A/B enabled” gate
p486pqpq55377.0Analytics(okhttp3)okhttp3.internal helpers (byte utils, BOM tables, version string, thread factory)
p429n4n44248.3Others(androidx)androidx.appcompat/constraintlayout R.styleable int constants + two empty maps + a fixed build-time Date

Totals: 350 files / 7.63 MB. 11 packages are first-party app code (203 files); 5 packages are relocated third-party libraries (123 files).


3. Package a7 — real a7 (disk p008a7, 50 files, 510 KB)h4

Responsibility: the app’s foundation. Holds Constants, the SQLiteOpenHelper for contactdb, the app-instance UUID used for A/B bucketing and device identity, the WebView dialog factory that installs four JavaScript bridges, and the MyApplication.onCreate tail (analytics bootstrap, Firebase RC fetch scheduler).

3.1 a7.a — AbstractC0039a — global constants (p008a7/AbstractC0039a.java, 608 lines)h5

Kind: final constants holder (abstract class, static-only). Public surface: 148 public static final C16458a0 (f7.a0 interned column names)

  • public static final String f168L1, f171M1, f174N1, f177O1, f180P1, f201a, f204b.

String constants — OBSERVED (AbstractC0039a.java:602-606):

FieldValueMeaning
f168L1 (L1)https://api.eyecon-app.com/app/API base URL; every .jsp endpoint is built from it
f171M1 (M1)-1default int
f174N1 (N1)START_ACTIVITY_ABOVE_SCREEN_LOCKEDflag for launching UI over the keyguard
f177O1 (O1)SP_TELL_SERVER_CONTACT_HAS_NO_PHOTOpref/broadcast key
f180P1 (P1)sp_isCalenderAvailablepref key (note the misspelling of “Calendar”)
f201a (a)<sdcard>/.Eyecon/.nomediasentinel that hides the app’s folder from the gallery
f204b (b)<sdcard>/.Eyecon/temp_photo.jpgtemp file for the “fresh photo” capture flow

Column-name constants (f7.a0 interned strings), grouped by owning table — this is the vocabulary every other cluster uses to talk to contactdb:

contacts — ab_raw_id, contact_id, cli, cli_in_server, name, is_pic_synced, update_date, call_count, cli_type, is_primary, is_single, sent_to_server, is_survey_pic, survey_pic_url, survey_pic_tag, has_photo, is_sub, is_viewer, os_type, sms_count, whatsapp_count, can_talk_count, is_can_talk_enable, vn, message, source, is_group, name_group, photo, id, fav_pin_pos, last_action_date, action_type, is_favorite, pinned_position, is_like, was_invited, pic_process_done, last_approved_tag, facebook_id, favorite_timestamp, can_talk_status, can_talk_timestamp, pic_status_in_server, contact_photo_uri, score, last_action_date_long, duration, contact_action_date_mili, fresh_pic_init_url, fresh_pic_init_tag, has_fresh_pic_init, has_thumbnail_photo, custom_phone_number_label, pre_eyecon_usage, eyecon_favorites_pinned_pos, default_cis, storage_photo_path, ab_photo_type, extra_values_json, default_sim_id, has_fresh_pic_init, has_thumbnail_photo, DB_COLUMN_UNICODE_SCORE, account_type, account_id history — duration (shared), is_played, is_saved, heart_ts, text, message_id, is_read, is_spam, is_suspiciousSpam, has_name, spam_type, url, time_ms, provider, cis, is_play, is_seen, total_time_played_ms, type, server_id, alias, key, date_ms, no_read, body (TtmlNode.TAG_BODY), sender, sender_name, account_id, participants, record_duration social — social_type, social_id, social_score, social_last_change, social_name, social_source, big_pic_url, small_pic_url, pic_seen, received_date, fresh_pic_tag shared_preference_v2 — key, data_type, data_part (+ literal data) table_api_requests — url, json_key, server_retry_count, db_column_uuid Surveys / statistics / scheduling — data_part, type, subject, priority, max_appearances, appearances, schedule_expression, last_seen, subject_description, subject_path, screens, had_user_interaction, local_ticket, order_value, max_per_session, absolute_position, first_position, steps, one_per_x_days, question_type, call_direction, main_cis, main_cli, secondary_cis, is_hash_updated, is_enable, create_timestamp, rowid, last_action_date_long sms_table — server_id, alias, key, date_ms, no_read, body, sender, sender_name, message_id, is_read, is_spam, is_suspiciousSpam ttm — cis, cli, text, message_id, is_played, is_read, date_ms Other — json_key, server_retry_count, db_column_uuid, ROWID

Note (OBSERVED): two constant names are supplied by third-party classes rather than literals — f224h1 uses DownloadModel.FILE_NAME and f236l1 uses MimeTypes.BASE_TYPE_TEXT. After de-obfuscation by JADX these resolve to the strings file_name and text. p008a7/AbstractC0039a.java:571,575.

3.2 a7.b — C0041b — SQLiteOpenHelper for contactdb (p008a7/C0041b.java, 1111 lines, 54.6 KB)h5

Kind: DB helper. public final class C0041b extends SQLiteOpenHelper. Constructor: C0041b(Context) → super(context, "contactdb", (CursorFactory) null, 57) — DB name contactdb, version 57. Static singleton: public static C0041b f281a (a), obtained via public static C0041b m156t() — used everywhere as C0041b.m156t().

Key methods:

SignaturePurpose
public static C0041b m156t()singleton accessor (the canonical DB handle)
public final SQLiteDatabase m160D(long j3)getWritableDatabase() + beginTransaction(); caller must endTransaction()
public final SQLiteDatabase m162v()getReadableDatabase() with a “timesum” readiness wait
public final SQLiteDatabase m163y()getWritableDatabase() with a readiness wait
public final Cursor m161R(String str)getReadableDatabase().rawQuery(str, null)
public static boolean m147Q(String table, String column, SQLiteDatabase db)PRAGMA table_info(table) → does column exist (migration guard)
public static void m148S(SQLiteDatabase)index/column reconciliation after a table rebuild
onCreate / onUpgrade(SQLiteDatabase,int,int) / onDowngrade(SQLiteDatabase,int,int)schema v1 → v57; see DATA-LAYER.md §4 for the full migration history

SQLite DDL builders (public static String methods, all CREATE TABLE IF NOT EXISTS): m149d() m150h() m151l() m152o() m153q() m154r() m155s() m157u() m158w(). m153q() is the prefs table and is reproduced verbatim below (OBSERVED, C0041b.java:236-243):

CREATE TABLE IF NOT EXISTS shared_preference_v2 (
key TEXT ,
data_type INTEGER ,
data_part INTEGER ,
data TEXT DEFAULT NULL )

Tables created by onCreate (OBSERVED, C0041b.java:396-626): table_share_media, sms_table, history, merge_contacts, merge_history, db_table_notification_reader, db_table_reverse_lookup_id, ttm, contacts, table_api_requests, plus the seven helpers listed above. Indexes: unique_contact_id, IDX_CONTACTS_CLI, IDX_CONTACTS_SERVER_CLI, IDX_CONTACTS_ID, and unique IDX_CONTACTS_CLI_NAME_ID (cli_in_server, name, id). onCreate also runs DELETE FROM history.

Notable (OBSERVED, C0041b.java:687-726, 777): two ALTER TABLE contacts RENAME TO tmp → recreate → DROP TABLE tmp cycles (v?? and v??) are the reason DATA-LAYER.md shows the contacts table growing to 48 columns; and CREATE TABLE IF NOT EXISTS shared_preference ( data BLOB DEFAULT NULL ) is the v1 legacy prefs blob table still created for downgrade safety.

Not a finding, but note: onUpgrade re-executes m153q()-adjacent DDL unconditionally and uses raw string concatenation for ALTER TABLE ... ADD COLUMN names — column names come from a7.a constants, not from user input, so this is not an injection vector (OBSERVED).

3.3 a7.k0 — AbstractC0060k0 — device/app identity, shortcuts, time, zip (AbstractC0060k0.java, 20.2 KB)h5

Kind: utility. public abstract class.

SignaturePurpose
public static synchronized String m314d()APP INSTANCE ID. Reads pref SP_KEY_app_instance_id; if absent generates UUID.randomUUID().toString() and persists it. Cached in public static String f314a. This is the ab_test_device_id used for A/B bucketing (d5.c.m31073a hashes m314d() + test_name).
public static void m312b(String src, Activity, BroadcastReceiver[])ShortcutManager.requestPinShortcut for the Eyecon home-screen shortcut; honours RC key shortCutToastDuration; fires analytics shortcut_popup_shown
public static boolean m320j()”is main thread?” gate used by d5.c.m31078f before dispatching activation events
public static boolean m321k()complementary thread check
public static int m319i()ShortcutManager.isRequestPinShortcutSupported compatibility probe (SDK ≥ 26 gate)
public static String m318h(String cc)Google cc-TLD map: bd→google.com.bd, eg→.eg, et→.et, il→google.co.il, in→google.co.in, kz→.kz, md→.md, pk→.com.pk, sa→.com.sa, ua→.com.ua, default google.com
public static String m313c(long ms) / m315e(long secs) / m316f(float)humanised duration formatting
public static long m311a(File)recursive directory size
m326p(File, String, ZipOutputStream, long, long, C16430n) / m327q / m331u / m328r(File,String)zip create/extract helpers (used by the backup path)
m322l, m323m(File, Gson), m324n(StaticLayout), m325o, m329s(), m330t(AbstractActivityC3498e), m317g, m313c, m318hmisc

Security note: m314d() generates the A/B bucketing identifier with java.util.UUID (SecureRandom-backed) — acceptable. It is a per-install identifier persisted in shared_preference_v2, so it survives clearApplicationUserData()? No — it is inside contactdb, which is inside app data, so a data wipe resets it. INFERRED, high confidence.

3.4 a7.w0 — AbstractC0084w0 — JSON helpers + emulator gate (AbstractC0084w0.java, 9.3 KB)h5

Kind: utility + anti-analysis. Public surface: m443a(C11859p,C11859p,String,String), m444b(String)→Long, m445c(int,View)→View, m446d(String)→C11859p, m447e(InterfaceC0047e)→Object, m448f(long)→long, m449g(long,long)→boolean, m450h()→boolean, m451i(C11859p)→C11859p, m452j(C11859p,C11859p,String)→boolean, m453k(String)→boolean. Fields: public static volatile Handler f375a, public static final Object f376b.

m450h() — isRunningOnEmulator() — OBSERVED (AbstractC0084w0.java:450h body, lines 24-63): returns true if any of:

CheckValues
Build.FINGERPRINT startsWithgeneric, unknown
Build.MODEL containsgoogle_sdk, sdk_gphone, Emulator, Android SDK built
Build.MANUFACTURER containsGenymotion
Build.BRAND startsWith generic and Build.DEVICE startsWith generic
Build.PRODUCT equalsgoogle_sdk

What it gates (OBSERVED): qo.r.m36274e0() returns !m450h(), and that is the “are A/B tests enabled?” predicate used at:

  • d5.c (C15803c) constructor — if false it logs "<EyeconRemoteConfig> update is not needed, ab tests are disabled" and never issues the cleartext HTTP fetch (C15803c.java:57-60);
  • d5.d.m31092m(String) and d5.d.m31093n(String) — both return null/fall through to the compiled-in defaults on an emulator, so Firebase Remote Config reads are also suppressed (AbstractC15804d.java:652, 665).

So the emulator check is not just anti-analysis — it is a functional kill-switch for the entire remote-config / A-B subsystem. A remote analyst therefore sees only hardcoded defaults. This materially changes what a dynamic analysis of the app reveals.

3.5 a7.t — AbstractC0077t — toasts, notifications, dialogs, WebView dialog (AbstractC0077t.java, 25.4 KB)h5

Kind: UI utility. Field: public static Toast f359d (also referenced from a7.a:601).

SignaturePurpose
m425J0(Context, String url, String title, RunnableC0070p0, String html, C3502i)the app’s WebView dialog — see security note below
m426K0(Context, String url)convenience wrapper over m425J0
m437V0(int,int,String) / m438W0(String) / m416A0 / m417B0Toast helpers
m421F0(...) / m433R0 / m434S0 / m435T0NotificationCompat.Builder factories + notification builders
m427L0(File,File,FragmentActivity,C18734w)share a file pair via FileProvider + EXTRA_STREAM
m422G0/m423H0/m424I0ValueAnimator / legacy Animation factories
m429N0(FragmentActivity)→C18399kActivityResultLauncher registry for sharing
m418C0/m419D0/m430O0/m432Q0/m440Y0permission / bitmap-swap predicates
m428M0(View)→int, m441Z0(Activity,String,String)→ProgressDialog, m442z0(String,String,HashMap)misc

Security-relevant — OBSERVED (AbstractC0077t.java:206-243): m425J0 inflates R.layout.message_webview_layout, turns on DOM storage and JavaScript, installs four JavaScript bridges, enables third-party cookies, and then loads either a remote URL (webView.loadUrl(str)) or raw HTML (webView.loadData(str3,"text/html","UTF-8")):

webView.getSettings().setDomStorageEnabled(true);
webView.getSettings().setJavaScriptEnabled(true);
webView.addJavascriptInterface(new C16457a(MyApplication.f12163h), "AnalyticsWebInterface");
webView.addJavascriptInterface(new C16132d(), "Eyecon");
webView.addJavascriptInterface(new C16481m(), "EyeconContest");
webView.addJavascriptInterface(new C0067o(dialog), "EyeconWebView");
CookieManager.getInstance().setAcceptThirdPartyCookies(webView, true);
MobileAds.registerWebView(webView);
  • AnalyticsWebInterface → f7.a (C16457a) — see §4.4. It exposes getBooleanRemoteConfig, getDoubleRemoteConfig, getIntRemoteConfig, getLongRemoteConfig, getStringRemoteConfig, logEvent, setUserProperty to page JavaScript, i.e. a remote page can read every Remote Config value and push arbitrary Firebase analytics events and user properties.
  • EyeconContest → f7.m (C16481m) — contest counters.
  • EyeconWebView → a7.o (C0067o) — close() only.
  • Who can call this? m425J0 is reached from any code path that opens a “message” dialog — including in-app-message (FIAM) cards and dynamic-area content. Since the URL argument is frequently supplied by remote content (see notSupportedKeys / EyeInAppMessagingListV2 / in_app_update in d5.d), this is effectively remote JS with a privileged bridge. Severity: see §21.

Good news (OBSERVED): the WebViewClient used here, a7.p (C0069p), implements onReceivedSslError by only logging and delegating to super (C0069p.java:174-176) — it does not call handler.proceed(). TLS errors are therefore correctly fatal. BACKEND-API.md does not mention this; noted here so it is not mis-flagged.

3.6 a7.x — AbstractC0085x — file/IO, signature hashing, backup writer (AbstractC0085x.java, 45.1 KB)h5

Kind: utility + backup encryption sink. Field: public static final HashMap f377c. Selected methods: m454e0(Uri,Uri), m455f0(File,File), m456g0(File,File,InterfaceC16421e), m457h0(Context,String)→String, m458i0(C16458a0[])→C0079u, m459j0()→Uri, m460k0(Context,String,String,String), m461l0(Context,String,String,String,String), m462m0(String)→String, m463n0(MyApplication,String,int)→String, m464o0(Context)→Intent, m465p0(File)→Uri, m466q0(Signature)→String, m467r0(MyApplication)→ArrayList, m468s0(Intent)→boolean, m469t0(Intent,String)→boolean, m470u0()→boolean, m471v0(View)→Bitmap, m472w0(DialogFragment), m473x0(...)→RunnableC0081v, m474y0(View,File)→boolean.

Backup encryption (OBSERVED): m466q0(Signature) hashes a Signature (app signing-certificate fingerprint); m463n0(MyApplication, String, int) produces the encrypted payload. The crypto itself is f7.f0.m32019n (§4.6) and the sinks are:

  • AbstractC0085x.java:530-533 — outputStreamWriter.write(m32019n(str))
  • AbstractC0085x.java:579-581 — m32019n(AbstractC0046d0.m196x(file2)) then m328r(file, …)
  • a7.s0 (C0076s0) :234 — m32019n(stackTraceOf(throwable))
  • f5.f (AbstractC16422f) :171,175 — error text sent to Google Drive and Eyecon backup
  • RegistrationActivity:637, PhoneNumberValidationActivity:269 — the registration payload
3.7 a7.p0 — RunnableC0070p0 — app-wide startup / maintenance dispatcher (29.3 KB)h5

Kind: Runnable with an int f348a selector (29 code sites). Called from MyApplication, a4.c, a4.d, a7.r0, a7.s0, b5.e, bm.s3, RecordingsFragment, MainActivity, RegistrationActivity, e5.d, f7.d0, y5.v, k6.s, kh.a, o4.f, u7.o — i.e. it is de-facto the app’s maintenance task queue.

caseBehaviour — OBSERVED
0-3synthetic/no-op or image scaling
4registers a ContentObserver on CallLog.CONTENT_URI (needs READ_CALL_LOG) via C19858g + HandlerThread/Looper.loop()
5Analytics bootstrap: b5.e.m3654e() (Facebook App Events init), b5.e.m3664o() (setUserId), then explicitly nulls 39 legacy Firebase user properties (ads_free_2days_nov17 … Unify_ID_enable), builds the nested "os_version_with_device" property (Device manufacturer, Dual SIM, Caller ID, Default dialer, Amount of contacts, …, “After call”), fires DEV MyApp onCreate, then reads ActivityManager.getHistoricalProcessExitReasons("com.eyecon.global", 0, 10) (API 30+) and emits an exit_reason analytics event per exit with reason, importance, process, status, description; persists SP_KEY_LAST_EXIST_EVENT_TIME
6b5.e.m3664o()
7Looper.prepare(); b5.e.f2334a.m32776h(); Looper.loop(); — the “Events” worker thread body; b5.e.m3666q() starts new Thread(new RunnableC0070p0(7))
8runs every Runnable in d5.d.f38508b (the RC post-fetch callback list, set by RegistrationActivity:3735-3741/3885 and q7.RunnableC19693v)
9PremiumFeaturesTest.updateFeaturesMap(AbstractC15804d.m31091l("premium_features", false))
10d5.d.m31080a(null) = Firebase Remote Config fetchAndActivate(), then reschedules itself every 7200000L ms (2 h) (RunnableC0070p0.java:334-337)
11c6.d.m4095c(false) (Kotlin/Hilt-ish init)
12posts case 11 onto h7.i main queue
13m409b() — a second content-observer/C16102d (recorder) initialisation guarded by C16102d.f39635c
14-17no-ops / activity bootstrap

Case 5 detail (OBSERVED, RunnableC0070p0.java:242): the 39 cleared user properties are ads_free_2days_nov17, ads_free_dec18, ads_free_dec18_final, ads_free_dec8, android_api_version, App_language, AppVestorUserStatus, battery_opt_granted, build_type, Campaign, CountryByNumber, custom_first_open_time, Custom_theme, DarkLightMode, Dialer_Mode, did_onboarding, foreground_notification_, Is_Default_Caller_ID_App, isAppVestorUser, isOneFeedEnable, JoinType, login_order_feb12, name, Network, Notifications, oneFeedIcon, participates_in_calling_, phone_contacts_call_logs, premium_sku, Product_name, PromotionViralId, Read_Notiperm, reg_permission_prompt_de, reg_welcome_page_nov17, reg_welcome_page_nov17_f, Registered_join, Registered_method, Skip_button, Unify_ID_enable. These are historical A/B cohorts; clearing them keeps the Firebase audience clean.

3.8 a7.r0 — RunnableC0074r0 — MyApplication.onCreate tail (20.9 KB, 385 lines)h5

Kind: Runnable, 20 code sites. Contains the rc_key overwrite call site: RunnableC0074r0.java:145 → AbstractC3464e.m3668s(); — invoked unconditionally during app start, right after C16483n.f40940k (calls-contest helper) and C22071c/C21845b (Singular) init and before AccountManager setup.

Also in this file (OBSERVED): creates the AccountManager account new Account("Eyecon","com.eyecon.global") (plus notifyAccountAuthenticated) so the Contacts sync-adapter works; and reconciles SP_KEY_AUDIO_RECORDING_MODE_JSON against the current device when android_version/manufacturer/device change.

3.9 a7.n0 — AbstractC0066n0 — feature-flag cache holder (1.4 KB)h5

Kind: volatile state holder. Fields: public static volatile Boolean f334a, volatile String f335b, volatile Double f336c, volatile Double f337d, public static final Object f338e, public static final AtomicBoolean f339f. Methods: m373a()→boolean, m374b(double,double,double,double)→double, m375c(String,boolean), m376d()→boolean. Used as the one-shot “has the new photo/feature check already run” latch (a7.r0/a7.s0 read AbstractC0066n0.f339f with compareAndSet).

3.10 Remaining a7 classes (full inventory)h5
Disk fileRealKindPurpose / key members
AbstractC0042b0a7.b0utilGeneral helpers: m168Q()→long, m171T(File), m172U(), m173V/m174W (install-source / device string), m176Y(int), m177Z(Locale), m178a0()→boolean = Environment.getExternalStorageState().equals("mounted") (this is what selects MyApplication.f12172q (external) vs getFilesDir() for the photo store), m169R(String) (synchronized), m165N(Cursor,…), m166O(Throwable)→String (stacktrace→String), m179b0/m180c0/m181d0, public static final HashMap f282b (an empty scratch map referenced by w6.a/w6.h)
AbstractC0046d0a7.d0utilm182A/m183B/m185D(int) (flags: FLAG_ACTIVITY_NEW_TASK, 134217728=FLAG_RECEIVER_FOREGROUND), m184C(long)→String, m186E(), m187F(Context), m188G(...), m189H(Activity,String,String)→Dialog, m190I, m191J(boolean,Activity,AbstractC16130b), m192K(String,String)→int, m193L(long), m194v(File), m195w(String), m196x(File)→String (file→String; feeds backup encryption), m197y(ArrayList), m198z()
AbstractC0048e0a7.e0syntheticShortcutInfo.Builder construction helpers: m206d(Activity) (id "my-shortcut"), m207e(Context,String), m214l(Object), m220r()
AbstractC0049fa7.fsyntheticm238f(String,String)→NotificationChannel(...,IMPORTANCE_LOW=3), m240h/m241i/m243k casts
AbstractC0062l0a7.l0utilDensity/screen + locale + file utils. public static String f320e/f321f/f322g/f323h/f325j (process name, locale, country, …), f324i (HashSet), f326k (Integer), f327l (int). m332A1(MyApplication), m333B1(String,boolean), m334C1, m335D1(long), m336E1/m342K1/m346a1(Activity,…) (Toasts), m337F1(float)→int, m345N1, m351f1, m353h1 (px↔dp), m338G1/m339H1/m340I1 (ConstraintLayout helpers), m341J1/m349d1(Context,…) (intents), m343L1(Context,Intent,String,boolean) (broadcast), m344M1, m347b1, m348c1/m350e1/m355j1 (bitmaps), m352g1(File), m354i1(AsyncTaskC19296a,Object...), m356k1/m357l1/m358m1/m369x1/m370y1 (strings), m371z1()→String = country code used by d5.d.m31082c CountrySpecificConfig and by d5.c targeting, m359n1, m360o1, m361p1/m362q1/m363r1/m364s1(long,…), m365t1/m366u1/m367v1/m368w1()→int (screen dims)
AbstractC0068o0a7.o0syntheticinlined DialogInterface.OnClickListener/view-listener holder (d,e,l,n,o,y)
AbstractC0080u0a7.u0markerpublic static final int f364a = 0 — Kotlin Unit singleton; decompiled as an int. Used as a marker in ~30 call sites for readability
AbstractC0082v0a7.v0markerpublic static final int f370a = 0 + static { Pattern.compile("\\+*0*([0-9]{6,})"); } — the phone-number normalisation regex (strip + and leading zeros, require ≥6 digits)
AnimationAnimationListenerC0040a0a7.a0listeneronAnimationStart/End/Repeat
C0043ca7.cdatapublic final Throwable f283a; equals/hashCode/toString — a Failure holder
C0045da7.ddatapublic final Object f285a; equals/hashCode/toString — a Pair holder
C0050f0a7.f0lambdaMediaPlayer.OnCompletionListener
C0052g0a7.g0mega iface implImplements a Vungle/Pangle ad lifecycle (onAdLoadSucceeded/Failed, quartiles, onAdImpression, onAdClicked, …), psq(xoo,IOException) and psq(xoo,AbstractC4916np) OkHttp callbacks, m277q(ArrayList), then(Object)→Task, onSuccess/onError/onComplete, plus mo260a..mo276p
C0053ha7.hlistenerValueAnimator.OnUpdateListener over 4 floats; finalize()
C0054h0a7.h0listenerApplication.ActivityLifecycleCallbacks + m289a(Intent)
C0055ia7.ilistenerAnimator.AnimatorListener
C0056i0a7.i0taskAbstractC16130b impl: mo308k()(onError) / mo309m()(onSuccess) / mo310p()(background result)
C0057ja7.jlistenerValueAnimator.OnUpdateListener over 2 ints
C0059ka7.klistenerValueAnimator.OnUpdateListener over float + 2 ints
C0067oa7.oJS bridge@JavascriptInterface–free close(); bound as "EyeconWebView"
C0069pa7.pWebViewClientFull WebViewClient incl. shouldInterceptRequest (both overloads), shouldOverrideUrlLoading (both), onReceivedSslError (logs only — does not proceed), onReceivedHttpAuthRequest, onReceivedLoginRequest, onSafeBrowsingHit, onReceivedClientCertRequest. Fields f341d, f342e
C0072q0a7.q0LottiesizeOf(Object,Object)→int (LottieImageAsset); the two instances are MyApplication.f12166k/f12167l
C0075sa7.sdatabare extends HashMap
C0076s0a7.s0taskAbstractC16130b impl that resolves a contact name (mo721c0 reads a7.a.f219g = name from the DB) and encrypts diagnostics via m32019n
C0078t0a7.t0listenerApplication.ActivityLifecycleCallbacks with f360a
C0079ua7.udataf362b/f363c + compare(Object,Object)→int (sort holder for a7.x.m458i0)
DialogInterfaceOnClickListenerC0051ga7.glistenerdialog button click
DialogInterfaceOnKeyListenerC0071qa7.qlistenerdialog key handling
InterfaceC0047ea7.einterfacemarker
InterfaceC0086x0a7.x0interfacevoid mo475i(String str) — consumed by a7.w0.m447e
KeyguardManagerKeyguardDismissCallbackC0044c0a7.c0callbackonDismissSucceeded/Cancelled/Error
RunnableC0058j0a7.j0lambdano-arg run()
RunnableC0061la7.llambdabitmap scale → ImageView on the UI thread (f318d target, f316b/f317c px)
RunnableC0064m0a7.m0lambdastartup worker; AbstractC3464e.m3666q() call site (:158) — one-shot via AbstractC0066n0.f339f
RunnableC0065na7.nlambdarun()
RunnableC0074r0a7.r0lambdasee §3.8
RunnableC0081va7.vlambdadrag-and-drop toast animation (f366b…f369e)
RunnableC0087ya7.ylambdarun()
RunnableC0088za7.zlambdarun()
ViewOnClickListenerC0073ra7.rlistenerdialog close-button click
ViewOnTouchListenerC0083wa7.wlisteneronTouch drag handling
ViewTreeObserverOnPreDrawListenerC0063ma7.mlisteneronPreDraw() — fires once then removes the observer (paired with a7.l)

4. Package f7 — real f7 (disk p284f7, 42 files, 214 KB)h4

Responsibility: the app’s only SharedPreferences implementation, the interned DB-column-name type every DAO uses, the ContentValues builder, the emulator fingerprint tables, the backup-payload crypto routine, and assorted UI utilities.

4.1 f7.y — SharedPreferencesC16497y — the custom SharedPreferences (8.7 KB)h5

Kind: public final class SharedPreferencesC16497y implements SharedPreferences.

Statics — OBSERVED (SharedPreferencesC16497y.java:26-48):

FieldMeaning
public static File f41006econtext.getFilesDir(), captured in the constructor. Base dir for the legacy *.eyecon.properties migration files
public static final Object f41005dthe global lock guarding the in-memory map
public static HashMap f41007fthe entire preference store, in memory, for the whole process. Copy-on-write: every successful apply() after the first replaces this object
public static final C16303b f41008gthe write-back task (new C16303b(2))
public static int f41009hwrite-generation counter (used to decide when to copy-on-write and to unwind on commit() failure)
instance f41010a”prefs loaded” latch
instance f41011b”do not write” latch (set during migration)
instance f41012cHashSet of pending keys

Constructor — OBSERVED (:51-55):

f41006e = context.getFilesDir();
m32086e(context, true); // kick off the async load
ContextCompat.registerReceiver(context, new C9080e(this,4),
new IntentFilter("EYECON.INTENT_ACTION_DATA_WAS_UPDATED"), 4);

i.e. the prefs object broadcast-receives EYECON.INTENT_ACTION_DATA_WAS_UPDATED so other processes invalidate their copy after a write.

Public API (all real SharedPreferences overrides plus extras):

SignatureNotes
public final Object m32082b(Object defaultValue, String key)the low-level read used everywhere in the app (SharedPreferencesC16497y.m32082b(null,"clientId")). Waits on m32084f(), then synchronized(f41005d){ map.get(key) }
public final void m32086e(Context, boolean)starts the load via C16495w on C16303b.f40374b
public final int m32085d(String key)atomic read-increment-write counter; used for paging/sequence numbers
public static AbstractC11857n m32083c(String key, String defJson)read a Gson JSON object stored as a String pref; throws EyeconException wrapping ("key = "+key+", je_str = "+raw) on parse failure
public static void m32084f()busy-wait readiness barrier: while (!ready && i3 < 3000) { Thread.sleep(10); i3 += 10; } — up to 3 s, then gives up silently
public static boolean m32081a(SharedPreferencesC16497y, HashMap, boolean clear)diff/apply logic; returns whether anything actually changed
getBoolean/getFloat/getInt/getLong/getString/getStringSet/getAll/contains/editstandard overrides, all final, all routed through m32082b
registerOnSharedPreferenceChangeListener / unregister…throw new UnsupportedOperationException("UNIMPLEMENTED_METHOD") — the API is deliberately not implemented
m32086e + m32084fthe load/wait pair

Security/robustness notes:

  • No encryption. Values (including the clientId credential, clientToken, SP_KEY_app_instance_id, pref cipher_key) are stored in plaintext in the contactdb SQLite file. See §21-F2.
  • m32084f() is a silent 3-second timeout: if the async load has not finished, every get* silently returns the caller’s default. INFERRED (high confidence): a slow/large DB load can therefore cause the app to read clientId == null and take error paths (e.g. w6.a throwing RuntimeException("Error missing CID")).
  • getAll() returns a shallow new HashMap(f41007f); mutable collections in it are shared with the live map. INFERRED (medium confidence): a caller mutating a returned Set mutates the store. getStringSet defensively copies; getAll does not.
4.2 f7.x — SharedPreferencesEditorC16496x — the custom Editor (7.9 KB)h5

Kind: public final class SharedPreferencesEditorC16496x implements SharedPreferences.Editor. Fields: f41002a (clear flag), f41003b (pending HashMap), f41004c (owner prefs).

Type-safe setters (the app’s own names): m32077d(String,boolean), m32078e(String,int), m32080g(String,long), m32079f(String,AbstractC11857n) (Gson object → String), m32076c(Object,String) (generic setter — used for Float and raw Objects), m32075b(String key, Integer cap) (increment capped at cap).

Commit — OBSERVED (:96-119 and :32-55):

public final boolean commit() {
... synchronized (f41003b) { synchronized (SharedPreferencesC16497y.f41005d) {
if (f41009h > 0) SharedPreferencesC16497y.f41007f = new HashMap(f41007f); // copy-on-write
if (!m32081a(f41004c, f41003b, f41002a)) return false;
f41009h++;
C16945i.m32773e(f41008g.f40374b, new RunnableC0032d(f41008g, map, 19, new boolean[]{false}));
return zArr[0]; // blocking
} } }
public final void m32074a(AbstractC16130b cb) { // apply()
... C16945i.m32774f(f41008g.f40374b, 0, new RunnableC0032d(f41008g, map, 18, cb)); // fire-and-forget
}
  • Every apply() rewrites the entire shared_preference_v2 table (DELETE FROM shared_preference_v2 + re-insert of every key) — see eo.b.m31713A (§5.2). There is no incremental write path. OBSERVED.
  • apply() swallows write failures: on exception it calls cb.m31515h() (onError) and returns; commit() propagates via the boolean[1] latch. OBSERVED.
4.3 f7.a0 — C16458a0 — the interned DB-column-name type (1.7 KB)h5

Kind: value type. public final class C16458a0. Fields: public static final AtomicInteger f40857c (ordinal source), public static final ConcurrentHashMap f40858d (string→instance intern table), public final String f40859a (the name), public final int f40860b (monotonic ordinal). Methods: public static C16458a0 m31978a(String) → new C16458a0(str, f40857c.incrementAndGet()); public static C16458a0[] m31979b(String[]); toString() returns f40859a.

Effect (INFERRED, high confidence): this turns every column name string in a7.a into an identity-comparable, interned object, so DAOs can compare columns with == instead of String.equals. It is why code writes c16470g0.m32035d(a7.a.f216f.f40859a) or passes C16458a0 objects straight into Cursor.getColumnIndexOrThrow(...).

4.4 f7.a — C16457a — AnalyticsWebInterface JS bridge (3.6 KB)h5

Kind: public final class C16457a. Field public final FirebaseAnalytics f40856a. Constructor: C16457a(MyApplication). Methods — all annotated @JavascriptInterface:

SignatureDelegates to
public boolean getBooleanRemoteConfig(String key)d5.d.m31081b(key)
public double getDoubleRemoteConfig(String key)d5.d.m31083d(key)
public int getIntRemoteConfig(String key)d5.d.m31086g(key)
public long getLongRemoteConfig(String key)d5.d.m31089j(key)
public String getStringRemoteConfig(String key)d5.d.m31091l(key, false)
public void logEvent(String name, String jsonAttrs)parses the JSON string into a Bundle (String/Integer/Double only) then firebaseAnalytics.f22308a.zza(name, bundle); logs "AnalyticsWebInterface: logEvent:<name>"
public void setUserProperty(String name, String value)firebaseAnalytics.m14523a(name, value); logs "AnalyticsWebInterface: setUserProperty:<name>"
public void LogD(String)empty body — vestigial

Security relevance (OBSERVED + INFERRED): this object is bound to JavaScript as window.AnalyticsWebInterface in a7.t.m425J0 (§3.5). Any page that WebView loads can therefore (a) read any remote-config value — including operational endpoints, ad-unit IDs, product SKUs, the circus_key default, analytics_worker_url, server_report_url, dynamic_area_url — and (b) inject arbitrary analytics events and overwrite arbitrary user properties. Combined with the fact that the WebView URL is frequently remote-supplied content, this is a remote information-disclosure and analytics-poisoning primitive. See §21-H1.

4.5 f7.q — C16489q — emulator-fingerprint data (3.6 KB)h5

Kind: data holder. public final class C16489q.

FieldValue — OBSERVED
f40972e (e) — emulator MSISDNs15555215554, 15555215556, 15555215558, 15555215560, 15555215562, 15555215564, 15555215566, 15555215568, 15555215570, 15555215572, 15555215574, 15555215576, 15555215578, 15555215580, 15555215582, 15555215584 (the Android-emulator +155552155xx range)
f40973f (f) — emulator IMEI/MEID000000000000000, e21833235b6eef10, 012345678912345
f40974g (g) — emulator IMSI310260000000000
f40975h (h) — Genymotion sockets/dev/socket/genyd, /dev/socket/baseband_genyd
f40976i (i) — goldfish tokengoldfish
f40977j (j) — QEMU sockets/dev/socket/qemud, /dev/qemu_pipe
f40978k (k) — x86/VBox/ttVM init filesueventd.android_x86.rc, x86.prop, ueventd.ttVM_x86.rc, init.ttVM_x86.rc, fstab.ttVM_x86, fstab.vbox86, init.vbox86.rc, ueventd.vbox86.rc
f40979l (l) — Andy filesfstab.andy, ueventd.andy.rc
f40980m (m) — Nox filesfstab.nox, init.nox.rc, ueventd.nox.rc
f40982a (a) — 15 C9118x system-property probesinit.svc.qemud(null), init.svc.qemu-props(null), qemu.hw.mainkeys(null), qemu.sf.fake_camera(null), qemu.sf.lcd_density(null), ro.bootloader==“unknown”, ro.bootmode==“unknown”, ro.hardware==“goldfish”, ro.kernel.android.qemud(null), ro.kernel.qemu.gles(null), ro.kernel.qemu==“1”, ro.product.device==“generic”, ro.product.model==“sdk”, ro.product.name==“sdk”, ro.serialno(null)
f40984chas-telephony flag
f40985dArrayList of emulator package names — populated in the ctor: com.google.android.launcher.layouts.genymotion, com.bluestacks, com.bignox.app
f40981nunused static singleton slot
m32070a(String[] paths)→booleantrue if any of the given filesystem paths exists

Consumed by: e5.m (RunnableC16111m.m31485a) — see §8.3. Note (OBSERVED): only Genymotion, BlueStacks and Nox are package-probed; Memu, LDPlayer, MEmu, Windroye, TiantianVM are only covered indirectly by the file/property heuristics (ttVM_x86 in f40978k).

4.6 f7.f0 — AbstractC16468f0 — the app’s Kotlin-idiom utility object (18.1 KB)h5

Kind: public abstract class (Kotlin object). Statics: public static final Pattern f40886a = Pattern.compile("[^A-Za-z0-9]"), public static final C16466e0 f40887b = new C16466e0(0), public static final ThreadLocal f40888c.

Full public surface (40 static methods) — grouped:

Null/empty predicates: m31998H(String) (the app’s isNullOrBlank — also treats the literal "null" as empty), m31999I(Collection), m32000J(Object[]), m32027v(Boolean). Equality/defaults: m32001K(Object,Object) (null-safe equals), m31996F(String,String), m31995E(Object), m32031z(int,Integer), m32021p(int,Object[]), m32022q(List,int,Integer). Gson typed defaults: m31991A(String,Integer,C11859p), m31992B(String,C11856m,C11859p), m31993C(String,Long,C11859p), m31997G(String,String,C11859p), m32025t(String,Boolean,C11859p), m32002L(String) (numeric regex \d+(\.\d+)?). Comparators: m32007b(double,double), m32008c(int,int), m32009d(long,long), m32010e(boolean,boolean). Collections: m31994D(Bundle)→HashMap, m32011f(String,String[]), m32014i(String,String[]), m32012g(char[],char), m32013h(Object[],Object), m32015j(Collection,String...), m32016k(String) (word count), m32027G(Object[])→Set. Text: m32004N(String) (extract only the digits, right-aligned), m32005O(String) (Title Case each word), m32030y(String) (text before the first space), m32006a(Boolean) → "Yes"/"No". Dialogs: m32003M(Dialog,Activity) (safe show()), m32017l(Dialog) (safe dismiss(), logs "OH"), m32018m(DialogFragment) (dismissAllowingStateLoss). Bundle/Intent: m32028w(Intent)→Bundle, m32029x(Bundle)→Bundle, m32023r(Intent)→String (action). Misc: m32020o(String,String) (case-insensitive contains), m32024s(Object)→Boolean (Number/Boolean/String→boolean), m32026u(int) (1→true, 0→false, else log+AbstractC9039h.m10632L), m32023r.

  • m32019n(String)→String — the payload crypto. See §4.6.1.
4.6.1 f7.f0.m32019n — hybrid encryptor — WEAK CRYPTO (OBSERVED, AbstractC16468f0.java:354-366)h6
public static String m32019n(String str) throws NoSuchPaddingException, InvalidKeySpecException,
NoSuchAlgorithmException, InvalidKeyException {
KeyGenerator kg = KeyGenerator.getInstance("AES");
kg.init(256);
byte[] aesKey = kg.generateKey().getEncoded(); // 32 random bytes
SecretKeySpec spec = new SecretKeySpec(aesKey, "Blowfish"); // <-- AES generator, Blowfish cipher
Cipher c = Cipher.getInstance("Blowfish");
c.init(1 /*ENCRYPT*/, spec);
String b64Body = Base64.encodeToString(c.doFinal(str.getBytes(UTF_8)), 0);
PublicKey pk = KeyFactory.getInstance("RSA").generatePublic(
new X509EncodedKeySpec(Base64.decode(MyApplication.m10343e().getString(R.string.pk_enc), 0)));
Cipher r = Cipher.getInstance("RSA/ECB/PKCS1Padding");
r.init(1 /*ENCRYPT*/, pk);
return AbstractC1574a.m1347n(Base64.encodeToString(r.doFinal(aesKey), 0), "#", b64Body);
}

Output format: "<base64(RSA(aesKey))>#<base64(Blowfish(plaintext))>".

Defects (all OBSERVED, all factual):

  1. Algorithm/key mismatch. A 256-bit AES key is generated and then handed to SecretKeySpec(…, "Blowfish"). Blowfish accepts arbitrary key lengths so it “works”, but the code is misleading and will silently change behaviour if the generator changes.
  2. Blowfish/ECB, no IV. Cipher.getInstance("Blowfish") defaults to ECB with a zero IV and no nonce. Identical plaintext blocks → identical ciphertext blocks; 64-bit block size enables birthday-bound collisions on large payloads (contacts/call logs).
  3. No authentication. The ciphertext has no MAC/AEAD tag — fully malleable and not tamper-evident. INFERRED (high confidence): a server that decrypts and re-serialises without verifying integrity is exposed to bit-flipping.
  4. RSA/ECB/PKCS1Padding = PKCS#1 v1.5. Not OAEP. INFERRED (medium-high): classic Bleichenbacher padding-oracle exposure if the server returns distinguishable decryption errors.
  5. Hardcoded public key in resources (R.string.pk_enc, res/values/strings.xml:1237, RSA-2048 X.509 SPKI, base64). Public keys are not secret, so this is not a leak — but it means the transport key can never be rotated without an app update. A second key pk_enc_2 exists (strings.xml:1238) and is used by p516r5\RunnableC19874r:191.

What it protects: the backup payload (Google Drive drive.appdata and Eyecon’s own backup server) and registration/diagnostic JSON. Sinks listed in §3.6. Severity: HIGH — see §21-F1.

4.7 f7.g0 — C16470g0 — ContentValues builder (7.3 KB)h5

Kind: public final class. Fields f40892a (ContentValues), f40893b (HashMap), f40891c (a C22174e(12) scratch). Methods: m32035d(String)→String, m32036e()→ContentValues, m32032a(C16458a0)→Double, m32033b(C16458a0)→Integer, m32034c(C16458a0)→Long, m32037f(String)→Object, m32038g(C16458a0,Double), m32039h(C16458a0,Integer), m32040i(C16458a0,Long), m32041j(C16458a0,String), m32042k(String,Serializable), toString(). This is the typed row-builder used by the contacts/history/social DAOs — it is why the column constants in a7.a are objects rather than strings.

4.8 f7.n0 — C16484n0 — the universal async-result wrapper (3.4 KB)h5

Kind: public final class extending p262e7.AbstractC16130b (the app’s three-state callback: mo310p() background → mo309m() success → mo31515h() error). Methods: m32061b()→Object (the computed result), m32062c(Serializable)→Object, m32063d(Object,String tag) / m32064e(Object,String tag) (tagged getters with defaults), m32065f(Object) (set result), m32066g(Object,String tag) (set a tagged diagnostic), onFailure(Exception), m32060a(...) (file-descriptor variant), public Object f40952a (the scratch HashMap(0)). This is the class behind the ubiquitous task.m32066g(x,"key") / task.m32064e(null,"key") idiom across every cluster.

4.9 f7.d0 — C16464d0 — “MySettingsInServer” (2.9 KB)h5

Kind: singleton server-settings cache. f40866c = instance. Pref keys: SP_KEY_MY_SETTINGS_LOCAL_JSON, SP_KEY_MY_SETTINGS_SERVER_JSON (both Gson JsonObject strings read via f7.y.m32083c). Fields: f40867d (new String[0]), f40868a (C11859p — the merged live settings), f40869b (ArrayList pending keys). Methods: m31983a() — persist the merged object to SP_KEY_MY_SETTINGS_LOCAL_JSON then RunnableC0070p0(18); m31984b(AbstractC16130b) — fetch from server on the w6.h “Server” pool via RunnableC21276d. Constructor kicks off a server refresh when clientId exists (AbstractC9039h.m10665v()).

4.10 Remaining f7 classes (full inventory)h5
Disk fileRealKindPurpose / key members
AbstractC16490rf7.rWebViewClientabstract base with public static long f40986b/f40987c counters, public final String f40988a, abstract mo407a(), and onRenderProcessGone → boolean (crash-tolerant WebView)
C16460b0f7.b0dataTreeSet f40862a + int f40863b = 1 (an ordered unique-id set)
C16461cf7.clistenerTextWatcher (f40864a flag) feeding f7.e
C16462c0f7.c0utilLocale override: m31980a(Context,String) mutates the global Configuration/LocaleList; m31981b(Context,String)→C16462c0 returns a ContextWrapper with the overridden locale + fontScale from AbstractC17448b.m33490J(m33492L())
C16465ef7.ecustom Viewauto-sizing/expanding TextView (m31985b(TextView,int,int), m31986c(...)→float, m31987a/m31988d/m31989e/m31990f)
C16466e0f7.e0utilinitialValue() → new char[0]-backed ThreadLocal
C16474i0f7.i0dataquotes item: f40904g (C16945i), f40905h (lock), f40906a/f40907b (strings), f40908c="", f40909d (Bitmap), f40910e (Runnable), f40911f; m32043a/m32044b/m32045c, toString()
C16476j0f7.j0service”QuotesLoader” singleton f40914f; thread pool f40915a = new C16945i(1,1,"QuotesLoader",false); fields f40916b (bool), f40917c = "en" (selected language), f40918d (C9080e receiver), f40919e (int). Methods m32046a/m32047b/m32048c, m32049d(C16474i0[],boolean), m32050e(String,String), m32051f() (reload), finalize(), m32052g(Runnable). Sets Authorization: Token <R.string.quotes_api_id> = Token e2eeb1aa9f32eb07fa04595a0c457ecb6fadb772 (C16476j0.java:102) — a hardcoded third-party API bearer token
C16478k0f7.k0Drawablerounded-rect Drawable (f40925a RectF, f40926b…f40929e flags, f40930f radius, f40931g Paint, f40932h stroke, f40933i ColorStateList, f40934j color)
C16479lf7.lmarkerempty class body
C16481mf7.mdata + JS bridgecalls-contest statistics, bound as "EyeconContest": getContactsCallsCount(), getNonContactsCallsCount(), getWithinEyeconContactsCallsCount(), getWithinEyeconNonContactsCallsCount(), getStartDate(), getEndDate(), getMyStartDate(), isOptIn()
C16482m0f7.m0lambdaMediaPlayer.OnCompletionListener
C16483nf7.nservice”CallsContestHelper” singleton f40940k. m32053a(...) loads SP_KEY_CONTESTS_JA + SP_KEY_CONTEST_INFO; m32054b(..., ArrayList) counts qualifying outgoing calls (a7.a.f157I == 2 outgoing, duration ≥ RC calling_contest_min_call_duration, within contest window, after join); m32055c(ArrayList), m32056g(int,int), m32057d(String), m32058e(String), m32059f(MainActivity,String,String). Fields f40942b/f40943c/f40944d (dates), f40945e (is_opt_in), f40946f..f40949i (counters), f40941a (HashMap), f40950j (C11856m)
C16485of7.otaskAbstractC16130b impl (mo415j/mo308k/mo309m)
C16487pf7.putilLinearSmoothScroller (calculateSpeedPerPixel, calculateTimeForScrolling, getVerticalSnapPreference, updateActionForInterimTarget)
C16488p0f7.p0utilWebView scroll/impression tracker: f40964a (WebView), f40965b, f40966c/f40967d/f40968e (booleans), f40969f, f40970g (ArrayList<C15754e> visibility rules), f40971h (Handler); m32068a(int,boolean), m32069b(). Drives the ad-impression logic in a4.c case 16
C16491sf7.sdatasparse-array multimap: f40989a/f40990b (SparseArray), m32071c(), m32072d(int)→Comparator, m32073e(int)→ArrayList, plus full sparse-array + Collection API
C16495wf7.wtaskthe prefs loader task (AbstractC16130b): mo415j(boolean) (error), mo308k(), mo309m() (installs the loaded map into f7.y.f41007f, sets f41010a=true), mo310p(). Constructors also inlined for FreePremiumUserActivity and HistoryLogActivity
C16498zf7.zsynthetic lambdain-app-update snackbar: watches Play Core InstallState (INSTALL_STATE_DOWNLOADED → “update downloaded / restart” snackbar coloured R.color.light_main_color; INSTALLED → unregister listener)
InterfaceC16463df7.dinterfacemarker
InterpolatorC16467ff7.futilgetInterpolation(float)
RunnableC16469gf7.glambdarun()
RunnableC16471hf7.hlambdarun()
RunnableC16472h0f7.h0lambdarun()
RunnableC16473if7.ilambdarun()
RunnableC16475jf7.jlambdarun() — also hosts f7.n’s async init
RunnableC16477kf7.klambdarun()
RunnableC16480l0f7.l0lambdarun() throws Throwable (16.3 KB of inlined sites)
RunnableC16486o0f7.o0listenerApplication.ActivityLifecycleCallbacks with f40956a (Handler), f40957b/f40958c (bools), m32067a(boolean)
RunnableC16492tf7.tlambdarun()
RunnableC16493uf7.ulambdarun()
RunnableC16494vf7.vlambdarun()
ViewOnLayoutChangeListenerC16459bf7.blisteneronLayoutChange(...) for f7.e

5. Package eo — real eo (disk p274eo, 4 files, 59.5 KB)h4

Responsibility: the only code that actually reads/writes the shared_preference_v2 table, plus the legacy-store readers, plus a few RxJava adapter lambdas that ProGuard happened to land here.

5.1 eo.b — C16303b — prefs persistence engine (p274eo/C16303b.java, 1238 lines)h5

Kind: public class C16303b implements InterfaceC19748o, InterfaceC16970c, InterfaceC20338h, OnCompleteListener, InterfaceC17157a, InterfaceC0199o0, SDKInitStatusListener, SuccessContinuation. The 8 interfaces come from unrelated SDKs that had their lambdas inlined into this file (Mbridge SDKInitStatusListener, Amplitude plugin, Gson TypeAdapterFactory, Kotlinx-serialization KSerializer, Tasks SuccessContinuation). Only the static methods below are meaningful to C1.

f40373a/f40374b/f40375c = the synthetic lambda selector fields.

5.1.1 public static void m31713A(HashMap map, C0041b helper) — the writer (OBSERVED, :168-267)h6

Full semantics:

  1. For each key in map, derive a type code: 0=String, 1=Boolean, 2=Integer, 3=Long, 4=Float, 5=Double. Anything else → AbstractC9039h.m10631K(new IllegalArgumentException("unknown type = …")) and the key is skipped.
  2. String values are chunked at 102 400 bytes (100 KiB), backing off so a chunk never ends mid-UTF-8-continuation-byte (while (iMin > 0 && (bytes[i10+iMin] & 192) == 128) iMin--;).
  3. Builds ContentValues per chunk: key (a7.a.f141C1), data_type (a7.a.f232k0), data_part (a7.a.f235l0, the chunk index), data (literal).
  4. helper.m160D(5000L) → getWritableDatabase() inside a transaction, then DELETE FROM shared_preference_v2 (no WHERE clause — the whole table).
  5. rawQuery("SELECT 1 FROM shared_preference_v2 LIMIT 1"); if a row survives it throws EyeconException("After delete the table is not empty!").
  6. Re-inserts every chunk, setTransactionSuccessful(), logs "EyeconSharedPreference / saveData_ done", endTransaction().

Consequence (INFERRED, high confidence): because every apply() rewrites the entire table, the prefs DB is written on every single editor apply() — a substantial write amplification, and a window during which another process reading f7.y.f41007f (its own cached copy) can diverge. The EYECON.INTENT_ACTION_DATA_WAS_UPDATED broadcast (sent by m31727z) exists to close that window.

5.1.2 public static HashMap m31717e(C0041b helper) — the v2 reader (OBSERVED, :321-501)h6

SELECT * FROM shared_preference_v2 ORDER BY key, data_part ASC, then reassembles chunked strings, coercing data_type 0-5. Returns null if the cursor is empty.

Failure behaviour — this is a serious finding (OBSERVED, :399-499):

  • On any throwable while reading: logs "EyeconSharedPreference / loadDataForRestoreOnly failed, key = %s, data = %s", AbstractC9039h.m10632L(th), and — if MyApplication.f12170o (main process) — sleeps 1000 ms, resolves an internal TaskCompletionSource, sleeps 3000 ms, then System.out.println("!!! killProcess killApp, System.exit(0) by eyecon !!!") and System.exit(0).
  • On SQLiteDatabaseCorruptException | SQLiteFullException specifically (:468-478): additionally calls ((ActivityManager) …getSystemService("activity")).clearApplicationUserData() and logs "EyeconTools5 / clearApplicationCache source = EyeconSharedPreference".

Security / robustness implications:

  1. clearApplicationUserData() wipes all app data — DBs, files, prefs — which includes the clientId credential. The device is therefore forced into a full re-registration. Anyone able to make contactdb hit SQLITE_FULL (e.g. by filling the device’s free space) triggers a remote-relevant data-loss event. Severity: HIGH (see §21-F3).
  2. System.exit(0) on a transient read error is a self-DoS: a locked/encrupted DB or a partially-written transaction makes the app kill itself on every launch.
  3. The process-kill loop at :490-495 has an inverted comparison — if (runningAppProcessInfo.pid == 0 && !processName.equals(appProcessName)) — so it only ever attempts to kill PID 0 and never kills sibling processes. The intended cross-process cleanup does not happen (dead code / logic bug).
  4. Logs the raw key and data on failure → sensitive values (incl. clientId) go to logcat.
5.1.3 public static HashMap m31718g(C0041b helper) — the v1 legacy reader (OBSERVED, :505-556)h6
"SELECT substr(data, <N>, 1000000) AS data FROM shared_preference"

concatenates the 1 MB slices of the shared_preference BLOB column, then:

return (HashMap) new ObjectInputStream(new ByteArrayInputStream(blob)).readObject();

This is a Java-deserialization sink. The blob lives in contactdb, i.e. app-private storage. Mitigating factor (OBSERVED): android:allowBackup="false" in the manifest (AndroidManifest.xml:266) removes the Android auto-backup/restore vector that normally makes this class of bug remotely reachable. Residual risk: on a rooted device, or if the DB is writable by another app via a debuggable/rooted context, a crafted blob reaches readObject(). Severity: MEDIUM (see §21-F4).

5.1.4 public static HashMap m31714C(byte[]) — gzip+deserialize (OBSERVED, :271-290)h6

new ObjectInputStream(new GZIPInputStream(new ByteArrayInputStream(bArr))).readObject() cast to HashMap. Used by a4.d.m95a() on the legacy *.eyecon.properties files — same deserialization class of issue.

5.1.5 public static boolean m31727z(HashMap map) — migration save (OBSERVED, :617-632)h6
if (!(MyApplication.m10342d() instanceof MyApplication)) return true; // test harness ⇒ no-op
if (MyApplication.m10348j().f41011b) return false; // in-flight migration guard
m31713A(map, C0041b.m156t());
AbstractC0062l0.m343L1(MyApplication.f12163h,
new Intent("EYECON.INTENT_ACTION_DATA_WAS_UPDATED")
.putExtra("EYECON.INTENT_KEY_PROCESS_NAME", MyApplication.f12163h.f12176a),
"INTENT_ACTION_DATA_WAS_UPDATED", true);
return true;
5.1.6 public static HashMap m31715b(C16303b, HashMap) (OBSERVED, :294-307)h6

Defensive copy that drops any value that is not a Number, Boolean or String — i.e. Set<String> values (which f7.x.putStringSet allows) are silently lost on the next full-table rewrite. INFERRED, high confidence. Any feature relying on putStringSet will lose its data across a restart. (No such usage found in C1.)

5.1.7 public static C16303b m31719o(String...), public static C16303b m31720p(char) (OBSERVED, :561-613)h6

Gson JsonWriter-escaping helpers / a CharIterator factory — third-party inlined code, no app semantics.

5.2 eo.a, eo.c, eo.d (OBSERVED)h5
RealDiskKindPurpose
eo.aC16302aextends AbstractC19747n (RxJava Single)3-case lambda: case 0 C4025d.m36181b(new C21940a(29,…)), case 1 m36181b(new C16303b(0,…)), default m36181b(new C16305d(InterfaceC19748o, CallableC16509k))
eo.cC16304cextends AbstractC19747nterminal: mo31729a(EnumC21167b.f56149a); onSuccess(Boolean.FALSE);
eo.dC16305dextends AtomicReference implements InterfaceC19748o, InterfaceC20176bRxJava concatMapE continuation: mo31729a, dispose() (EnumC21166a.m38245a), onError(Throwable) (re-subscribes to f40377b.f41052b, wraps failures in CompositeException), onSuccess(Object); serialVersionUID = -5314538511045349925L

6. Package x6 — real x6 (disk p647x6, 5 files, 22 KB)h4

Responsibility: the durable retry queue for network calls. Every request the app wants to make is serialised into the table_api_requests table so it survives process death and can be replayed.

6.1 x6.c — AbstractC21650c — ApiRequestObj base (p647x6/AbstractC21650c.java)h5

Kind: public abstract class. Fields: f57896a = urlStr, f57897b = requestMethod, f57898c = endPoint (logging tag), f57899d = addAutoHeaders, f57900e = uuid, f57901f = typeRequest, f57902g = isCameFromReg, f57903h = serverRetryCount. Constructors: (String url, String method, String endpoint, boolean addAutoHeaders, String uuid, int typeRequest, boolean isCameFromReg, int retryCount) and a convenience (String url, String method, boolean isCameFromReg, int typeRequest, String endpoint) that forces addAutoHeaders=true, generates UUID.randomUUID().toString() and retryCount=0. Methods:

  • public final ContentValues m38855a()→ContentValues — maps to the DB row using a7.a columns: url (f257s1), json_key (f159I1, the serialised request), server_retry_count (f162J1), db_column_uuid (f165K1).
  • public abstract int mo38856b() — execute the request; returns the HTTP status code.
  • public void mo38857c(JSONObject) — serialises the request into {"urlStr","requestMethod","endPoint","addAutoHeaders","uuid","typeRequest","isCameFromReg"}.
  • toString() returns "serverRetryCount N\n" + pretty-printed JSON.

Transport selection: typeRequest 1 = JsonApiRequest, 2 = GzipApiRequest, 3 = StandardApiRequest (see x6.b.m38853b, which validates the range and falls back to 3).

6.2 x6.b — C21649b — ApiRequestDB singleton (p647x6/C21649b.java)h5

Statics: f57893a (instance), f57894b and f57895c — new C16945i(1, 1, "ApiRequestDB", true) — two single-threaded daemon executors. Methods:

  • public static AbstractC21650c m38852a(Cursor cursor, int[] columnIndexes) — hydrate from a cursor using url, json_key, server_retry_count, db_column_uuid.
  • public static AbstractC21650c m38853b(int retryCount, String json, String uuid, String endpoint) — the deserialiser. Reads urlStr, typeRequest, isCameFromReg, requestMethod, jsonExpectedResult, endPoint, dataName, contentType, requestJsonBody, addAutoHeaders, gzipEnable, timeout (default 5000), responseCode[], params[], headerValuesMap, headerResult, then constructs the matching subclass. (Note the contentType; key with a stray semicolon at m38853b — a cosmetic bug; the value is still readable as contentType via optString on a different key.)
  • public final void m38854c(AbstractC21650c) — re-enqueue for replay on f57894b.
6.3 x6.d, x6.e, x6.f — the three transports (OBSERVED)h5
RealDiskTransport calledNotes
x6.dC21651dw6.a.m38424h(ArrayList, String url, int[] code, String endpoint, String rid) — gzip POSTPreflight AbstractC9039h.m10665v() (clientId present) else returns -1. Logs tag "GzipApiRequest". Serialises params as [{name,value},…]
x6.eC21652ew6.a.m38425i(int[], boolean addAuth, String body, String dataName, String url, String endpoint, String contentType, String method, String rid) — form/JSON POSTLogs tag "JsonApiRequest". Fields f57905i (requestJsonBody), f57906j (dataName), f57907k (contentType)
x6.fC21653fw6.a.m38423g(ArrayList, String url, String method, int[], boolean jsonExpected, String endpoint, HashMap headers, boolean addAuth, boolean gzip, HashMap respHeaders, String rid) — generalLogs tag "StandardApiRequest". Extra fields f57909j (jsonExpectedResult), f57910k (headerValuesMap), f57911l (gzipEnable), f57912m (headerResult), f57913n (timeout, default 60000)

All three catch IOException | OutOfMemoryError and return -1.

Security note (OBSERVED): the persisted request JSON includes urlStr, requestJsonBody, endPoint and all custom headers. Because headerValuesMap is serialised verbatim, any header the caller passes — including e-auth / e-auth-c / e-auth-k if a caller ever sets them explicitly — is written in plaintext to contactdb. In the current code the auth headers are added inside w6.a (after deserialisation), so they are not persisted. INFERRED (medium confidence): safe today, but the design has no allow-list and would silently start persisting credentials if a caller ever used headerValuesMap for them.


7. Package a4 — real a4 (disk p005a4, 8 files, 155.6 KB)h4

Responsibility: the preference load / migration engine, the cleartext remote-config HTTP fetch, the analytics dual-write, and a vendored Digital Turbine Ignite attribution-authentication component.

7.1 a4.c — RunnableC0031c — dispatcher; contains the cleartext RC fetch (p005a4/RunnableC0031c.java, 621 lines)h5

Kind: public final class RunnableC0031c implements Runnable, selector f96a, payload fields f97b/f98c. 29 code sites, of which the security-relevant ones are:

7.1.1 case 17 — CLEARTEXT HTTP fetch of the A/B config (OBSERVED, :453-477)h6
C11803c cfg = (C15803c) obj;
C15802b cb = (C15802b) obj2;
try {
C11859p prev = cfg.f38506b != null ? cfg.f38506b.mo14592c() : null;
long t0 = SystemClock.elapsedRealtime();
int[] code = new int[1];
String body = C21273a.m38423g(
new ArrayList(),
AbstractC15804d.m31091l("eyecon_remote_config_url", false),
ShareTarget.METHOD_GET, // "GET"
code, false, "getUpdatedEyeconRemoteConfig", null, false, false, null, "");
C18416a.m34588y("EyeconRemoteConfig", "updateFromServer, done! time = %s", …);
if (isEmpty(body) || code[0] < 200 || code[0] >= 300) cb.m31515h();
else cb.m31518o(C15803c.m31073a(cfg, prev, C18416a.m34586w(body).m14606i()));
cb.m31516i();
} catch (Exception e) { log("updateFromServer, failed"); cb.m31515h(); }

Facts:

  • The URL comes from Remote Config key eyecon_remote_config_url, whose compiled-in default is http://cloudfront-cdn-all.eyecon-app.com/configs/ab-test.txt (d5.d.AbstractC15804d:300) — http://, not https.
  • The fetch is a plain GET with addAuth = false, so no e-auth-* headers are attached and there is no request authentication or integrity protection at all.
  • The response is JSONObject-parsed and handed to d5.c.m31073a(...), which merges it into the live A/B config (see §19.4).
  • Transport: w6.a.m38423g → URL.openConnection(). With http:// the platform HttpURLConnection is used directly; there is no certificate to validate.
  • AndroidManifest.xml:272 android:usesCleartextTraffic="true" and res/xml/network_security_config.xml <base-config cleartextTrafficPermitted="true"> → cleartext is permitted app-wide, so this request is interceptable on any network (open Wi-Fi, hostile ISP, ARP/DNS spoofing) with no user interaction.

Launched from d5.c (C15803c) constructor on a plain new Thread(...) (C15803c.java:77) whenever the locally cached version is older than RC key eyecon_remote_config_version — i.e. on app start after the shipped default version bump. OBSERVED.

7.1.2 Other notable a4.c cases (OBSERVED)h6
caseBehaviour
0Ignite: AbstractC19779r.m36265a(new JSONObject(c0033e.f112j.version())) — IgniteAuthenticationComponent / resolveIgniteServiceVersion, wrapped in C16698b.m32479b("%s: resolveIgniteServiceVersion : unable to resolve version : %s", …) on failure
1Firebase Crashlytics AbstractC9039h.m10644a(false, exc) (suppressed for IOException)
2,3bitmap scale-into-ImageSwitcher
4startForegroundService(ForegroundLauncherService) with INTENT_KEY_ACTION_FLS; on failure logs "EyeconTools5, startServiceOrForeground FINAL RESULT = FAILED for action = …"
5Drawable.createFromStream(file) then AbstractC16130b.m31518o(...)
6,7AbstractC3458o (Mintegral/Pangle player) obtainMessage(111/110/107, …)
9FCM topic subscription toggle — compares pref "eyecon.TOPIC_SUBSCRIPTION-" against the desired state, then FirebaseMessaging.onSuccessTask(new C2082a(topic, 3|4))
10b5.e.m3650a(property, value) — Firebase setUserProperty + Singular + AppsFlyer-style
12DummyActivity keyguard-unlock intent routing: EYECON.INTENT_ACTION_UNLOCK_KEYGUARD, …_AND_OPEN_WHATSAPP, …_AND_OPEN_WHATSAPP_WITH_FALLBACK — parses "intent_uri" with Intent.parseUri(…, 0) and startActivity
16WebView ad-impression tracker — walks C15754e visibility rules, accumulates dwell time, and once a threshold is reached calls webView2.evaluateJavascript(<inject 1x1 pixel or <script src=…>>). iIntValue == 1 → document.createElement('img'); img.src="<url>"; …; iIntValue == 2 → document.createElement('script'); script.src="<url>"; script.type="text/javascript"; …. The URLs come from the response headers/JSON, so this is remote JS + remote pixel injection into a live WebView.
19Ad-size test: resizes the native-ad container to AbstractC0062l0.m337F1(adHeightResPx) dp and propagates the new layout up to R.id.RL_base_da
21Lottie animation from a FileInputStream
22”read more” ellipsis expansion on a TextView
24C15854y (data-entry dialog) — disables the view, runs RunnableC0031c(12)
27Recorder callback: C16102d.m31468b(c16104f), Collections.binarySearch(c16102d.f39634b, …), remove(...), m32066g("RND_1_"+n,"CB_ERROR")
7.2 a4.d — RunnableC0032d — prefs load/migration + analytics dual-write (p005a4/RunnableC0032d.java, 1294 lines)h5

Kind: Runnable, selector f100b/f101c/f102d, 29 code sites (case 0…case 28). (run() itself is not decompiled — JADX emitted a smali listing and throw new UnsupportedOperationException("Method not decompiled: …"), then case 1: etc. are reachable through the private helpers and synthetic constructors.)

7.2.1 private final void m95a() — the preference loader / legacy migration (OBSERVED, :112-…)h6

Resolution order for the initial HashMap<String,Object>:

  1. eo.b.m31717e(C0041b.m156t()) — read shared_preference_v2.
  2. eo.b.m31718g(C0041b.m156t()) — read the v1 legacy shared_preference BLOB (Java-deserialised; see §5.1.3).
  3. Legacy Android SharedPreferences (PreferenceManager.getDefaultSharedPreferences(ctx)):
    • skip if OLD_VERSION_COPY_TO_NEW is already true;
    • skip if getAll() is null/empty;
    • skip if the map has no "clientId" entry;
    • otherwise, if eo.b.m31727z(map) succeeds, set defaultSharedPreferences.edit().putBoolean("OLD_VERSION_COPY_TO_NEW", true).apply().
  4. Legacy Java-serialised property files in context.getFilesDir(): main.eyecon.properties then secondary.eyecon.properties. Read with ObjectInputStream.readObject(); if the result is a HashMap use it, otherwise eo.b.m31714C((byte[]) object) (gzip+deserialize). (Note: the decompiled control flow around these two files is tangled by JADX — the r10 reassignments at :154-168 are partially unreachable duplicates. The intent is unambiguous; the exact fallback ordering is INFERRED, medium confidence.)

Then it installs the map via f7.w.mo309m() → f7.y.f41007f.putAll(map) and sets f41010a = true, under synchronized (f7.y.f41005d).

Case 19 (L1de) → eo.b.m31727z(map) and decrements f7.y.f41009h. Case 20 (L1ca) → eo.b.m31713A(map, (a7.b) …). Case 18 (L199) → file-log write (lo.a.m34565a).

7.2.2 case 2 (L59a) — analytics dual-write (OBSERVED, :1196-1249)h6
Bundle fb = new Bundle(); Bundle amb = new Bundle(…);
b5.e.m3652c(fb); // normalise param names (40-char limit, "Am" for "+", "PCT" for "%", "Num" for "#")
fb.putLong("value", l);
FirebaseAnalytics.getInstance(...).f22308a.zza(b5.e.m3657h(40, eventName), fb);
b5.e.m3651b(amb); // normalise (40 chars, "-" for "+", PCT, Num)
b5.e.m3654e(); // ensure Facebook App Events initialised
b5.e.f2336c.f13184a.m10682e(value, amb, b5.e.m3656g(eventName)); // FB App Events
AbstractC21888a.m39141a(b5.e.f2336c.f13184a, new Runnable() { … });

So every analytics event is written twice — once to Firebase Analytics, once to the Facebook App Events SDK — under two different parameter-name normalisation schemes (GA4’s 40-char limit vs Facebook’s). The two normalisation functions are b5.e.m3656g and b5.e.m3657h.

7.3 a4.a, a4.f, a4.g — Ignite interface plumbing (OBSERVED)h5
RealDiskKindPurpose
a4.aInterfaceServiceConnectionC0029ainterface … extends ServiceConnection, p259e4.InterfaceC16097bThe Ignite auth contract: mo77a/mo81b(AbstractC0034f), mo78a(String) auth-failed, mo79a() bound-alive, mo80b() authenticate, mo82b(String) connect-failed, mo83c(ComponentName,IBinder), mo84c(String) auth-success, mo85c(), mo86d()→String package, destroy(), mo87e()→String token, mo88f()→boolean expired, mo89g()→Context, mo90h()→boolean authenticated, mo91i()→String version, mo92j()→boolean, mo93k()→IIgniteServiceAPI
a4.fAbstractC0034fabstract class … implements a4.aDelegate/decorator: registers itself via f120a.mo77a(this); mo81b(this);, forwards every method, and routes events to InterfaceC16096a (onIgniteServiceAuthenticated/AuthenticationFailed/ConnectionFailed/Connected)
a4.gC0035gfinal class … extends a4.fA two-mode inlined subclass (f122c 0 or 1). Mode 1 short-circuits everything to a no-op (mo79a()→false, mo88f()→false, mo89g()→null, mo90h()→false, mo92j()→false, mo93k()→null, mo85c()→false, all callbacks return without notifying) — i.e. a “detached/disabled Ignite” stub. Also declares 10 empty private methods m100l…m108t
a4.eC0033efinal class … implements a4.aThe real IgniteAuthenticationComponent. Constructor resolves the Ignite package via queryIntentServices(new Intent("com.digitalturbine.ignite.cl.IgniteRemoteService"), 0) and stores it in f110h. mo84c(String token) stores the JWT in f113k.putString("clientToken", token), decodes the JWT body with new String(Base64.decode(str.split("\\.")[1], 8), "UTF-8") (flag 8 = Base64.URL_SAFE), reads exp and computes the session-expiry wall time, logging "Ignite session will exp in: %s". mo88f() compares against System.currentTimeMillis()/1000. m99l() calls this.f112j.authenticate(f108f, f109g, bundle{ sdkFlowTypeKey=1 }, f115m) over AIDL. onCredentialsRequestSuccess/Failed come from p259e4.InterfaceC16097b. Fields f116n/f117o (success/failure delegates), f113k (Bundle), f114l (lock), f118p (token), f119q (Runnable), f115m (BinderC22067a AIDL callback)

Security note (OBSERVED): the JWT is only decoded, never verified — signature, issuer, audience and nbf are all ignored; only exp is honoured, and even that is derived from an attacker-suppliable token if a hostile local service can bind IgniteRemoteService. INFERRED (medium): a malicious app on the device declaring that intent could mint an arbitrary session token and satisfy the attribution flow. Mitigation: the package name is read from the resolved ResolveInfo and passed to authenticate(); whether the service itself authenticates the caller is outside the APK.

7.4 a4.b — RunnableC0030b (17.8 KB) and a4.h — C0036h (15.3 KB)h5

Both are pure synthetic lambda holders used by a4.e/a4.c (Ignite session-expiry polling and the service-connection continuation). No first-party logic.


8. Package e5 — real e5 (disk p260e5, 27 files, 222.7 KB)h4

Responsibility: the call-recorder’s data layer (RecordedNote model, the recorder state machine, the recordings list UI) and the app’s full emulator detector.

8.1 e5.d — C16102d — recorder state singleton (p260e5/C16102d.java, 19.8 KB)h5

Kind: public final class C16102d. Fields: f39630d (instance), f39631e (global lock — every method synchronized (f39631e)), f39632f (Boolean, cached result), f39633a = new C16945i(1,1,"RecodedNotesData",false) (thread pool — note the typo “Recoded”), f39634b (C16491s = f7.s sparse multimap of pending recordings), f39635c (initialised latch). Methods: m31467a(C16102d), m31468b(C16104f)→int (add a recording, returns an error code — < 1 means error, consumed by a4.c case 27), m31469c(C16104f), m31470d(), m31471e(boolean)→ArrayList, m31472f()→ArrayList, m31473g(boolean)→ArrayList, m31474h(ArrayList,ArrayList)→C16491s, m31475i() throws IOException, m31476j()→boolean, m31477k(Runnable).

8.2 e5.f — C16104f — RecordedNote (p260e5/C16104f.java, 12.8 KB)h5

Kind: data class with compareTo/equals/hashCode/m31483e()→File. Fields: f39639a (int), f39640b/f39641c/f39642d (longs: start/duration/end), f39643e/f39644f/f39645g/f39646h/f39647i (strings), f39648j (boolean), f39649k (int), f39650l/f39651m (strings), f39652n (boolean), f39653o (int), f39654p (Uri). Statics: m31479a(String)→String[] throws ParseException (date split), m31480c(Cursor)→int[], m31481d(Uri)→long, m31482b()→ContentValues (writes to the recorded_notes table), m31484f()→boolean. (Column names come from a7.a; see DATA-LAYER.md §2.7 for the recorded_notes schema.)

8.3 e5.m — RunnableC16111m — THE EMULATOR DETECTOR (p260e5/RunnableC16111m.java, 1787 lines)h5

Kind: public final class RunnableC16111m implements Runnable, selector f39670a, payload f39671b/f39672c. 29 code sites (case 0…case 28) plus the private method m31485a().

8.3.1 private final void m31485a() — full detection chain (OBSERVED, :178-500)h6
StageTechnique — OBSERVED
1Build string heuristics (:207): FINGERPRINT.startsWith("generic"); MODEL contains google_sdk / droid4x / Emulator / Android SDK built for x86; MANUFACTURER contains Genymotion; HARDWARE == "goldfish" or "vbox86"; PRODUCT ∈ {"sdk" (via Constants.RequestParamsKeys.SDK_VERSION_KEY), "google_sdk", "sdk_x86", "vbox86p"}; BOARD/BOOTLOADER/HARDWARE/PRODUCT lower-cased contains nox
2Build.SERIAL / Build.getSerial() (:213-228, gated on SDK_INT >= 26 and READ_PHONE_STATE): lower-cased contains nox
3Generic-brand/device pair (:237-242) and PRODUCT == "google_sdk"
4Telephony (:460-493): getLine1Number() ∈ f7.q.f40972e (16 emulator MSISDNs); getDeviceId() ∈ f7.q.f40973f (3 values); getSubscriberId() equals f7.q.f40974g[0] = 310260000000000; getNetworkOperatorName().equalsIgnoreCase("Android")
5Filesystem (:247,266,440,475): f7.q.m32070a over the Genymotion sockets, Nox init files, QEMU sockets, and the vbox/ttVM/x86 init files
6/proc/tty/drivers and /proc/cpuinfo read (1024-byte buffer) looking for goldfish (:248-264)
7Hidden-API reflection (:270-280, 311-314, 341-342, 408-410, 429-438): context.getClassLoader().loadClass("android.os.SystemProperties").getMethod("get", String.class).invoke(...) for each of the 15 probes in f7.q.f40982a; flags the device if ≥ 5 probes come back null
8Subprocess (:286-336, 403-450): new ProcessBuilder("/system/bin/netcfg") with directory(new File("/system/bin/")) and redirectErrorStream(true); inspects stdout for wlan0, tunl0, eth0, and 10.0.2.15
9Installed emulator packages (:355-363, 495-499): getLaunchIntentForPackage(…) for com.google.android.launcher.layouts.genymotion, com.bluestacks, com.bignox.app
8.3.2 What it does on detection — OBSERVED (:367-383)h6
C16770d cb = (C16770d) this.f39671b;
String cli = (String) cb.f41635b; // the phone number being validated
AbstractC16130b task = (AbstractC16130b) cb.f41636c;
if (isEmulator) { task.m31516i(); return; } // else: continue registration
JSONObject j = new JSONObject(); j.put("cli", cli);
C21280h.m38436i("android-a001", "error", j); // <-- EXFILTRATES THE NUMBER
task.m32066g(Boolean.TRUE, "isEmulator");
C21280h.m38439l(task, EnumC19659g1.IS_CODE_VALID, EnumC19653e1.CLIENT_EMULATOR);
task.m31515h(); // fail the validation callback

C21280h.m38436i is not local — it POSTs to the backend (OBSERVED, p607vk/RunnableC21129z default case):

C21273a.m38425i(new int[]{-1}, AbstractC9039h.m10665v(),
json.toString(), "data",
AbstractC0039a.f168L1 + "eventreport.jsp",
"reportEvent", ShareTarget.ENCODING_TYPE_URL_ENCODED, ShareTarget.METHOD_POST, "");

i.e. https://api.eyecon-app.com/app/eventreport.jsp receives {"name":"android-a001","type":"error","attributes":{"cli":"<user phone number>"},"timestamp":<unix>}. So: emulator detection ⇒ the user’s own phone number is transmitted to the first-party backend. This is the app reporting on itself, not third-party exfiltration — but it is a privacy-relevant automatic transmission of a personal identifier triggered purely by the device environment. See §21-H2.

There is a second, independent emulator gate at a7.w0.m450h() (§3.4) with a slightly different signature set (sdk_gphone, Android SDK built, FINGERPRINT "unknown"; no Nox, no telephony, no netcfg, no /proc). The two detectors are not the same check.

8.4 Remaining e5 classes (full inventory)h5
RealDiskKindPurpose / key members
e5.aAbstractC16098autilSAF file helpers: m31458a(File,int,InterfaceC16421e)→Comparable, m31459b(int,Uri,String)→boolean, m31460c(int)→File, m31461d(String,String)→Uri, m31462e(Comparable,String,String)→Comparable
e5.a0C16099a0adapterViewPager.Adapter (getCount, getPageTitle, instantiateItem, destroyItem, isViewFromObject, getItemPosition) + m31463a(C16099a0,Bitmap,View), m31465c(View,C18712a0,int,C18736y), m31466d(String,View,RunnableC3850i); fields f39625i/f39626j
e5.bC16100butilFileFilter — accept(File,String)→boolean (extension filter)
e5.cRunnableC16101clambdarun()
e5.eEnumC16103eenumrecording filter/sort mode; f39638a int; m31478a()→C11859p builds the {android_version, manufacturer, device, mode} object consumed by a7.r0
e5.gViewOnTouchListenerC16105glisteneronTouch(View,MotionEvent); field f39656b
e5.hViewOnClickListenerC16106hlisteneronClick
e5.iViewOnClickListenerC16107ilisteneronClick
e5.jRunnableC16108jlambdarun()
e5.kRunnableC16109klambdarun()
e5.lC16110ltaskAbstractC16130b impl (mo415j/mo308k/mo309m)
e5.nC16112nItemDecorationgetItemOffsets(Rect,View,RecyclerView,State)
e5.oC16113oItemDecorationgetItemOffsets(...)
e5.pC16114pItemDecorationgetItemOffsets(...)
e5.qC16115qViewPager.OnPageChangeListeneronPageSelected, onPageScrollStateChanged, onPageScrolled; f42676a
e5.rC16116rlambdaMediaPlayer.OnCompletionListener
e5.sRunnableC16117slambdarun()
e5.tC16118tlistenerAnimator.AnimatorListener; f39685a int
e5.uViewOnClickListenerC16119ulisteneronClick
e5.vViewOnClickListenerC16120vlisteneronClick
e5.wViewOnClickListenerC16121wlisteneronClick; f39693a = true
e5.xC16122xViewHolder/taskrecordings row: 15 view fields (f39696c…f39709p) incl. LottieAnimationView f39705l; mo721c0(AbstractC16130b) resolves the contact name from a7.a.f219g; m31498i()→C16104f, m31499j(Bitmap,C16104f), mo723w(Bitmap), mo722s(), mo716Q, mo717U(ArrayList), mo718V(String)
e5.yC16123yadapterRecyclerView.Adapter for RecordingsFragment: fields f39712d/f39726r (ArrayLists), f39714f (RecyclerView), f39715g, f39716h (fragment), f39717i (MediaPlayer), f39718j (Handler with C3504k(…,3)), f39719k (HashMap), f39720l (long, −1), f39721m (int[]{-1,-1}), f39727s, f39713e = (int) Math.ceil(AbstractC0062l0.m368w1() * 0.24f) (row height from 24 % of screen height), f39723o/f39724p (R.drawable.in_call/out_call). Methods m31500a/m31501b/m31502c/m31503d(), getItemCount, getItemId, onBindViewHolder, onCreateViewHolder
e5.zC16124zFragmentmo4090i0(LayoutInflater,ViewGroup)→View, onDestroy(); three C3888t view refs

9. Package m7 — real m7 (disk p420m7, 36 files, 198.9 KB)h4

Responsibility: contact-photo storage on disk (Blowfish-encrypted), a Google-Images scraper built on JS-injected WebViews, the “quotes/inspiration” content, and bitmap/network image utilities.

9.1 m7.d — C18517d — ContactsPhotosStorage (p420m7/C18517d.java, 2.4 KB)h5

Kind: public final class C18517d. Fields: f46764e (instance), f46765a (Cipher), f46766b = new C16945i(1,1,"ContactsPhotosStorage",false), f46767c (boolean — “crypto initialised”), f46768d (byte[1024] reusable read buffer). Constructor: schedules RunnableC17750b(this, 10) on f46766b and starts it. Methods:

  • public static File m34710a(MyApplication) → MyApplication.f12172q + "/ContactsPhotos" when a7.b0.m178a0() (external storage mounted) else getFilesDir() + "/Eyecon/ContactsPhotos".
  • public static void m34711b() — deletes every file in that directory twice (loop repeated; the second pass is a no-op in practice).
9.2 The Blowfish photo-store cipher — WEAK CRYPTO + DESERIALIZATION (OBSERVED)h5

Key derivation — p379k5/RunnableC17750b.java:396-421:

String prefix = MyApplication.f12163h.getString(R.string.cipher_key); // = "Ewu"
String salt = (String) SharedPreferencesC16497y.m32082b("", "cipher_key");
if (isEmpty(salt)) {
salt = String.valueOf(new Random().nextInt(RoomDatabase.MAX_BIND_PARAMETER_CNT)); // 0..999
MyApplication.m10347i().m32076c(salt, "cipher_key");
MyApplication.m10347i().m32074a(null);
}
SecretKeySpec key = new SecretKeySpec((prefix + salt + "sTQHGK").getBytes(), "Blowfish");
Cipher.getInstance("Blowfish").init(ENCRYPT_MODE, key); // discarded
c18517d.f46765a = Cipher.getInstance("Blowfish");
f46765a.init(DECRYPT_MODE, key);

Defects (OBSERVED):

  1. Keyspace = 1000. The key is "Ewu" + an integer in [0,999] + "sTQHGK". Both literals are in the APK (res/values/strings.xml:272 cipher_key = "Ewu", and "sTQHGK" hardcoded). The random value is persisted in plaintext prefs, so recovering it is trivial — and even if it is not recovered, brute-forcing 1000 candidates against any one stored file is instant. All stored contact photos on the device are therefore decryptable by anyone with filesystem read access (root, ADB backup, or another app on a pre-Android-11 device with broad storage perms).
  2. java.util.Random, not SecureRandom — nextInt(1000) is predictable; seeded from System.nanoTime() on most devices, so the sequence is recoverable.
  3. Blowfish/ECB, no IV — deterministic, structurally leaking, 64-bit blocks.
  4. No authentication — no MAC/AEAD.

Consumption path (OBSERVED, p516r5/C19854e.java:898-963 and am/RunnableC0215t1.java:102-214): the contact-photo bitmap cache is read with

new ObjectInputStream(
new CipherInputStream(new ByteArrayInputStream(cipherBytes, 0, len), c18517d.f46765a))
.readObject() // -> byte[] -> BitmapFactory.decodeByteArray

The only gate before this is c18517d.f46767c && AbstractC22312g.m39747M("android.permission.READ_EXTERNAL_STORAGE") — a check that the permission is declared/present, not that it is granted.

Combined risk (INFERRED, medium-high confidence): because the key space is 1000, anyone who can plant a file into the ContactsPhotos directory (external storage on legacy layouts, or any path m34710a() resolves to when external storage is mounted) and can force the app down this read path controls the byte stream fed to ObjectInputStream.readObject(). That is a classic Java-deserialization gadget sink. Mitigating factors: allowBackup="false"; the trigger requires a specific contact-lookup code path; and modern Android scoped storage limits external writes. Still: HIGH as a design flaw (see §21-F5) — the combination of a 1000-value key space and ObjectInputStream on the decrypt path should be treated as a code-execution risk if any gadget chain is reachable in the DEX (the APK bundles a very large third-party SDK surface, so one almost certainly is).

9.3 m7.l — C18533l — the Google-Images scraper (p420m7/C18533l.java, 31.5 KB)h5

Kind: public final class C18533l — the orchestrator for reverse-image/photo search. Fields: f46815b (int), f46816c (Object[] search terms), f46817d (WebView[]), f46818e (Object host), f46819f (Object), f46820g (HashMap of pending JS callbacks), f46821h (Object parsed link array), f46822i (C11859p — the google_photos_link_loader RC object). Methods: m34721j(String)→ArrayList, m34722a(String), m34723b(String name,String str), m34724c()→C19264v, m34725d()→int, m34726e(String host), m34727f(PhotoPickerActivity), m34728g(String,C19264v), m34729h(String,int,int,boolean,boolean), m34730i(String), toString().

9.3.1 m34727f(PhotoPickerActivity) — OBSERVED (C18533l.java:199-221)h6
String ua = AbstractC15804d.m31091l("user_agent", false); // RC key
String searchUrl = ((C11859p) f46822i).m14613s("search_url").mo14598m();
C18531k jsBridge = new C18531k(this);
for (int i = 0; i < webViews.length; i++) {
WebView wv = new WebView(activity);
wv.getSettings().setBuiltInZoomControls(true);
wv.getSettings().setJavaScriptEnabled(true);
wv.getSettings().setUserAgentString(ua);
wv.getSettings().setLoadsImagesAutomatically(false);
wv.setWebViewClient(new C18519e(this));
wv.setTag(terms[i]);
wv.addJavascriptInterface(jsBridge, "Android"); // <-- privileged JS alias
wv.loadUrl(searchUrl + terms[i]);
}

Facts:

  • The search URL, the user agent, the harvesting script and the click-through script are all Remote Config values, read from d5.d:
    • user_agent — compiled-in default Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36 (d5.d:271) — a desktop Chrome/Windows UA presented from an Android WebView (UA spoofing, used to defeat UA-based gating on the search engine).
    • google_photos_link_loader (d5.d:272) — a JSON object with search_url (https://www.google.com/search?hl=en&udm=2&q=), max_links (75), links_script (JS that walks document.getElementsByTagName("a"), reads each anchor’s <img> child src / data-src, assigns result.webview_tag, result.small / result.data, pushes into search_results and returns JSON.stringify(search_results)), and big_url_script (JS that clicks [ITEM_ID], waits, then regex-extracts the imgurl= query parameter from the resulting google_url).
  • addJavascriptInterface(..., "Android") — the highest-risk alias name. m7.k (C18531k) exposes @JavascriptInterface onBigLink(String tag, String p1, String p2) which resolves a pending callback out of f46820g and completes it with an arbitrary JS-supplied string, plus a no-op test(String).
  • m34726e(String host) validates the host against an allow-list (AbstractC19504b.m35900b(AbstractC15999a.m31378d(0,0,host,7,false)) — URL canonicalisation
    • sha256 hex) and throws IllegalArgumentException("unexpected host: …") otherwise, and m34728g/m34729h throw IllegalStateException("Invalid URL host: …") on malformed links. So there is host validation — but the allowed hosts are effectively whatever the RC-supplied search_url points at.

Security conclusion (INFERRED, high confidence): whoever controls the Remote Config project can point this component at any URL and have the app execute arbitrary JavaScript in a JS-enabled WebView that is granted a privileged Android bridge and a spoofed desktop user agent — with results flowing back into the app. Combined with the fact that w6.a.m38423g attaches e-auth* headers only to *eyecon-app.com* URLs, this is a remote code-in-content / web-abuse primitive. See §21-H3.

9.4 Remaining m7 classes (full inventory)h5
RealDiskKindPurpose / key members
m7.aC18511aLayoutLayout subclass for a custom span: getSize(Paint,CharSequence,int,int,FontMetricsInt)→int, draw(Canvas,CharSequence,int,int,float,int,int,int,Paint), m34704a(FontMetricsInt)→int
m7.bC18513bdataString f46745a + compareTo — a sortable string holder
m7.cAbstractC18515cutilpublic static final C16945i f46749a = new C16945i(1,1,"PhotosPath",true); f46750b (static ArrayList)
m7.c0C18516c0Fragmentphoto picker: fields f46751H/f46752I/f46753J (Files), f46758O/f46759P (ints), f46754K(C17524j), f46755L(C3888t), f46756M(C21557e), f46757N=true, f46760Q/f46761R(RunnableC21886n), f46762S(RunnableC21876d), f46763T=-1; mo4120k0, onActivityResult, onRequestPermissionsResult, onViewCreated, onDestroy, m34705p0(C18516c0,File), m34706q0(int)→File, m34707r0, m34708s0(Uri), m34709t0
m7.eC18519eWebViewClientmo407a(), onLoadResource, onPageFinished, onReceivedHttpError, shouldOverrideUrlLoading(String)→boolean
m7.fC18521fmarkerempty class body
m7.gC18523glambdaonReceiveValue(Object)
m7.g0C18524g0Fragmentzoomable image viewer: f42780i (Bitmap), f42781j (C19862i), f42782k (ZoomImageView), f42783l; mo4090i0, onCreateView, onDestroy, onPause
m7.hC18525hWebView hostmulti-WebView analytics/impression tracker: f46784a(String[]), f46786c/f46787d(C18519e), f46788e(WebView), f46789f(WebView[]), f46790g(C18622a), f46785b, f46791h=-1, f46792i(HashSet), f46793j(LinkedHashMap); statics m34717a(C18525h)→JSONArray, m34718d(WebView,String), methods m34719b, m34720c
m7.iC18527idataf46795a/f46796b/f46797c; getBody()→String, getFid()→String, getHeaders()→String — a prepared HTTP request descriptor
m7.jC18529jlambdaonReceiveValue(Object); f46808a
m7.j0C18530j0listenerGestureDetector.OnDoubleTapListener + onFling/onLongPress/onScroll/onSingleTapConfirmed
m7.kC18531kJS bridge ("Android")@JavascriptInterface onBigLink(String,String,String), @JavascriptInterface test(String)
m7.lC18533lscrapersee §9.3
m7.l0C18534l0listenerScaleGestureDetector.OnScaleGestureListener
m7.mC18535mutila self-contained Blowfish implementation (int[1024] P/S boxes, double[][], char[], m34731a(Bitmap)→String) — used for the photo payload encoding. static final double[] f46825k (digit constants)
m7.m0EnumC18536m0enum5 constants f46836a…f46840e — image-scale/format modes
m7.n0C18538n0data3 floats + ImageView.ScaleType f46846d — a zoom/scale triple
m7.oInterfaceC18539ointerfaceimage-load completion callback (used by m7.p)
m7.pC18540pimage loaderf46847c (singleton, name "OnlinePicLoader"), f46848d (lock), f46849e (HashMap cache), f46850f (C11856m), f46851a (pool), f46852b (Pools.SynchronizedPool(3)); statics m34732a(C18540p,String,boolean,InterfaceC18539o)→HashMap, m34733b(String,String), m34734c(String), m34735d(String), m34736e(...), m34737f(String,InterfaceC18539o), m34738g(URLConnection[],String,int[],String[],InterfaceC18539o)→InputStream, m34739h(String,String)
m7.qInterfaceC18541qinterfacemarker
m7.sAbstractC18543sutilbitmap toolkit (35.9 KB): m34740a(Bitmap,CompressFormat)→byte[], m34741b(Uri,Uri), m34742c(int,int,Config)→Bitmap, m34743d(Bitmap), m34744e(String), m34745f(int), m34746g(Drawable,boolean), m34747h(String), m34748i(), m34749j(Drawable), m34750k(Drawable,int,int), m34751l(int,int,int,AbstractC16130b), m34752m(int)→Drawable, m34753n(int,Resources,boolean), m34754o(Bitmap,int,boolean,String,boolean), m34755p(Bitmap,float), m34756q(URLConnection[],String,int[],String[])→InputStream, m34757r(Bitmap,int,int), m34758s(Bitmap,Canvas,int,int), m34759t(Bitmap,File,CompressFormat)→boolean; f46857a (lock), f46858b (cached Bitmap)
m7.xC18548xtaskAbstractC16130b impl (mo415j/mo308k/mo309m)
m7.yC18549yDialogFragmenttheme/photo dialog: f46881H(C19862i), f46883J, f46885L(C20667k), f46882I, f46884K = "no_action"; statics m34760p0(int,AbstractActivityC3498e,Runnable,C19862i), m34761q0, m34762r0; onShow, onClick, run(), mo4120k0, mo4121l0, onDestroy
m7.a0RunnableC18512a0lambdarun()
m7.b0RunnableC18514b0lambdarun()
m7.e0RunnableC18520e0lambdarun()
m7.f0GestureDetectorOnDoubleTapListenerC18522f0listeneronDoubleTap, onDoubleTapEvent, onSingleTapConfirmed
m7.i0RunnableC18528i0lambdazoom/pan animator; fields f46798a(long), f46799b…f46803f, AccelerateDecelerateInterpolator f46804g, f46805h/f46806i (PointF)
m7.k0InterfaceC18532k0interfacemarker
m7.nRunnableC18537nlambdarun()
m7.rRunnableC18542rlambdarun()
m7.zRunnableC18550zlambdarun()
m7.d0ViewOnLayoutChangeListenerC18518d0listeneronLayoutChange(...)

10. Package i7 — real i7 (disk p340i7, 39 files, 130.5 KB)h4

Responsibility: an app-authored custom-View / Drawable / RecyclerView library. No security-relevant logic; no network, no storage, no crypto.

RealDiskKindPurpose / key members
i7.aInterfaceC17157ainterfaceonLoadSuccess-style callback (mo31519h()→Boolean)
i7.a0C17158a0listenerCompoundButton.OnCheckedChangeListener
i7.bC17159blistenerAnimator.AnimatorListener
i7.cInterfaceC17161cinterfaceh-shaped callback
i7.dC17163dutilstartScroll(int,int,int,int,int) override; f42660a
i7.d0C17164d0layout inflater singletonf42661d (instance), f42662a (HashMap layout-id cache), f42663b (Handler), f42664c (ArrayList); m33002a, m33003b(int,ViewGroup)→LayoutParams, m33004c/m33005d/m33006e/m33007f/m33008g (inflate variants), m33009h(FrameLayout,int), m33010i(int,LayoutInflater,HashMap), m33011j(int,View), m33012k(Runnable,boolean), m33013l(int). Used by a7.t.m425J0 to inflate R.layout.message_webview_layout
i7.eC17165eutilCanvas f42665a, Bitmap f42666b scratch holder
i7.e0InterfaceC17166e0interfacedispatchTouchEvent observer
i7.fGestureDetectorOnGestureListenerC17167flistenerfull OnGestureListener
i7.f0C17168f0ViewGroupdispatchTouchEvent(MotionEvent)→boolean override + setDispatchTouchEventListener; f42668a
i7.gEnumC17169genum4 ints f42673a…f42676d (drawable states); m33014a(int)→ColorStateList
i7.g0C17170g0OutlineProvidergetOutline(View,Outline)
i7.h0C17172h0OutlineProvidergetOutline(View,Outline)
i7.iInterfaceC17173iinterfacemo…i(String)
i7.jEnumC17175jenumsingle constant f42688a
i7.j0AbstractC17176j0view util21 statics: m33016a(int,View), m33017b(float,int,int,View)→ValueAnimator, m33018c(View,int,RunnableC20056j), m33019d(View,Integer,Integer,Integer,Integer), m33020e(int,View,ViewGroup), m33021f/m33022g/m33023h/m33035t(int,View[,int]), m33024i(View,Class)→View, m33025j(int)→String, m33026k(int,int)→RippleDrawable, m33027l(Activity), m33028m(Context,View)→boolean, m33029n(LottieAnimationView,int), m33030o/m33032q/m33033r(ViewGroup)/m33034s/m33036u(View), m33031p(Context,TypedArray,int,int)→int
i7.kC17177kDrawabledraw(Canvas), getOpacity, setAlpha, setColorFilter; f42690g(int), f42691a(Paint), f42692b…f42694d(ints), f42695e(Canvas), f42696f(Bitmap)
i7.k0ViewTreeObserverOnGlobalLayoutListenerC17178k0listeneronGlobalLayout()
i7.lEnumC17179lenumsingle constant f42700a
i7.m0C17182m0utilf42709a (ArrayList); m33037b(View,Runnable), m33038c(View,Runnable)→RunnableC17180l0, m33039a(), mo3712f0(View,Runnable)
i7.nViewOnLongClickListenerC17183nlisteneronLongClick(View)→boolean
i7.n0InterfaceC17184n0interfacef0 callback
i7.oViewOnClickListenerC17185olisteneronClick
i7.pViewOnClickListenerC17186plisteneronClick
i7.qC17187qOutlineProvidergetOutline; f42717b
i7.sC17189sTextWatcherbeforeTextChanged/onTextChanged/afterTextChanged
i7.tAbstractC17190tabstractmo33050a/b/c/d(String), mo33053d(), mo33055f(), mo33056g(String)
i7.uC17191udata4 ints f42723a…f42726d + equals/hashCode/toString
i7.vC17192vdata9 ints f42727a…f42735i + equals/hashCode/toString
i7.wC17193wColorStateListgetOpacity, isStateful, onStateChange(int[]); f42736a(ColorStateList), f42737b(int), f42738c(int)
i7.xRunnableC17194xlambdarun()
i7.yC17195ylistenerTextView.OnEditorActionListener
i7.zInterfaceC17196zinterface5 methods
i7.b0RunnableC17160b0lambdarun()
i7.c0RunnableC17162c0lambdarun()
i7.hRunnableC17171hlambdarun()
i7.i0RunnableC17174i0lambdarun(); f42684b flag
i7.l0RunnableC17180l0lambdarun() — 18.1 KB, also hosts the m7.d photo-read entry point (am interop)
i7.rAnimationAnimationListenerC17188rlisteneronAnimationStart/End/Repeat

(39 files: 36 listed above plus the 3 marker/synthetic classes folded into the table.)


11. Package y7 — real y7 (disk p674y7, 15 files, 83.2 KB)h4

Responsibility: custom themes and the “dynamic area” (Menifa) card system.

RealDiskKindPurpose / key members
y7.iEnumC21881ienumfont theme enum: f59003a (name), f59004b (int), f59005c (Typeface, lazily resolved in m39120b() under synchronized)
y7.jEnumC21882jenumtheme variant enum: 8 ints f59011a…f59018f incl. two booleans f59017g/f59018h
y7.kInterfaceC21883kinterfacea(String)
y7.lC21884ltheme manager singletonf59019g (instance), f59020h=-1, f59021a (ArrayList), f59022b (EnumC21882j), f59023c/f59024d (bools), f59025e (String), f59026f (Bitmap). Statics m39122a(C21884l,boolean), m39123c()→File, m39124d()→Bitmap, m39125e()→File, m39127g(FragmentActivity)→int, m39128i()→boolean; instance m39129b(EnumC21882j,boolean,String,boolean), m39130h(), m39131j(). Also referenced from f7.z for the in-app-update snackbar accent colour (MyApplication.m10346h(R.color.light_main_color))
y7.mC21885mtaskAbstractC16130b impl (mo308k/mo309m)
y7.c / y7.fC21875c / C21878ftasktwo more AbstractC16130b impls with identical shape (mo308k/mo309m)
y7.oC21887oFragmenttheme preview/apply screen: f59033H(String), f59034I(bool), f59035J(LinearLayout), f59036K(C3500g), f59037L(EnumC21882j), f59038M(bool), f59039N/f59040O(CustomThemeView), f59041P(ActivityResultLauncher for StartActivityForResult), f59042Q(Bitmap). Statics m39132p0(C21887o,String), m39133q0(C21887o); instance mo4120k0, onCreate, onDestroy, m39134r0, m39135s0, m39136t0, m39137u0, m39138v0(Bitmap), m39139w0, m39140x0, mo4124o0
y7.aRunnableC21873alambdarun()
y7.bViewOnClickListenerC21874blisteneronClick
y7.dRunnableC21876dlambdarun(); f58983b
y7.eRunnableC21877elambdarun() — also used by b5.e.m3650a to fan out user-property writes (AbstractC3464e.m3650a → C22071c.m39333d(new RunnableC21877e(…,3,str2)))
y7.gRunnableC21879glambdarun()
y7.hRunnableC21880hlambdarun()
y7.nRunnableC21886nlambdarun() (also used by m7.c0)

12. Package gf — disk gf (13 files, 62.1 KB) — Google Play Core In-App Updateh4

(third-party — com.google.android.play.core.appupdate.*)

Identity established from the AIDL descriptor string "com.google.android.play.core.appupdate.protocol.IAppUpdateServiceCallback" (gf/AbstractBinderC16738h.java:23) and the intent "com.google.android.play.core.install.BIND_UPDATE_SERVICE".

RealDiskKindPurpose / key members
gf.bInterfaceC16732binterfaceempty marker (implemented by gf.f)
gf.cAbstractC16733cAppUpdateManager (35 KB)The manager: requestUpdateInfo, startUpdateFlowForResult, completeUpdate, listener registry, and the app’s version string via AbstractC18646q.m34958V0(…, "Client")
gf.aC16731adataInstallStatus: f41565a (updateAvailability), f41566b/f41567c (PendingIntents), f41568d (already-started flag), m32579a(C16743m)→PendingIntent
gf.dC16734dAppUpdateListenerRegistrytag "AppUpdateListenerRegistry", filter "com.google.android.play.core.install.ACTION_INSTALL_STATUS"; m32609a/register, m32610b/unregister, m32611c() (registers the receiver with RECEIVER_NOT_EXPORTED = 2 on API 33+)
gf.eC16735efactoryzza() → either new C16734d(ctx) (listener registry) or new C16742l(ctx) (storage policy)
gf.fC16736fAppUpdateManager implm32612a(C16731a,int,Activity)→boolean → startIntentSenderForResult(…, 104, …); m32613b(C16498z) unregister
gf.gC16737gasync taskrequestUpdateInfo(...), completeUpdate(...), and the gms-task onComplete path
gf.hAbstractBinderC16738hAIDL stubIAppUpdateServiceCallback; mo32615d(Bundle) (onRequestInfo), zzb(Bundle) (onCompleteUpdate)
gf.iBinderC16739iAIDL stubOnCompleteUpdateCallback — maps error.code != 0 → InstallException
gf.jBinderC16740jAIDL stubOnRequestInstallCallback — parses version.code, update.availability, install.status, client.version.staleness, in_app.update.priority, bytes.downloaded, total.bytes.to.download, additional.size.required, four PendingIntents and the four update.precondition.failures:* int lists → builds C16731a
gf.kC16741kAppUpdateServicef41588e tag "AppUpdateService", f41589f = Intent("com.google.android.play.core.install.BIND_UPDATE_SERVICE").setPackage("com.android.vending"); statics m32616a(C16741k,String)→Bundle (adds package.name, version.code), m32617b()→Bundle. Ctor verifies the Play Store signing certificate: SHA-256 of com.android.vending’s signature must equal 8P1sW0EPJcslw7UzRsiXL64w-O50Ed-RBICtay1g24M, or GXWy8XF3vIml3_MfnmSmyuKBpT3B0dWbHRR_4cgq-gA when Build.TAGS contains dev-keys/test-keys — otherwise the service is not created
gf.lC16742lstorage policym32618a(File)→long recursive size of <filesDir>/assetpacks
gf.mC16743mdataAppUpdateOptions builder: m32619a(int) (appUpdateType, allowAssetPackDeletion=false fixed), equals, hashCode, toString

Security note: gf.k’s certificate check is the standard Play Core check and is implemented correctly (constant-time-insensitive string compare is acceptable here). Hardcoded digests are public Play Store values, not secrets.


13. Package g7 — real g7 (disk p305g7, 1 file, 0.7 KB)h4

RealDiskKindPurpose
g7.aAbstractC16716aabstract class … extends BroadcastReceivergoAsync() receiver base. public abstract void mo10056a(Context, Intent); onReceive(Context, Intent) defers everything to C16945i.m32771c(new RunnableC3589a(this, context, new Intent(intent), goAsync(), 11)) — i.e. it copies the Intent, takes the PendingResult, and finishes it asynchronously. Used by every app BroadcastReceiver so long work cannot trigger an ANR

14. Package l — disk p394l (16 files, 26.7 KB) — Glide internalsh4

(third-party — Glide image loading)

Identity: l.l declares Set<String> built from {"image/jpeg","image/webp","image/heic","image/heif"} via AbstractC19779r.m36257V0 (Glide mime-type set); l.j is an InputStream over a ByteBuffer; l.d is an int-pair ScrollView OnScrollChangeListener subclass.

RealDiskGlide role
l.aC18253aResource subclass (extends AbstractC22312g)
l.bAbstractC18254bempty synthetic marker
l.cC18255cResource wrapper / ResponseHeader (extends er.AbstractC16369r), close() + read(C16354h,long) with exception capture
l.dC18256dTransition/RequestOptions int pair
l.eC18257eRequestBuilder coroutine continuation (invokeSuspend)
l.fC18258fPreloadTarget/RequestBuilder coroutine body
l.gAbstractC18259g$WhenMappings — int[] {1..28}, m34395a/b/c, m34398d(int)→int[]. Note: m34396b/m34397c throw null when the argument is 0 — these are the decompiled forms of Kotlin’s intrinsic null checks and are only reachable if the generated null-check invariants are violated (i.e. never in practice). x6.b calls AbstractC18259g.m34398d(3) to clamp typeRequest
l.hC18260hBitmapDrawableResource - f45632a (BitmapDrawable) + f45633b (boolean), with equals/hashCode
l.iC18261iDownsampleStrategy/PreloadOptions data class (f45634c singleton)
l.jC18262jByteBufferInputStream extends InputStream — mark/reset over a ByteBuffer (f45637a mode, f45638b = mark position, default 1073741824)
l.kAbstractC18263kpublic static final Paint f45640a = new Paint(3) — the Glide “tag” paint used by RequestBuilder
l.lAbstractC18264lthe mime-type Set
l.mC18265mRealResource extends AbstractC18266n — close() (closes f45647f and f45645d), mo34401d(), mo34402h()
l.nAbstractC18266nabstract class … implements Closeable — mo34401d()→AbstractC22312g, mo34402h()→InterfaceC16358j
l.oC18267oPooledByteBufferInputStream extends AbstractC22312g (f45648a)
l.pC18268pDataCacheWriter extends AbstractC18266n — requires a directory (IllegalArgumentException("cacheDirectory must be a directory.")), close(), mo34401d(), mo34402h() (throws IllegalStateException("closed") after close)

15. Package oq — real oq (disk p466oq, 47 files, 186.8 KB) — OkHttp 4.x internalsh4

(third-party — okhttp3, Kotlin rewrite)

Identity: oq.a holds SocketFactory, SSLSocketFactory, HostnameVerifier, Socket, ConnectionSpec and CertificatePinner fields (the OkHttpClient.Builder shape); oq.f is sslSocketFactory(SSLSocketFactory, X509TrustManager); oq.m0 has a Companion and an EMPTY singleton (okio ByteString).

This is the transport C1’s HTTP core actually uses. w6.a.m38417a touches:

Used by w6.aClass
OkHttpClient instanceoq.c0 (C19227c0)
OkHttpClient builder/configoq.b0 (C19225b0)
Request builder (m35533a(k,v) = addHeader)oq.e0 (C19231e0)
Requestoq.f0 (C19233f0)
Response (FirebasePerfOkHttpClient.execute result)oq.o0 (C19251o0)
MediaTypeoq.y (C19268y)
Dispatcher/executoroq.u (C19262u)

Full inventory:

RealDiskOkHttp role
oq.aC19222aOkClient.Builder (10 fields)
oq.a0C19223a0Builder extends oq.m0
oq.bC19224bAddress implements oq.v0
oq.b0C19225b0OkHttpClient.Builder/config (owns the X509TrustManager, lines 12 & 83)
oq.cC19226cResponseBody (extends oq.s0)
oq.c0C19227c0OkHttpClient — 15 hits on X509TrustManager/SSLSocketFactory (platform trust-manager selection, standard OkHttp)
oq.dC19228dConnection
oq.d0EnumC19229d0internal enum
oq.eC19230eHeaders/builder-adjacent helper (extends er.AbstractC16368q)
oq.e0C19231e0Request.Builder (m35533a)
oq.fC19232fRequestBody implements Closeable, Flushable
oq.f0C19233f0Request
oq.gC19234gRoute
oq.g0C19235g0Builder extends oq.m0
oq.hInterfaceC19236hCall.Factory/Call interface
oq.h0C19237h0Builder extends oq.m0
oq.iInterfaceC19238iCloneable interface
oq.i0C19239i0EMPTY ByteString
oq.jInterfaceC19240jinterface
oq.j0C19241j0Builder extends oq.m0
oq.kC19242kAuthenticator
oq.k0C19243k0Builder extends oq.m0
oq.lC19244lComparator
oq.l0C19245l0Companion
oq.mC19246mInterceptor.Chain
oq.m0AbstractC19247m0ByteString base (Companion, EMPTY)
oq.n0C19249n0RealConnection/pool node
oq.oC19250oEventListener
oq.o0C19251o0Response (implements Closeable)
oq.pC19252pCertificatePinner (plain final class, 10.7 KB)
oq.p0C19253p0Reader subclass (challenge/auth reader)
oq.qC19254qRouteException
oq.q0C19255q0ResponseBody subclass
oq.rC19256rEventListener.Factory
oq.r0C19257r0Cache entry
oq.sC19258sBuilder extends oq.m0
oq.s0AbstractC19259s0ResponseBody implements Closeable
oq.tC19260tDns
oq.t0C19261t0ConnectionSpec-adjacent
oq.uC19262uDispatcher implements Iterable
oq.u0EnumC19263u0internal enum
oq.vC19264vConnectionSpec (TLS-version / cipher-suite helper)
oq.v0InterfaceC19265v0q1 interface (address contract)
oq.wInterfaceC19266winterface
oq.xInterfaceC19267xinterface
oq.yC19268yMediaType
oq.zC19269zHandshake

Security-relevant observation (OBSERVED): oq.p (CertificatePinner) exists in the library but is never used — a tree-wide search for CertificatePinner / .pinner( outside okhttp3 internals returns zero hits. The OkHttpClient in w6.a.m38417a is built with new C19227c0(new C19225b0()), i.e. default platform TLS with no pinning, and the HttpURLConnection path likewise relies on the platform trust store. Combined with network_security_config.xml’s app-wide cleartextTrafficPermitted="true", transport security rests entirely on the OS. See §21-A2 / §21-N1.


16. Package qo — real qo (disk p506qo, 38 files, 248.6 KB) — Kotlin stdlib collections + app utilitiesh4

Kind: mostly third-party (kotlin.collections.*), plus one large app-level utility object.

qo.r — AbstractC19779r (74.9 KB) is the app-relevant class. It is a miscellaneous helper holding ~150 static methods, including:

MethodPurpose
public static final boolean m36274e0()return !AbstractC0084w0.m450h(); — the “A/B tests enabled” gate (emulator -> false)
m36261X0(PublicKey, String data, String signature)→booleanRSA signature verification
m36264Z(String x509)→PublicKeyKeyFactory.getInstance("RSA").generatePublic(new X509EncodedKeySpec(...))
m36266a0(String kid)→Stringkey-id resolution
m36265a(JSONObject)→C3459aIgnite service-version resolution (IgniteAuthenticationComponent); consumed by a4.c case 0
m36215A0(byte[],C18240i)→byte[], m36218C(int,int,byte[]), m36302s0, m36303t, m36306u0array copy/fill helpers (incl. System.arraycopy)
m36219C0(InputStream), m36221D0(Socket), m36314y0(Socket), m36224F(File), m36236L(String,String), m36263Y0(String,String)IO/socket/Okio wrappers
m36226G(int,int,RoundingMode), m36288l0(int,RoundingMode), m36229H0(double,EnumC18884c), m36230I..m36234Kduration/time conversions
m36225F0(JSONObject)→C16908a, m36235K0/m36239M0/m36237L0(JSONArray), m36287l(JSONObject), m36292n0/m36294o0(String,JSONObject), m36296p0(String), m36273e(String)JSON helpers
m36275f..m36295pthe kotlin.collections _Arrays/CollectionsKt block: asList, listOf, setOf, mapOf, arrayOf, firstOrNull, indexOf, joinTo, toArray, maxOf, contains, contentEquals, …
m36259W0(String identifier)thread-name prefixing / uncaught-exception tagging

The other 37 qo.* classes are the Kotlin collection implementations (qo.a Collection, qo.b/qo.e0/qo.f0/qo.s Iterators, qo.f List, qo.h Map, qo.i/qo.j/qo.k/qo.l Collection/List/Map/Set bases, qo.a0 ArrayList, qo.b0 HashMap, qo.c0 HashSet, qo.d ListIterator, qo.i0/qo.z ListIterators, qo.t/qo.v/qo.w/qo.u collection chains, qo.g0 Map extends Map, qo.h0 AbstractC16257t1 subclass, qo.m0 InterfaceC16320o (functional), qo.c/qo.d0/qo.g/qo.m/qo.x/qo.z misc, qo.o/qo.p/qo.q/qo.l0 list subclasses, qo.e RandomAccess list, qo.s..qo.w set/map subclasses). All OBSERVED from the class declaration lines; no app-specific behaviour.

Security note: m36261X0 performs RSA signature verification and m36264Z builds a public key from a string. The only in-scope caller chain is Digital Turbine Ignite (a4.e), whose JWT is not passed through these helpers — so signature verification is effectively unused in this cluster. INFERRED (medium confidence).


17. Package pq — real pq (disk p486pq, 5 files, 5377 KB) — okhttp3 internalh4

(third-party)

RealDiskRole
pq.bAbstractC19504bokhttp3.internal.Util — hex dumping, canonicalizeHost, SHA-256 helpers. 5.3 MB on disk — the largest single file in the APK; JADX expanded a very large inline constant/method block here. m35899a(int,int,String)→byte[] is the hex decoder over C18645p("([0-9a-fA-F]*:[0-9a-fA-F:.]*)|([\\d.]+)")
pq.aC19503aResponseBody extends oq.s0 implements InterfaceC16357i0 — contentLength(), contentType()
pq.cAbstractC19505cokhttp3.internal.Util BOMS — f50026a = new byte[0], f50027b = setOf("efbbbf","feff","fffe0000","fffe","0000feff"); m35901a(long,long,long) bounds check
pq.dThreadFactoryC19506dThreadFactory — new Thread(r, name), setDaemon(flag)
pq.eAbstractC19507eokhttp3.internal.http.HttpDate/client helpers — f50030a = TimeZone.getTimeZone("GMT"), f50031b = versionString("okhttp3.", "Client"); m35917a(C19264v, C19264v) ConnectionSpec equality

18. Package n4 — real n4 (disk p429n4, 4 files, 248.3 KB) — androidx R constantsh4

(third-party)

RealDiskKindContent — OBSERVED
n4.aAbstractC18698aconstantspublic static final Date f47377a = new Date(1776946263760L) — a fixed build-time timestamp = 2026-04-23, matching the version string 4.2026.04.23.1145 hardcoded in w6.h ("vc_749_vn_4.2026.04.23.1145_a")
n4.bAbstractC18699bconstantsSparseArray{0 → "_all"} (f47378a) — ConstraintLayout’s “all views” tag
n4.cAbstractC18700cconstantsHashMap(0) (f47379a) — an empty per-process scratch map
n4.dAbstractC18701dconstantsandroidx.appcompat/constraintlayout R.styleable indices: ActionBar_layout_gravity = 0, ActionBar_background = 1, … plus ConstraintLayout_ReactiveGuide_reactiveGuide_applyToAllConstraintSets, ConstraintLayout_ReactiveGuide_reactiveGuide_applyToConstraintSet, MaterialAlertDialogTheme_materialAlertDialogButtonSpacerVisibility. 247 KB of public static final int declarations

19. Remote Config — the complete mechanismh4

There are two independent configuration systems in this app, plus a legacy shared_preference_v2-backed settings cache. Conflating them is the single biggest source of error in this codebase, so they are separated here.

+----------------------------------------------+
Firebase RC ----->| d5.d AbstractC15804d (typed facade) |<-- app-wide reads
(TLS, Firebase) | f38507a = ~350 compiled-in defaults | d5.d.m3108xb(key)
| f38508b = post-fetch callback list |
+----------------+------------------------------+
| m31091l("circus_key", false)
v
b5.e.m3668s() --reflection--> w6.a.rc_key
(-> header `e-auth-k`)
^
Firebase RC cached as --------------+
prefs `FB_RC_<key>`
(written by y5.v / C21867v after fetchAndActivate)
+----------------------------------------------+
CLEAR-TEXT HTTP -->| d5.c C15803c (Eyecon A/B engine) |<-- d5.c.m31076d/m31077e
ab-test.txt | f38506b = merged JsonObject (live) | (16 keys, 19.6)
+ eyeball A/B | f38505a = activated activation events |
+----------------------------------------------+
+----------------------------------------------+
server (w6.h) ---->| f7.d0 C16464d0 "MySettingsInServer" |<-- SP_KEY_MY_SETTINGS_{LOCAL,SERVER}_JSON
+----------------------------------------------+
19.1 System 1 — d5.d AbstractC15804d, the typed Firebase-RC facadeh5

Disk p236d5/AbstractC15804d.java (680 lines) · Real d5.d (outside C1’s disk scope, but it is the public API C1 exposes; documented here because C1’s f7.a JS bridge and m7.l scraper are its main consumers.)

f38507a (static block, lines 40-452) is a HashMap with ~350 compiled-in Remote Config defaults. This is a large secret/endpoint surface in its own right — see §19.7 for the notable entries.

Typed accessors (the public API every cluster uses):

SignatureBehaviour — OBSERVED
public static boolean m31081b(String key)RC boolean; main process -> Firebase value else ((Boolean) f38507a.get(key)); secondary process -> Boolean.parseBoolean(prefs("FB_RC_"+key))
public static double m31083d(String key)RC long/double; strips non-[\d.] chars when the value is not numeric
public static int m31086g(String key)RC long
public static long m31089j(String key)RC long
public static long m31090k(String key, long def)same with an explicit default
public static String m31091l(String key, boolean ignoreRemote)the master accessor (see §19.4)
public static C17656n m31092m(String key)raw Firebase FirebaseRemoteConfigValue; returns null when f44676b == 0 (SOURCE_STATIC) or when qo.r.m36274e0() is false (emulator)
public static String m31093n(String key)secondary-process read: FB_RC_<key> from prefs, else f38507a, else logs "Remote config is missing, key = ..." and returns null
public static Object m31082c(Class cls, String key)country-aware typed read: CountrySpecificConfig.parseConfig(m31091l(key,false), cls).getValue(AbstractC0062l0.m371z1()) (ISO country from a7.l0), then falls back per-type
public static AbstractC11857n m31087h(String key)RC value -> Gson object, null on empty/parse failure
public static AbstractC11857n m31088i(String key)RC value -> Gson object, throws JsonSyntaxException on failure
public static synchronized C17307b m31084e()C19129g.m35481c().m35485b(C17311f.class).m33293a("firebase") — the FirebaseRemoteConfig singleton via the ComponentRegistrar
public static void m31080a(C15805e cb)m31084e().m33287a().addOnCompleteListener(new C21867v(cb, 21)) — fetchAndActivate()
public static ArrayList f38508bpost-fetch callback list (assigned by RegistrationActivity:3735-3741,3885 and q7.RunnableC19693v)

Fetch schedule (OBSERVED): a7.p0 case 10 (RunnableC0070p0.java:334-337) calls d5.d.m31080a(null) then re-schedules itself every 7200000 ms (2 hours). The success path is y5.v (C21867v) case 21 (:806-863).

19.2 System 1 — the fetch success path (y5.v / C21867v, p672y5/C21867v.java:806-863)h5

On a successful fetchAndActivate():

  1. If RC key dev_remote_config_tester == 1, iterate every key in f38507a.keySet() and write m31091l(key,false) into the custom prefs under "FB_RC_" + key (:821-826) — a developer backdoor that snapshots the whole RC space into the local prefs store; then schedule an EyeInAppMessagingHelper reload after 1000 ms and run a7.p0 case 8.
  2. Otherwise enqueue a unique one-time WorkManager job named "RemoteConfigUpdater" (:844-847) with NetworkType.CONNECTED + RequiresBatteryNotLow, 20 s initial delay, EXPONENTIAL backoff from 20 s, tag "RemoteConfigUpdater".
  3. b5.e.m3668s() — the reflective rc_key overwrite (:861).
  4. C18416a.f46357c = d5.d.m31081b("print_logs_in_release") (:862).

On failure, the exception is simply printStackTrace()d.

19.3 System 2 — d5.c C15803c, the Eyecon A/B engineh5

Disk p236d5/C15803c.java (482 lines) · Real d5.c

Kind: public final class C15803c. Statics f38504c (singleton), fields f38505a (HashSet of activation events), f38506b (C11859p — the live merged config).

Constructor (OBSERVED, :50-79):

  1. If !qo.r.m36274e0() (i.e. emulator) -> log "<EyeconRemoteConfig> update is not needed, ab tests are disabled" and return.
  2. Load the cached config from prefs key SP_KEY_EYECON_REMOTE_CONFIG_JO_v2 via f7.y.m32083c(key, null) (a Gson JsonObject stored as a String).
  3. f38505a.addAll(m31075c(cached)) — collect targeting.activation_event values.
  4. Compare EyeconRemoteConfigVersion (an int inside the cached JSON) with RC key eyecon_remote_config_version (compiled-in default 1).
  5. If stale -> new Thread(new a4.c(17, this, new C15802b(this, version, 0))).start() -> the cleartext fetch (§7.1.1).

Config document shape (INFERRED from m31073a, high confidence):

{
"calculated_tests": {
"<test_name>": {
"targeting": {
"exposure": <float 0..100>,
"countries": ["all"|"us"|...],
"languages": ["all"|"en"|...],
"versions": {"from": <int>, "to": <int>},
"activation_event": "<event name>"
},
"didActivationEventTriggered": <bool>,
"winner_variant": { "_variant_name": "...", "_variant_chance": <int>, ...arbitrary keys... }
}
}
}

public static C11859p m31073a(C15803c, C11859p prev, C11859p incoming) — the merge/evaluate routine (OBSERVED, :88-355):

  • For each entry in incoming.tests:
    • if already calculated, re-apply its stored winner_variant;
    • targeting filters, all four evaluated with isNotMatch... semantics:
      • exposure: SHA-256(appInstanceId + " " + test_name), first 8 bytes as a long & Long.MAX_VALUE, (v / 9.223372E18f) * 100f; excluded if > exposure. (Exclusion, i.e. an exposure roll-up / holdback.)
      • countries: Locale.getDisplayCountry(Locale.ENGLISH).toLowerCase() matched case-insensitively against the list, "all" wildcard.
      • languages: same on getDisplayLanguage.
      • versions: hardcoded 749 against {"from","to"}.
    • Deterministic bucketing: BigInteger(SHA-256(appInstanceId + " " + test_name).hex[0..16]).longValue() % SUM(_variant_chance), then a cumulative walk over variants[] picking the winner; sets winner_variant and records _variant_name.
    • "FINAL_DECISIONS_DONT_DELETE" is a magic test name (OBSERVED, :138,284,471): when it wins, m31074b copies all non-underscore-prefixed keys of the winning variant into the root of the config object — i.e. an A/B test can override arbitrary top-level keys.
  • public static void m31074b(C11859p root, C11859p winnerVariant) (:359-367) — the root-merge primitive, skipping keys starting with _.
  • public final void m31078f(String eventName) / m31079g(String eventName) — the activation-event trigger. When a test’s activation_event matches, it marks didActivationEventTriggered=true, fires analytics event "active_split_tests" with ab_test_device_id / test_name / variant_name, then re-applies all triggered winners and persists back to SP_KEY_EYECON_REMOTE_CONFIG_JO_v2. m31078f has a main-thread gate (AbstractC0060k0.m320j()) and, off the main thread, parks on an int[]/wait() handshake (RunnableC16943g).
  • public final Boolean m31076d(String key, Boolean def) and public final String m31077e(String key, String def) — the readers.
19.4 The decisive question: can the cleartext channel set rc_key? — NOh5

This is the correction to BACKEND-API.md §3.3. The proof is in the resolution order of d5.d.m31091l(key, ignoreRemote) (AbstractC15804d.java:613-646), which is the only function b5.e.m3668s() uses:

public static String m31091l(String str, boolean z10) {
HashMap map = f38507a; // compiled-in defaults
if (z10) { ... return map value ... } // unused by m3668s
if (!MyApplication.f12170o) { // SECONDARY process
String v = m31093n(str); // -> prefs "FB_RC_" + key
return v == null ? "" : v;
}
C17656n firebase = m31092m(str); // MAIN process
if (firebase != null) return firebase.m33908d(); // -> FirebaseRemoteConfig
Object o = map.get(str);
if (o == null) return null;
String s = o.toString();
return isEmpty(s) ? null : s; // -> compiled-in default
}

m31091l never touches d5.c (C15803c) or f38506b. d5.c is a completely separate object graph reached only through d5.c.m31076d/m31077e.

Therefore:

ChannelReaches d5.d (circus_key -> w6.a.rc_key -> e-auth-k)?
Firebase Remote Config backendYES
Prefs FB_RC_<key> (written by y5.v after a successful Firebase fetch; and by the dev_remote_config_tester == 1 backdoor)YES
Compiled-in default mapYES (fallback, value PgdtSBeR0MumR7fO)
http://cloudfront-cdn-all.eyecon-app.com/configs/ab-test.txtNO
Local file / DB tamperingYES (prefs are plaintext in contactdb)

Prerequisite for the Firebase rows: m31092m returns null unless AbstractC19779r.m36274e0() is true (i.e. not an emulator, a7.w0.m450h()).

When is m3668s() called? (OBSERVED — 3 call sites)

  • a7.r0:145 — during MyApplication.onCreate (main process), every launch.
  • y5.v:861 — after every successful fetchAndActivate() (i.e. every 2 h, per §19.2).

What an attacker controlling the Firebase RC project achieves (INFERRED, high confidence): they set circus_key to an arbitrary string; the reflective write replaces w6.a.rc_key; from the next request onward every call to api.eyecon-app.com carries e-auth-k: <attacker value> plus the victim’s real e-auth (CID). That is (a) a client-identity forgery primitive that requires no app update and no device compromise, and (b) a way to invalidate the legitimate key, denying service to every client that adopts the new value. Whether the reflective Field.set on a private static final field succeeds is ART-version dependent — on ART the field is static final but not necessarily ConstantValue-inlined for non-primitive-constant strings, so it typically succeeds. BACKEND-API.md already flags this ART dependence; this document adds the resolution-order proof.

19.5 Every cleartext fetch in this cluster (OBSERVED)h5
#WhereURL sourceMethodAuthResponse
1a4.c case 17 -> w6.a.m38423gRC key eyecon_remote_config_url, default http://cloudfront-cdn-all.eyecon-app.com/configs/ab-test.txtGETnone (addAuth=false)JSON A/B document, merged into d5.c.f38506b
—(global permission)AndroidManifest.xml:272 android:usesCleartextTraffic="true" + res/xml/network_security_config.xml <base-config cleartextTrafficPermitted="true"> with <trust-anchors><certificates src="system"/></trust-anchors>; <debug-overrides cleartextTrafficPermitted="true"> with <certificates src="user"/>; <domain-config cleartextTrafficPermitted="true"><domain includeSubdomains="true">127.0.0.1</domain></domain-config>

debug-overrides is inert in this build because android:debuggable is not set in AndroidManifest.xml (verified by exhaustive search). So the user-CA trust anchor does not apply in the shipped APK — good. The cleartextTrafficPermitted="true" base config is the live problem.

Mitigating: BACKEND-API.md §9 already lists the other three cleartext destinations (http://numberlookup.instabridge.com/respond-sms, http://join.eyecon-app.com/, plain-UDP ptt.eyecon-app.com). Those are outside C1’s 16 packages.

19.6 Every key the cleartext channel can reach (OBSERVED)h5

These are the only keys read through d5.c (f38506b), i.e. the only MITM-controllable configuration in the app:

KeyRead at
reg_flow_modeRegistrationActivity:622
permission_prompt_textRegistrationActivity:652
allow_asking_role_multiple_times_in_a_rowRegistrationActivity:1187
login_orderRegistrationActivity:1397
eyecon_consent_show_locationRegistrationActivity:1901
reg_ask_runtime_permission_firstRegistrationActivity:1922
is_short_onboarding_enableRegistrationActivity:4231
show_fake_spam_call_for_sourceRegistration/Onboarding/FakeCallActivity:191
is_new_onboarding_enableRegistration/Onboarding/OnboardingActivity:52
reg_ask_sms_permissionp360j7/C17534t:193,275; p494q7/C19640a0:38
reg_ask_location_permissionp360j7/C17534t:201
use_pre_happy_flow_mandatory_logicp360j7/C17534t:220,296
is_fullscreen_intent_a_mustp360j7/C17534t:250
is_aftercall_splash_screen_ads_enablep645x4/C21609j:95
is_aftercall_interstitial_ads_enablep645x4/C21609j:189,215

Plus, structurally:

  • the activation-event -> test -> winner mechanism (m31078f), so a remote document decides which of the app’s own named events activate which test, and the FINAL_DECISIONS_DONT_DELETE test can add arbitrary new keys to the root config (m31074b) — meaning the effective key set is open-ended and server-defined, not limited to the 16 above. INFERRED (high confidence).
  • d5.c.f38506b is persisted verbatim to prefs key SP_KEY_EYECON_REMOTE_CONFIG_JO_v2, so a single successful injection is sticky across launches even if the network is clean afterwards. OBSERVED (C15803c.m31079g:476-478).

Concrete impact of controlling that channel (INFERRED, high confidence): the attacker can suppress or alter the consent/permission prompt text, disable the SMS and location permission requests, change the onboarding/registration flow order and skip steps, suppress the after-call splash/interstitial ads (or conversely force them), and turn the fake-spam-call demo on or off. This is a consent-flow tampering and user-deception primitive, not a code-execution one.

19.7 Notable compiled-in RC defaults that are themselves sensitive (OBSERVED, d5.d static block)h5
KeyDefaultWhy it matters
circus_keyPgdtSBeR0MumR7fOthe API auth key (§19.4)
eyecon_remote_config_urlhttp://cloudfront-cdn-all.eyecon-app.com/configs/ab-test.txtthe cleartext channel itself
eyecon_remote_config_version1drives the fetch
uid2_config{"subscription_id":"TSqMwCWMkK","public_id":"UID2-X-P-MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAEEsoYptIJKSUsi1ZysIEsDgDZiUMrlB/IeLTcDWgRLaKyNpH6szy9Mg+Kk7h8h1JMHrVkoB9E1Z+GN45rSsr0uw=="}hardcoded LiveRamp/UID2 identity shipped in the APK; used for advertising identity resolution
dev_remote_config_tester0when set to 1, dumps the entire RC key space into local prefs (C21867v:817-826) — a developer backdoor reachable by anyone who can flip the RC value
print_logs_in_releasefalsewhen true, enables lo.a.m34588y(...) -> writes every formatted log line to /data/data/com.eyecon.global/files/eye_log.txt (5 MB rotation, trimmed to 50 lines). Production disk logging of tags, URLs, CIDs and response fragments, switchable remotely. See §21-H4
user_agentMozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36desktop-Chrome UA spoofing (§9.3)
google_photos_link_loaderfull JSON: search_url, max_links, links_script, big_url_scriptremote-controlled JS injection into the photo scraper (§9.3)
ac_ads_test_internet_connection_urlhttps://www.google.comconnectivity probe destination
dynamic_area_urlcards.eyecon-app.comdynamic-area content host
call_contest_report_calls_urlhttps://apps.eyecon-app.com/call-contest/a/report-callscall-volume reporting endpoint
welcome_back_notification_urlGoogle Forms viewform + formResponse URLswin-back flow via a Google Form
analytics_worker_urldisabled_by_remoteanalytics upload endpoint
server_report_url""telemetry endpoint
support_emailsupport@eyecon-app.com—
premium_features, premium_products, premium_products_v2, premium_products_ids_list, golden_users_productslarge JSON blobs / ~60 SKU ids (no_ad_yearly_2.99, yearly_100_dollar_3_day_trial_v1, whatsapp_id_1.99, custom_background_theme, golden_user_test_product, …)the entire monetisation catalogue is remote
*_ad_unit_id / *_gam_combine_unit_id / *_admob_* / *_native_ad_unit_id~40 AdMob ids under publisher ca-app-pub-2959190743815944 and ~40 GAM paths under network /21825178605/, plus Amazon slot UUIDs and mobitech_* publisher keys EC170523APP / EC180623Othe ad topology
amplitudeNotTrackableEventsa ~200-entry JSON array of event names excluded from Amplitudeanalytics policy
singular_events_json, singular_session_timeout (43200000 ms = 12 h)frequency caps + timeoutattribution policy
regexForAddSocialregexes for Facebook profile URL / fb://profile/(\d+) / Instagram profile + og:image + instagramMatchesRegexsocial-profile scraping rules
facebooke_Extract_URL_Regex, findFirstMp4UrlForInstagramRegexbrowser_native_sd_url":"([^"]+)", https?://[^\s'"]+\.mp4[^\s'"]*media scraping rules
mobitech_video_html_script, mobitech_content_html_script, mobitech_interstitial_html_scriptraw HTML+JS wrapping third-party ad tags (the-news-desk.com, s3.amazonaws.com/embed.footylight.com)ad-tag injection
balwan_recognizerJSON array of URL/segment matchers, first entry fbcdn.net with 7 hardcoded Facebook CDN segment idsrecognises specific CDN assets
in_app_update{"type":"immediate","max_appearance_per_version":-1,"cool_down_time":1}in-app update policy (gf)
notSupportedKeys{} (read by p621w7)key allow/deny list

Full default list: BACKEND-API.md §8.3 lists a subset; the authoritative source is p236d5/AbstractC15804d.java:40-452.

19.8 RC-controlled behaviour switches worth calling outh5
KeyDefaultEffect
server_connection_test{"enabled":true,"times_pattern":[60,6]}periodic backend reachability probing
must_permissions_worker_enable / _repeat_timetrue / 1a worker that repeatedly nags for permissions
DEV_should_consume_chromium_exceptiontruesuppresses chromium WebView crash reporting
mobitech_oom_max_crashes_allow, mobitech_oom_crash_counter_version5, V1disables the ad SDK after N OOM crashes
allow_to_reset_compact_aftercall_views, compact_after_call, use_IB_ad_loading*mixedafter-call UI / ad-loading strategy
server_data_cache{"enabled":true,"photo_expiration_time":604800000,"name_expiration_time":604800000}7-day photo/name cache
toki_ports[5620,50000-50010]plain-UDP push-to-talk port list
open_whatsapp_intentsfour large intent templatesdeep-link construction
isWebviewTimersHandlerEnable, is_photo_reveal_anim_view_enable, debug_test, debug_wait_call_activity_timeout—debug/dev switches shipped in release builds

20. Public API surface other clusters consume from C1h4

20.1 The credential (clientId) read path — the single most-used C1 APIh5
com.facebook.appevents.AbstractC9039h.m10662s() (SDK facade, com.facebook.appevents)
+-> p358j5.AbstractC17503r.m33607k("clientId", "")
+-> p284f7.SharedPreferencesC16497y.m32082b("", "clientId") <-- C1
presence check: AbstractC9039h.m10665v()

AbstractC9039h also exposes:

  • m10631K(Throwable) / m10632L(Throwable) — the app’s global error reporters; m10632L is called from eo.b.m31717e with the raw prefs key and value on a read failure, so personal data reaches logcat.
  • m10641U(String cid, String reason) — the destructive 401 handler (see BACKEND-API.md §3.5).
  • m10644a(boolean, Throwable) — Crashlytics report gate.
  • m10661r()→long — join timestamp (used by f7.n contest counting).
  • f13178e — a static CID used to avoid self-de-registration.
20.2 MyApplication static facade (com.eyecon.global.Others.MyApplication, 597 lines)h5
MemberTypeUse
f12163hMyApplicationapp singleton
f12170obooleanisMainProcess() — gates config reads, error handling and 401 handling
f12172qFileexternal-storage root (when mounted) — selects the photo store location
f12171pC19858gthe CallLog ContentObserver
f12174sRunnableC16486o0lifecycle dispatcher
f12166k / f12167lC0072q0 (a7.q0)LottieImageAsset caches
m10342d()→Contextapp context
m10343e()→ResourcesgetString(R.string.*) — the source of pk_enc, cipher_key, facebook_app_id, quotes_api_id, amazon_app_key, applovin_key, app_vestor_*
m10349k()→Resourceslocalised strings
m10344f(int)→int, m10345g(int,Context)→int, m10346h(int)→intdimension / colour resolution
m10347i()→SharedPreferencesEditorC16496xwrite API — m32076c(value,key) + m32074a(null)
m10348j()→SharedPreferencesC16497yread API — getBoolean/getString/..., m32082b(def,key), m32083c(key,defJson), m32085d(key)
20.3 C1 APIs used from outside C1h5
C1 symbolReal nameExternal consumers (representative)
MyApplication.m10348j() / m10347i()f7.y / f7.xessentially every cluster
AbstractC0039a column constantsa7.ae5, eo, m7, x6, f7, a4, plus all com.eyecon.global.* activities, w6, b5, q7
AbstractC0062l0a7.l0b5, d5, j7, q7, x4, e5, all activities (density/locale/country)
AbstractC0077ta7.tevery activity (Toasts/dialogs/notifications), incl. f7.z
AbstractC16468f0f7.f0b5, w6, d5, a7, e5, m7, y7, f5, activities (esp. m32019n for backup)
C16458a0f7.a0every DAO in q7, j7, b7, v8, …
C0041ba7.bDBContacts, e5, f7, eo, m7, x6, d5
eo.beo.bf7.y, f7.x, a4.d
AbstractC0060k0.m314d()a7.k0d5.c (A/B bucketing), d5.d
C16489qf7.qe5.m
AbstractC0084w0.m450h()a7.w0qo.r -> d5.c, d5.d
AbstractC19779r.m36274e0()qo.rd5.c, d5.d
C18517dm7.dr5.C19854e, am.RunnableC0215t1, bm.*, lg.*, p6.*
C18533l / C18531km7.l / m7.kPhotoPickerActivity, MenifaFragment, v8.*
C21884ly7.lf7.z, themes
C16945i (p315h7)thread poolused by C1 everywhere; owned by another cluster
C18416a (p410lo)logging/formattingused by C1 everywhere; owned by another cluster
C21273a / C21280hw6.a / w6.ha4.c, d5.c, e5.m, x6.*, m7.*
C21569d (p262e7)AbstractC16130bthe three-state callback interface every C1 task implements

Net effect: C1 is the narrow waist. Three symbols — a7.a’s column constants, f7.y/f7.x prefs, and a7.b’s contactdb handle — are the interface through which all other clusters talk to storage. Any change to their shapes is a whole-app change.

20.4 Third-party APIs C1 exposes outwardh5
  • oq (OkHttp) — C1 ships the OkHttp runtime that the rest of the app (and the third-party ad/attribution SDKs) use. w6.a.m38417a is the only place C1 configures it.
  • gf (Play Core) — gf.c/gf.k provide in-app update; f7.z renders the “update downloaded / restart” snackbar.
  • g7.a — every app BroadcastReceiver extends it.

21. Security findings register (C1 scope)h4

Severity uses: CRITICAL > HIGH > MEDIUM > LOW > INFO. F = crypto/data-protection, H = hardcoded secret / exfiltration, N = network, A = anti-analysis.

F1 — Backup/diagnostic payloads protected by RSA-PKCS1v1.5 + Blowfish/ECB, no AEAD — HIGHh5

Where: f7.f0.m32019n (p284f7/AbstractC16468f0.java:354-366), duplicated at tc.C20369c:639-640. Detail: see §4.6.1 — an AES-generated key used with a Blowfish SecretKeySpec, Cipher.getInstance("Blowfish") = ECB with zero IV, RSA/ECB/PKCS1Padding (v1.5, not OAEP), no MAC, and a hardcoded RSA-2048 public key in res/values/strings.xml:1237. Sinks: Google Drive drive.appdata backup + Eyecon backup server (a7.x:530,579; f5.f.AbstractC16422f:171,175), registration payload (RegistrationActivity:637, PhoneNumberValidationActivity:269), diagnostic stack traces (a7.s0:234). Impact: contacts + call logs in the backup are deterministically encrypted, unauthenticated and structurally leaking; a Bleichenbacher-style oracle on the server side would expose the symmetric session key, which would decrypt all stored backups.

F2 — The entire preference store, including the clientId credential, is plaintext SQLite — HIGHh5

Where: a7.b.C0041b (contactdb, v57) -> shared_preference_v2 (key, data_type, data_part, data); eo.b.m31713A; f7.y.f41007f. Detail: no SQLCipher, no EncryptedSharedPreferences, no key derivation. The clientId (join.jsp response body) is stored as a plain String row ('clientId', 0, 0, '<value>'). Same for clientToken (Ignite JWT), SP_KEY_app_instance_id, pref cipher_key (the photo-store salt) and the whole RC snapshot (SP_KEY_EYECON_REMOTE_CONFIG_JO_v2, FB_RC_*). Impact: root / ADB-backup / forensic access to contactdb yields the app’s only credential plus the entire RC cache.

F3 — clearApplicationUserData() and System.exit(0) on a prefs read failure — HIGHh5

Where: eo.b.m31717e (p274eo/C16303b.java:399-499). Detail: any throwable during shared_preference_v2 load -> m10632L (logging key and value) -> 1 s sleep -> 3 s sleep -> System.exit(0). SQLiteDatabaseCorruptException | SQLiteFullException -> ActivityManager.clearApplicationUserData(). Impact:

  1. Filling device storage (SQLITE_FULL) is enough to wipe all app data, which destroys the clientId and forces a full re-registration — a resource-pressure-triggered data-loss/DoS.
  2. A transient lock or partially-applied transaction self-DoSes the app on every launch.
  3. The intended sibling-process kill loop is dead code (pid == 0 && ... inverted comparison at :491), so multi-process state can diverge.
  4. Sensitive values are logged.
F4 — ObjectInputStream.readObject() on stored blobs — MEDIUMh5

Where: eo.b.m31718g (v1 shared_preference BLOB, p274eo/C16303b.java:532), eo.b.m31714C(byte[]) (:271-290), a4.d.m95a() (p005a4/RunnableC0032d.java:171-197, legacy main.eyecon.properties / secondary.eyecon.properties). Mitigating: android:allowBackup="false" (AndroidManifest.xml:266) removes the Android auto-backup delivery vector; the files are app-private. Residual: rooted devices, or any future switch of allowBackup, re-open the deserialization sink. No allow-list, no ObjectInputFilter.

F5 — 1000-value keyspace + Blowfish/ECB + ObjectInputStream on the contact-photo store — HIGHh5

Where: key derivation p379k5/RunnableC17750b:396-421; cipher field m7.d.C18517d.f46765a; consumption am/RunnableC0215t1:131,204 and p516r5/C19854e:898-963. Detail: key = "Ewu" + java.util.Random().nextInt(1000) + "sTQHGK", Blowfish ECB, no IV, no MAC, then ObjectInputStream(CipherInputStream(...)).readObject(). See §9.2. Impact: all cached contact photos are decryptable with <=1000 guesses by anyone with read access; and a planted file plus a known key gives control of the ObjectInputStream byte stream, which is a deserialization gadget sink in an APK that bundles a very large third-party SDK surface.

F6 — Weak AES key source: KeyGenerator("AES").init(256) fed into Blowfish — MEDIUMh5

Where: f7.f0.m32019n:355-358. Detail: the KDF/algorithm pairing is incoherent. Harmless today (Blowfish takes any key length) but a latent correctness bug; combined with F1’s ECB mode.

H1 — Privileged JavaScript bridge reachable from remote content — HIGHh5

Where: a7.t.m425J0 (p008a7/AbstractC0077t.java:216-223) registering AnalyticsWebInterface (f7.a), Eyecon (e7.d.C16132d), EyeconContest (f7.m), EyeconWebView (a7.o); plus m7.l.m34727f registering "Android" (m7.k.onBigLink). Detail: see §3.5 and §9.3. f7.a exposes all five RC getters + logEvent + setUserProperty to page JS; m7.k resolves arbitrary pending callbacks with JS-supplied data; a4.c case 16 injects <script src> / <img src> from response-derived URLs into a live WebView. Impact: a page served from any content channel the app renders (FIAM, dynamic area, in-app update, photo search, quotes) inherits analytics-write + RC-read capability.

H2 — Phone number transmitted to the backend on emulator detection — MEDIUMh5

Where: e5.m.m31485a (p260e5/RunnableC16111m.java:374-383) -> w6.h.m38436i("android-a001","error",{"cli": <number>}) -> p607vk/RunnableC21129z -> https://api.eyecon-app.com/app/eventreport.jsp. Detail: see §8.3.2. Automatic, environment-triggered transmission of a personal identifier; the same site also reports CLIENT_EMULATOR. Note: first-party destination, so this is a privacy/notice issue rather than third-party exfiltration.

H3 — Remote Config can rewrite the URL and the JS executed by the photo scraper — HIGHh5

Where: m7.l.m34727f (p420m7/C18533l.java:199-221), keys user_agent and google_photos_link_loader (d5.d:271-272). Detail: see §9.3. search_url, links_script and big_url_script are all RC values; the WebView has JS enabled, a spoofed desktop UA, and the "Android" bridge. Impact: anyone able to modify those two RC values can point the app at any origin and have it execute attacker-chosen JavaScript in a JS-enabled WebView with a privileged bridge. Combined with H1 this is the most powerful primitive in C1. Caveat: these keys live in the Firebase RC channel, not the cleartext one (§19.4).

H4 — print_logs_in_release enables production disk logging remotely — MEDIUM-HIGHh5

Where: y5.v (p672y5/C21867v.java:862) sets lo.a.f46357c; consumed by lo.a.m34588y (p410lo/C18416a.java), which appends to /data/data/com.eyecon.global/files/eye_log.txt via lo.a.m34565a (5 MB rotation, trimmed to 50 lines). Detail: m34588y(tag, msg, args...) is the formatted/verbose logging call used throughout the app — including the URLs, CIDs, response bodies and headers logged by w6.a (C21273a:151,170,206,260,385) and eo.b.m31717e. Impact: an RC operator (or anyone who can set FB_RC_print_logs_in_release) causes release builds to write detailed operational and personal data to a file inside app storage. Readable on root, or via the app’s own backup/share surface.

H5 — Hardcoded secrets compiled into the APK / resources — MEDIUMh5

Java (OBSERVED): w6.a.rc_key = "PgdtSBeR0MumR7fO" (p620w6/C21273a.java:42); f7.q emulator IMEI/IMSI/MSISDN tables; gf.k Play Store cert digests (public); d5.d ~350 RC defaults including uid2_config and the whole monetisation catalogue.

Resources (src/resources/res/values/strings.xml):

ResourceLineValue
pk_enc1237RSA-2048 X.509 SPKI, base64 (public key — not secret, but un-rotatable)
pk_enc_21238second RSA-2048 SPKI
cipher_key272"Ewu" — the Blowfish key prefix (see F5)
google_api_key770AIzaSyCD1Lmwj_ln-h4RMLHApAZjuPZNWAzJi3M
google_crash_reporting_api_key772same value
facebook_app_id5951716660955257637
gcm_defaultSenderId745820458390093
project_id1269eyecon-app
firebase_database_url719https://eyecon-app.firebaseio.com
google_storage_bucket774eyecon-app.appspot.com
default_web_client_id396820458390093-i5qaq32l80noq6it19dfuqummm685pbv.apps.googleusercontent.com
quotes_api_id1481e2eeb1aa9f32eb07fa04595a0c457ecb6fadb772 — used as Authorization: Token ... at f7.j0:102
applovin_key120UZvHsQBqsDCf0qv_yVDKn0UeTmRknnQSgTT3HP7HFikqs8IxdD-7uWdloUQVGTn09CQ0LThxLm6tfMnKDFS9xa
amazon_app_key107905c0238-13dd-4cf9-af3d-90f64672a4a9 — used at o4.AbstractC19054g:88
app_vestor_api_token117980d4e3f-d057-4e5b-9034-1587edadf5a4
app_vestor_app_key1188c830f45-9f86-48d3-bcca-4e14fe2c1129
crackle_key370fai=1:917722272265:android:af67d7515574ed8c689ed8~~~fak=AIzaSyBxXQfYa0khnAZ2-NiK3WQV-4JbMWCw6ls~~~fpi=eyecon---eyecon-caller-id...

Impact: the Firebase/AdMob/AppLovin/Amazon/Quotes keys are the “restricted but not secret” class; the operational risk is that they cannot be rotated without shipping a new APK, and any of them treated as a bearer token by a third party is effectively public.

A1 — Two independent emulator detectors, one of which gates remote config — MEDIUMh5

Where: a7.w0.m450h() (gates qo.r.m36274e0() -> d5.c fetch and d5.d Firebase reads) and e5.m.m31485a() + f7.q (blocks registration, reports CLIENT_EMULATOR, exfiltrates the CLI). Detail: see §3.4 and §8.3. Techniques include reflection into the hidden android.os.SystemProperties.get API (15 props, threshold >=5 absent), a /system/bin/netcfg subprocess, /proc/tty/drivers and /proc/cpuinfo reads, installed-package probes for Genymotion/BlueStacks/Nox, and telephony IMSI/IMEI/MSISDN matches. Analyst/consumer impact: on an emulator the app silently serves only the compiled-in configuration and skips the network config fetch — so dynamic analysis misses the real behaviour entirely. On a device the registration flow is blocked and the number is reported.

A2 — No certificate pinning anywhere in the app — MEDIUMh5

Where: oq.p (CertificatePinner) ships but is never used (zero call sites outside okhttp3 internals); w6.a.m38417a builds new C19227c0(new C19225b0()) — a default OkHttpClient; w6.a.m38423g/m38424h/m38425i use platform HttpURLConnection/HttpsURLConnection. Combined with network_security_config.xml’s app-wide cleartextTrafficPermitted="true", the entire first-party API surface (api.eyecon-app.com, apps.eyecon-app.com, cards.eyecon-app.com, …) is protected only by the platform trust store.

N1 — App-wide cleartext permission — HIGHh5

Where: AndroidManifest.xml:272 android:usesCleartextTraffic="true"; res/xml/network_security_config.xml <base-config cleartextTrafficPermitted="true">. Detail: permits plaintext HTTP to any domain, which is what makes the fetch in §19.5 row 1 exploitable. The <debug-overrides> block trusting user-installed CAs is inert here because android:debuggable is not set — confirmed by exhaustive search.

N2 — Desktop-Chrome User-Agent spoofing — LOWh5

Where: RC key user_agent default (d5.d:271), applied at m7.l:210; also a hardcoded desktop UA inside w6.a.m38423g:172, m38424h:247, m38425i:339 (Chrome/87.0.4280.88). Detail: an Android app presenting a Windows/Chrome UA to Google’s image search is a ToS/anti-abuse red flag and an attribution-integrity problem for the scraped content.

N3 — The cleartext RC fetch carries no auth and no integrity protection — HIGHh5

a4.c case 17 (C1’s own code) chooses the http:// scheme and passes addAuth=false, so the A/B document is fetched anonymously over plaintext and merged unverified into the live config. See §19.5 row 1 and §19.6. Called out separately because a future RC change could point the fetch at any URL, and because the merge result is persisted (SP_KEY_EYECON_REMOTE_CONFIG_JO_v2) and therefore sticky.

D1 — f7.y.m32084f() silent 3-second readiness timeout — LOWh5

Every get* can silently return the caller’s default if the async load has not finished. Combined with w6.a’s throw new RuntimeException("Error missing CID"), a slow contactdb load can push the app onto error paths.

D2 — eo.b.m31715b silently drops non-String/Number/Boolean values — LOWh5

Any Set<String> written via SharedPreferences.Editor.putStringSet is lost on the next full-table rewrite. No such usage found inside C1.

D3 — Inverted PID comparison makes the cross-process cleanup dead code — INFOh5

eo.b.m31717e:490-495.

D4 — Hardcoded client version string — INFOh5

w6.h.m38428a sends apsversion = "vc_749_vn_4.2026.04.23.1145_a" to addcontacts.jsp and getcontactslang.jsp. Used for server-side version gating; consistent with n4.a’s build timestamp 1776946263760L (2026-04-23).

P1 — Correct behaviours worth recording (to avoid false positives)h5
  • a7.p (WebViewClient) onReceivedSslError does not call handler.proceed() — TLS errors are correctly fatal (p008a7/C0069p.java:174-176).
  • gf.k correctly verifies the Play Store signing-certificate digest before binding.
  • ri.m.m36727g (p526ri/C19932m.java:488-503) implements AES/GCM/NoPadding with a 12-byte SecureRandom nonce and a 128-bit tag — a properly implemented AEAD. It is the KMM/iOS shared-crypto bridge and is unrelated to the Blowfish work in F1/F5.
  • f7.j0 sets LocaleList.setDefault explicitly (API 24+) and uses createConfigurationContext — no global-locale leak on modern devices.
  • f7.y’s getStringSet returns a defensive copy.
  • x6’s three transports all check AbstractC9039h.m10665v() (clientId present) before issuing a request, and catch IOException | OutOfMemoryError rather than crashing.

22. Corrections and additions to the existing documentsh4

Doc / sectionStatementCorrection
BACKEND-API.md §3.3”Whether circus_key is also settable over cleartext depends on a server-side allow-list that static analysis cannot see.”Wrong premise. The cleartext channel feeds d5.c (C15803c); d5.d.m31091l — the only reader of circus_key — never consults d5.c. There is no allow-list to infer: the two stores are disjoint. See §19.4 for the resolution-order proof.
BACKEND-API.md §2”m38417a (Ktor path)“It is OkHttp 4.x (oq.c0/oq.b0), instrumented with FirebasePerfOkHttpClient. See §1 row 3.
BACKEND-API.md §9 row 1”…the response is the config map that contains circus_key”The response contains the A/B document (tests[] -> calculated_tests -> winner_variant), which is merged into d5.c.f38506b — not the typed config map that circus_key is read from. See §19.3 and §19.6.
BACKEND-API.md §3.5401 path described as returning 700Confirmed: TypedValues.TransitionType.TYPE_DURATION decompiles to 700 (C21273a.java:134). No change.
DATA-LAYER.md §2.19shared_preference_v2 schemaConfirmed verbatim from a7.b.m153q(). Newly documented: the 100 KiB chunking with UTF-8 continuation-byte backoff (eo.b.m31713A) and the 0..5 type codes.
DATA-LAYER.md §5.1”MICO does not use Android SharedPreferences for its own state”Correct for current state, but incomplete: the legacy default SharedPreferences file is still read and migrated (a4.d.m95a()), gated by the OLD_VERSION_COPY_TO_NEW flag, and clientId is specifically required in that map for migration to trigger. See §7.2.1.
DATA-LAYER.md (absent)—New: eo.b.m31717e calls clearApplicationUserData() on SQLITE_FULL/SQLiteDatabaseCorruptException and System.exit(0) on any read failure. See F3.
DATA-LAYER.md (absent)—New: eo.b.m31715b drops non-scalar values. See D2.
DATA-LAYER.md (absent)—New: table_api_requests rows are full serialised requests including urlStr, requestJsonBody, endPoint and headerValuesMap, with typeRequest 1=JSON / 2=gzip / 3=general. See §6.
MANIFEST-AND-SECURITY.md (assumed)cleartext postureConfirmed and expanded: full res/xml/network_security_config.xml content; android:debuggable not set so <debug-overrides> user-CA trust is inert. See N1.

23. Coverage statementh4

  • Packages documented: 16 / 16 (a7, f7, e5, m7, i7, y7, eo, x6, a4, g7, gf, l, oq, qo, pq, n4)
  • Files documented: 350 / 350 (every .java in every listed directory was opened; class identity was recovered from /* JADX INFO: renamed from: */ in all 350)
  • Classes given detailed treatment (full method signatures / constants / findings): 49 — a7.a, a7.b, a7.k0, a7.w0, a7.t, a7.x, a7.p0, a7.r0, a7.n0, a7.b0, a7.d0, a7.l0, a7.p, a7.u0, a7.v0, a7.s0, a7.q0; f7.y, f7.x, f7.a0, f7.a, f7.q, f7.f0, f7.g0, f7.n0, f7.d0, f7.c0, f7.j0, f7.m, f7.n, f7.w, f7.z, f7.r, f7.s, f7.p0, f7.i0, f7.e; eo.b, eo.a, eo.c, eo.d; x6.c, x6.b, x6.d, x6.e, x6.f; a4.c, a4.d, a4.a, a4.e, a4.f, a4.g; e5.m, e5.d, e5.f, e5.e, e5.y, e5.x; m7.d, m7.l, m7.k, m7.m, m7.s, m7.p, m7.h; y7.l, y7.o, y7.i, y7.j; gf.c, gf.d, gf.k, gf.j; g7.a; oq.r; l.g; n4.a
  • Remaining classes (~250, mostly the OkHttp / Kotlin-stdlib / Glide relocated libraries and the synthetic lambda holders) are covered by per-package inventory tables that give, for each of the 350 files: real name, kind, supertypes and key members.
  • Out-of-scope classes read in order to establish the facts in this document (each cited inline): p620w6/C21273a.java + C21280h.java (w6.a, w6.h), p032b5/AbstractC3464e.java (b5.e), p236d5/AbstractC15804d.java + C15803c.java (d5.d, d5.c), p607vk/RunnableC21129z.java, p349ij/C17307b.java, p379k5/RunnableC17750b.java, p516r5/C19854e.java, p282f5/AbstractC16422f.java, p410lo/C18416a.java, p526ri/C19932m.java, p672y5/C21867v.java, com/eyecon/global/Others/MyApplication.java, com/facebook/appevents/AbstractC9039h.java, plus src/resources/AndroidManifest.xml, res/values/strings.xml, res/xml/network_security_config.xml.

End of Cluster C1 — core platform.

Cluster C2 — Registration, Identity, Billingh3

App: com.eyecon.global (MICO / Eyecon — caller ID, spam blocker, premium subscription) Version: 4.2026.04.23.1145 (versionCode 749) Source tree: C:\Users\btc_e\Downloads\mico-analysis\src\sources Scope: 50 packages, 962 .java files, ~6.0 MB Owning modules: Registration, Billing, PhoneAccount, IdPlus


0. How to read this documenth4

  • OBSERVED = directly readable in the decompiled source (class declaration, method body, string literal, JADX renamed from comment).
  • INFERRED = a conclusion drawn from OBSERVED facts plus Android/Play semantics; the reasoning is spelled out so it can be checked.
  • Real package/class names are taken from the /* JADX INFO: renamed from: x.y */ comments. On disk the obfuscated packages appear under pNNN<name> directories (e.g. p494q7 = q7, p365jd = jd).
  • Decompiler caveats: several methods are emitted with throw null; or JADX WARN comments where control flow could not be reconstructed; those are called out individually.
0.1 Package index (scope, as briefed)h5
DISK_DIRREAL_PKGFILESKBOWNING_MODULE
asas1291164Billing
bmbm156688Registration
p237d6d649359Billing
p672y5y526347Registration
p252dsds22316IdPlus
p035b8b819271Registration
p365jdjd52268Registration
p494q7q758204Registration
p515r4r416187IdPlus
p244dfdf25182IdPlus
p489q1q130173Registration
p034b7b713171Billing
p047c7c725140Billing
p418m5m516134Registration
p358j5j529122Billing
p285f8f814118Registration
p607vkvk40115Registration
p640xx30107Billing
p314h6h61687IdPlus
p448o4o42281IdPlus
p048c8c8579IdPlus
p263e8e81177IdPlus
p621w7w71477Billing
p303g5g51269Billing
p261e6e6464Billing
p045c5c51162Billing
p014ajaj960Registration
p553t4t41653Billing
p410lolo145Billing
p325hhhh243Billing
p397l5l5741Registration
p491q4q4632IdPlus
p518r7r7830Registration
p398l6l6730Registration
p685yjyj127Billing
p321hdhd524PhoneAccount
p313h5h5622IdPlus
p339i6i6522IdPlus
p671y4y4213Registration
p262e7e7613Billing
p040c0c0913Billing
p006a5a5111Billing
p700z4z4310Registration
p419m6m659Registration
p376k0k049Billing
p050cece35Registration
p381k7k735PhoneAccount
p359j6j634IdPlus
p527rjrj52Billing
p254ee11Registration

1. Package inventory, one-line responsibility, and generated-vs-handwritten verdicth4

(Completed below, package by package. gen? column answers the brief’s explicit question for n4, y5, zd.)

Disk dirReal pkgKindResponsibility
p494q7q7appRegistration controller/presenter layer: join/rejoin callbacks, phone-number validation state machine, error enums, onboarding UI logic
p365jdjdappjoin.jsp HTTP client + join request/response models
p516r5r5app (not in C2 brief list but referenced)cli_auth / checkcode CLI-registration client
p489q1q1appPlay Billing service proxy: BillingClient wrapper, BillingServiceConnection, purchase flow, ack/consume, zz* protobuf JSON codecs
p303g5g5appPlay Billing engine/manager: product + offer loading, price formatting, purchase orchestration, entitlement cache
p358j5j5appPremium entitlement model (PaidPremium, FreePremium) and premium Activity/Fragment UI logic
p621w7w7appPremium purchase dialog / price / plan selection UI
p045c5c5appPremium bottom-sheet / dialog UI logic
p040c0c0appSmall UI helpers used by x/k0 (Lottie/anim dispatch)
p376k0k0appLottie animation wrapper used by x
p262e7e7appEyecon JavaScript bridge object + tiny store-support types
p006a5a5appSingle-class prefs/analytics facade
p640xxappLottie/animation player orchestration (Choreographer frame loop, thread pool)
p685yjyjappOne large class: billing-callback/subscription glue (out of Billing TSV row but present)
p410loloappOne 45 KB class: lo.a — coroutine/logging/analytics helper referenced almost everywhere
p325hhhhappTwo classes: media/exo event glue
p048c8c8appc8.e abstract Activity base (IdPlus screens)
p314h6h6appIdPlus Notification-Reader UI
p313h5h5appIdPlus Notification-Reader premium gate UI
p339i6i6appIdPlus Telegram/notification small screens
p359j6j6appTelegram window click wiring
p448o4o4appIdPlus profile/account UI + InterfaceC19056i (advertising-ID provider used by Eyecon.getAdvertisingId)
p515r4r4appGoogle AdMob native/mediation ad loader used by IdPlus screens
p263e8e8appFacebook App Events / Graph JSON helpers
p252dsdsappIdPlus ad mediation orchestration
p244dfdfappGeneric “bottom sheet + retry dialog” framework (used by IdPlus screens)
p047c7c7appError/retry dialog framework (c7.t, c7.j) + base AlertDialog, also c7.l = C3888t paywall-ish dialog
p034b7b7appBase Activity (b7.e AbstractActivityC3498e) — all app Activities extend it
p553t4t4appAdMob preload / manager-ad wrapper
p321hdhdappExoPlayer Metadata.Output implementation (SCTE-35 splice commands) — PhoneAccount module row but actually generic media glue
p381k7k7appTiny helpers over gf.c
p285f8f8appFacebook-login helper (LoginManager wiring)
p607vkvkappInMobi/analytics event-dispatch runnables (mostly SDK glue)
p418m5m5appShared “phone number / line” helpers + gson DTOs
p397l5l5appAnalytics/identity bootstrap runnables
p398l6l6appOnboarding-ad / interstitial glue
p518r7r7appOnboarding UI (sliding page) + FakeCallActivity support
p671y4y4appSingular SDK glue runnables
p700z4z4appSingular/AppsFlyer-adjacent event runnables
p419m6m6appSmall click handlers / search-UI helpers
p671y4/p254esee aboveapp—
p254eeappSingle tiny abstract class
p527rjrjapp5 marker interfaces only
p050ceceappGoogle Identity (com.google.android.libraries.identity.googleid) credential-wrapper helpers — Google sign-in for join
p014ajajappApache-HTTP based aj.* — install-referrer / Play-Store referrer HTTP fetch? (verified below)
p035b8b8appGiant generated-style view-binding/form-building class b8.b (155 KB) — see §2 verdict
p237d6d6appStore/paywall WebView UI (store screens)
p672y5y5appHuge single classes y5.k (146 KB), y5.f (101 KB), y5.v (43 KB) — see §2 verdict
asas3rd-partykotlinx.coroutines internals
bmbm3rd-partyOkHttp internals

2. Verdict on the three “large package” questionsh4

The briefing asked explicitly whether n4, y5 and zd are generated or hand-written. (n4 and zd are not in the C2 package list at all — they were bundled into the question because they are the biggest obfuscated packages in the APK.)

2.1 p429n4 = n4 — 100 % GENERATED. No app logic.h5
  • OBSERVED: p429n4\AbstractC18701d (renamed from: n4.d) is 253 KB and consists of nothing but public static final int <Name> = <number>; declarations — e.g. ActionBar_layout_android_layout_gravity = 0, ActionBar_background = 0, ActionBar_backgroundSplit = 1, … It imports exactly one thing: com.eyecon.global.R.
  • OBSERVED: n4.a (C18698a) holds a single constant Date f47377a = new Date(1776946263760L).
  • OBSERVED: n4.b (C18699b) holds a SparseArray with one entry put(0, "_all").
  • VERDICT: this is the Android Gradle Plugin generated resource-ID table (the obfuscated twin of com.eyecon.global.R) plus two R-internal helper classes. 248 KB / 4 files of no behaviour. It should not be attributed to Registration/Billing/IdPlus at all; the C1–C6 briefs’ module attribution that lists n4 under a feature cluster is a classification error.
2.2 p708zd = zd — THIRD-PARTY LIBRARY (ExoPlayer). No app logic.h5
  • OBSERVED: zd.f (C22159f, 313 KB) imports android.media.MediaCodec, MediaCrypto, MediaFormat, Surface, com.google.android.exoplayer2.ExoPlaybackException, android.opengl.* — the signature import set of ExoPlayer’s ExoPlayerInternal.
  • OBSERVED: zd.g (HandlerThreadC22160g) builds EGL14 / EGLDisplay / EGLContext / EGLConfig / GLES20 / SurfaceTexture — GLSurfaceView-style GL thread for video rendering.
  • OBSERVED: zd.n (C22167n) uses DisplayManager, Surface, WindowManager — androidx.media3.ui PlayerView-adjacent window logic.
  • VERDICT: bundled ExoPlayer / androidx.media3. Zero MICO code. Should not be in a feature cluster.
2.3 p672y5 = y5 — HAND-WRITTEN APP LOGIC. Genuinely enormous, monolithic, not generated.h5

This is the one the briefing was really asking about, and it is in the C2 scope (26 files / 347 KB, module Registration).

  • y5.k (C21856k, 146 KB) — imports android.database.sqlite.SQLiteDatabase, Cursor, com.eyecon.global.Contacts.Database.DBContacts, com.eyecon.global.DynamicArea.CardsException, CallStateService. It is the Dynamic-Area (“cards”) engine + local SQLite access layer: hand-written imperative code that reads/writes DBContacts and builds card models.
  • y5.f (C21851f, 101 KB) — implements InterfaceC16693h, …, CrackleUserRewardListener, OnCompleteListener, InstallReferrerStateListener? (no, that is y5.v), …. A god-object that is the app’s media/utility/analytics dispatcher: AssetManager, MediaMetadataRetriever, ContentValues, SharedPreferences, RectF. It is also the class used as the billing / Crackle callback (new C21851f(23) is passed to the Play BillingClient.Builder in g5.i.m32536p), so it doubles as a general purpose “shared worker” object.
  • y5.v (C21867v, 43 KB) — implements …, InstallReferrerStateListener, MediaViewListener, …: install-referrer + media-view orchestration, i.e. it is the class the InstallReferrerReceiver flow talks to.
  • VERDICT: hand-written app code, not machine-generated. The size comes from god-object design (one class holds an entire subsystem), not from codegen. y5.k, y5.f, y5.v should be treated as prime decompilation targets for the Dynamic-Area and install-referrer flows.
2.4 Bonus verdict: p035b8 = b8 — HAND-WRITTEN, but ViewBinding-style glueh5
  • b8.b (C3508b, 155 KB) implements ViewBinding and builds an entire form UI programmatically (LayoutInflater, LottieAnimationView, EyeButton, RoundedCornersFrameLayout, CustomTextView, EyeAvatar, EyeEditText). It is a hand-written “mega form” builder — real app logic (register / paywall forms), just extremely repetitive. Not generated.
  • b8.g (C3513g) implements ViewBinding, InterfaceC20338h — same pattern.
  • b8.p (C3522p) uses ViewPager, CardView, Group — a page/card layout binder.
2.5 Bonus verdict: as and bm — THIRD-PARTY, misfiledh5
  • as = kotlinx.coroutines internals (as.a … as.z3, 129 files / 1.16 MB). Proof: as.a extends wo.c, and p636wo\wo.c is a CancellableContinuation (field intercepted, ctor (InterfaceC20763i, InterfaceC20758d)); as.s imports kotlinx.serialization.KSerializer, kotlin.jvm.internal.AbstractC17892p. Tagged “Billing” by the TSV purely because billing code uses it.
  • bm = OkHttp internals (bm.b = RealInterceptorChain; bm.h2/m4/p4 = Dispatcher executors; bm.a0 = an Okio BufferedSink — imports java.nio.ByteBuffer, InvalidMarkException, ArrayDeque). Tagged “Registration” for the same reason.
  • Together as + bm = 1.85 MB of the 6.0 MB C2 budget (31 %) and contain zero MICO logic.

3. The credential chain: what clientId really ish4

3.1 e-auth header construction — OBSERVEDh5

p620w6\C21273a (renamed from: w6.a, TAG "NetworkHelper") is the single HTTP client used for every Eyecon backend call. Two code paths add auth headers identically:

// C21273a.m38421e(String) — "e-auth-c"
int numericValue = 0;
for (String str2 : str.split("-")) {
char[] charArray = str2.toCharArray();
if (charArray.length >= 1) numericValue += Character.getNumericValue(charArray[0]);
}
return String.valueOf(numericValue);
// C21273a.m38427k(HttpURLConnection) (and C21273a.m38417a for the OkHttp-style path)
if (string.startsWith("https://api.eyecon-app.com/app/") || string.contains("eyecon-app.com")) {
String strM10662s = AbstractC9039h.m10662s(); // prefs["clientId"]
if (isEmpty(strM10662s)) throw new RuntimeException("Error missing CID");
conn.setRequestProperty("e-auth-v", "e1");
conn.setRequestProperty("e-auth", strM10662s);
conn.setRequestProperty("e-auth-c", m38421e(strM10662s));
conn.setRequestProperty("e-auth-k", rc_key);
}
3.2 rc_key is a hard-coded shared secret — HIGHh5
  • OBSERVED: private static final String rc_key = "PgdtSBeR0MumR7fO"; — a 16-char literal compiled into classes3.dex in w6.a. It is sent verbatim as the e-auth-k header on every Eyecon API request.
  • INFERRED: this is a static client-side key (“key” suffix). It is not derived from the device and is identical for every install of every build, so it provides no per-client authentication — it is obfuscation, not a secret. Anyone who unzips the APK can read it.
3.3 e-auth-c is a trivially forgeable checksum — MEDIUMh5
  • OBSERVED: the “checksum” is the sum of the numeric value of the first character of each dash-separated segment of the clientId (see 3.1). For a clientId like a1b-2c3-4d5 it is 10 + 2 + 4 = 16.
  • INFERRED: this is a corruption/truncation check, not a MAC. It carries no secrecy and no authentication strength. It does confirm that clientId is expected to be dash-delimited with segment-initial digits/characters.
3.4 clientId = the entire raw join.jsp response body — CONFIRMED, briefing is correcth5
  • OBSERVED: com\facebook\appevents\AbstractC9039h.m10662s() is literally return (String) AbstractC17503r.m33607k("clientId", ""); and m10665v() is return !isEmpty((String) SharedPreferencesC16497y.m32082b(null, "clientId"));
  • OBSERVED: in p365jd\RunnableC17614z (renamed from: jd.z) case 1, after a successful join.jsp GET the code does: c19673l0.m31518o(strM38423g); where strM38423g is the raw HTTP response body string.
  • OBSERVED: AbstractC16130b.m31518o(Object) → C16484n0.m32065f(Object) → map.put("DEFAULT_RESULT", obj); and C16484n0.m32061b() is map.get("DEFAULT_RESULT").
  • OBSERVED: q7.l0.mo309m() case 4 then does String str6 = (String) m32061b(); and editor.m32076c(str6, "clientId").
  • Therefore: clientId is the complete join.jsp response body, stored verbatim. The same body is separately JSON-parsed into a HashMap (keys pi, sd, is_new) which is used for control flow, but it is the unparsed string that becomes the bearer credential. There is no field extraction.
  • CORRECTION TO ANY PRIOR CLAIM: nothing here extracts a cid/token JSON field. A statement such as “clientId is the cid field of the join response” is wrong; it is the whole payload.
3.5 Storage — CONFIRMEDh5
  • OBSERVED: writes go through MyApplication.m10348j() (p284f7\SharedPreferencesC16497y) and MyApplication.m10347i() (a fresh SharedPreferencesEditorC16496x).
  • The app’s custom prefs table is shared_preference_v2 in the contactdb SQLite DB (see C1/C4 data layer). Reads therefore go through SharedPreferencesC16497y.m32082b(...) rather than through a regular SharedPreferences file, which is why the getBooleanSP/getIntSP/getStringSP JS bridge methods (§5) can read arbitrary app keys from arbitrary web content.

4. Flow 1 — Account creation: onboarding → SMS/CLI validation → join.jsp → clientIdh4

4.1 Entry points (OBSERVED)h5
Wrapper classNotes
com\eyecon\global\Registration\RegistrationActivity210 KB. The main 7-child ViewFlipper (f12914H) registration screen: country, number, code, social login, permissions, etc.
com\eyecon\global\Registration\PhoneNumberValidationActivity83 KB. Second-number / line validation screen (separate ViewFlipper f12854U.f2863x).
com\eyecon\global\Registration\Onboarding\OnboardingActivity8 KB. Pre-registration onboarding.
com\eyecon\global\Registration\Onboarding\FakeCallActivity15 KB. Simulated incoming-call onboarding.
com\eyecon\global\Registration\SMSRetrieverBroadcast1.1 KB — SMS Retriever API receiver.
com\eyecon\global\Registration\InstallReferrerReceiver7.6 KB.
com\eyecon\global\Registration\RegisterNotificationWork1.7 KB — androidx.work.WorkManager worker.
com\eyecon\global\Registration\PreparingContactView19 KB. Contact-scan progress view.
4.2 The callback-object contract (OBSERVED)h5

Both RegistrationActivity and PhoneNumberValidationActivity drive the backend through p262e7\AbstractC16130b (renamed from: e7.b), a tiny generic “async result holder”:

  • fields: Handler c, boolean d, boolean e;
  • Object m32061b() → map.get("DEFAULT_RESULT") (inherited from f7.n0);
  • void m32065f(Object) → map.put("DEFAULT_RESULT", obj), re-exposed as m31518o(Object);
  • void m32066g(Object,String) → map.put(key,obj);
  • Object m32063d(Object def, String key) / m32062c(Serializable def) → defaulted lookups;
  • terminal dispatchers: m31515h() = onError (mo308k() then mo415j(false)); m31516i() = onSuccess (mo31100l() then mo309m() then mo415j(true)).
  • The synthetic subclass e7.b(boolean, long) installs a Handler that fires m31515h() after a timeout.

So every registration sub-flow is one anonymous switch-on-a-constant callback object.

4.3 Step-by-step — CLI (phone-number) registrationh5
  1. q7.i1 (C19665i1) is the per-Activity registration state holder. Fields: AppCompatActivity a, int b = 30000 (default timeout ms), String c = "", String d (cli), String e (last error), Enum q7.h1 f50630g (transport), String h = null (svr value), boolean i.

    • m36103b(int) maps a numeric error code onto the svr string: -9999 → null, -999 → "2", 0 → "1", -1 → "3", < -1 → "3", > 0 → "1".
    • m36102a(String regId) enqueues new RunnableC17614z(cli, regId, isAlreadySocialAuth, transport, svr, new C19647c1(this, 1)) on the shared “Server” executor C21280h.f56470f.f56472a.
  2. jd.z (RunnableC17614z) — default branch = cli_auth/gettransport. OBSERVED request (GET https://api.eyecon-app.com/app/cli_auth/gettransport): apsversion=vc_749_vn_4.2026.04.23.1145_a, cli=<formatted cli>, reg_id=<regId>, is_already_social_auth=<bool>, tr=<transport> (e.g. mo_sms,so_sms, so_call,so_flash,huawei,fb,social_login,auto), optional last_error=<err>. Response JSON fields: error_status (int), subcode (0..3), tr (transport), details{ send_to, timeout, body, code } or details{ timeout, pattern } for so_flash. subcode mapping: 0 → SERVER_ALL_RETRIES_FAILED, 1 → SERVER_COUNTRY_NOT_SUPPORTED, 2 → SERVER_DELETE_ME_OVER_LIMIT, 3 → SERVER_BANNED_BY_TRAFFIC_ANALYZER. Error/verb keys pushed into the callback map: respond_code, V_ERROR_STATUS, V_CLI, V_TIMEOUT, V_SMS_MESSAGE, V_CODE, no_internet. Note: the same RunnableC17614z class is reused with a different constructor for the network-failure path of jd.b0 (C17566b0).

  3. r5.s (RunnableC19875s) case 1 — cli_auth/checkcode. GET https://api.eyecon-app.com/app/cli_auth/checkcode with cli=<formatted>, code=<user code>, apsversion=vc_749_vn_4.2026.04.23.1145_a, tr=<transport>, is_already_social_auth=<bool>. Verb EnumC19659g1.IS_CODE_VALID. On NumberFormatException of the body it reports -1. Success condition: Integer.parseInt(body) == 0 && httpCode == 200. Retry: one retry after AbstractC0077t.m439X0(5000L) (5 s backoff) if the first call throws. On success it sets C16489q.f40981n.f40984c = true and runs new RunnableC16111m(13, c16489q, new C16770d(25, cli, callback)) — p308gj.C16770d is the “code accepted → now join” trampoline (it is what supplies the svr= value used in step 4).

  4. jd.z case 1 — join.jsp. (This is the join itself; see 4.4.)

  5. Success landing — q7.l0 (C19673l0). mo309m() case 4 (join success). OBSERVED, in order:

    • boolean isJoin = ((Boolean) map.get("isJoin")).booleanValue(); (isJoin comes from the join response’s is_new field: "1" → true).
    • RegistrationActivity.m10511a0(activity, "success"); C21845b.f58845b.m39065f("onJoinSuccess"); C22071c.f60175e.m39334e("onJoinSuccess", null); EyeconAnalyticsWorker.m9988b(true).
    • AbstractC3464e.m3658i("New join") when isJoin.
    • Event: RunnableC0031c(10, "JoinType", false, isJoin ? "Join" : "Rejoin") — Join vs Rejoin.
    • Guard if (!AbstractC9039h.m10665v()) — i.e. only if we do NOT already have a clientId:
      String method = MyApplication.m10348j().getBoolean("SP_KEY_NOT_REGISTERED_WITH_REAL_CLI_V2", false)
      ? ( registrationActivity.f12921K0.f50536l == -1 ? "Not set"
      : registrationActivity.f12921K0.f50536l == 1 ? "Google" : "Facebook" )
      : "Phone number";
      String deviceName = AbstractC0062l0.m357l1();
      String osName = "Android " + Build.VERSION.RELEASE + " ; " + Build.VERSION.SDK_INT;
      editor.m32076c(str6, "clientId"); // str6 == (String) m32061b() == raw join body
      editor.m32076c(method, "SP_KEY_VALIDATION_WITH");
      editor.m32076c(deviceName, "deviceName");
      editor.m32076c(osName, "osName");
      editor.m32080g(System.currentTimeMillis(), "sp_join_time");
      editor.m32076c("4.2026.04.23.1145", "SP_JOIN_VERSION");
      editor.m32078e(749, "SP_JOIN_VERSION_CODE");
      editor.m32077d(!isJoin, "SP_KEY_IS_REJOIN");
      editor.m32074a(null);
    • CallStateService.m10184H(activity); transport-specific analytics ("Reg MO joined" / "Reg SO joined" / "Reg SO flash joined"); "Registration joined".
    • AbstractC0060k0.m329s(); MorningAdRefreshJob.m9917b(); C19845p.f51102c.m36570a();
    • UI re-layout when AbstractC9039h.m10667x() (has clientId).
    • WorkManager.getInstance().cancelAllWorkByTag("notificationWork").
    • Rejoin-only: RunnableC21276d(new C3463d(10), 4) on the server executor (re-sync).
    • C17487c.m33583h(new C19678n0(0, activity, isJoin)) → free-premium notification.
    • If not join (i.e. rejoin): C16464d0.f40866c.m31984b(new C3855c(activity, elapsedRealtime(), 1)) (pull mysettings from the server). If join: m10549j0(new RunnableC19643b0(activity, 11)).
    • activity.m10557u0(); if !f12947X0 then m10560x0().
  6. Success landing — q7.n (C19677n). mo309m() case 2 (phone-number validation success, in PhoneNumberValidationActivity). OBSERVED:

    String str3 = (String) m32061b();
    activity.f12884y0 = true;
    editor.m32076c(str3, "clientId");
    editor.m32077d("SP_KEY_NOT_REGISTERED_WITH_REAL_CLI_V2", false);
    editor.m32074a(null);
    if (f50630g == EnumC19662h1.SO_FLASH) { ...updateViralStatus(SO_FLASH)... }
    AbstractC0077t.m416A0(9, null);
    WorkManager.getInstance().cancelAllWorkByTag("notificationWork");
    C17487c...; C21295o.m38470l(...);
    activity.m10468E0(new RunnableC19672l(activity, 2));

    This confirms the briefing exactly: both write sites are in q7, both obtain the CID from m32061b(), and the second additionally clears SP_KEY_NOT_REGISTERED_WITH_REAL_CLI_V2.

4.4 join.jsp request/response — OBSERVED in fullh5

From jd.z case 1:

  • URL = C21280h.f56467c + "?" + C18416a.m34580q(params) + "&cv=vc_749_vn_4.2026.04.23.1145_a" where C21280h.f56467c = AbstractC1574a.m1345l("https://api.eyecon-app.com/app/", "join.jsp") = https://api.eyecon-app.com/app/join.jsp. Method GET.
  • Query parameters (in order):
    keyvalue
    cliC18312b.m34437h().m34444e(rawCli) (formatted E.164-ish) — skipped if isAlreadySocialAuth
    usernameliteral New_User_Eyecon
    devicenameAbstractC0062l0.m357l1()
    user_langAbstractC0062l0.m356k1()
    os"Android <RELEASE> ; <SDK_INT>"
    public_idAbstractC9039h.m10625E()
    adv_idInterfaceC19056i.f48663a.m35361a() (advertising ID, "" if null)
    mcAbstractC0042b0.m175X()
    viral_idprefs SP_KEY_INVITER_ID
    cli_cccountry code
    svrC19644b1.f50539o.toString() — only when already social-auth’d
    trtransport enum string (mo_sms/so_sms/so_call/so_flash/huawei/fb/social_login/auto)
    generated_cliprefs SP_REJOIN_OLD_GENERATED_CLI if non-null
    imeiC18502p.f46692k.m34691f()
    os_idAbstractC0042b0.m172U()
    apsversionAbstractC0039a.f201a
  • Pre-flight validation: if formatted cli or devicename is empty → CLIENT_FORMATTED_CLI_EMPTY / CLIENT_FORMATTED_DEVICE_NAME_EMPTY, m31515h() (error path).
  • Response handling:
    • HTTP != 200 → C21280h.m38437j(...) then m31515h().
    • HTTP 200 but empty body → JOIN_RESPOND_CODE_200_WITHOUT_CID (-16), error.
    • HTTP 200: map is filled by C21273a.m38423g(...); keys used: pi (new public id), sd ("0" ⇒ isDeviceChanged), is_new ("1" ⇒ isJoin).
    • pi empty → throw new Exception("Join, missing public id") (logged, isJoin forced false).
    • pi present → MyApplication.m10347i().m32076c(pi, "SP_UUID") + RunnableC0070p0(6) event.
    • Then c19673l0.m31518o(body); m32066g(isJoin,"isJoin"); m32066g(isDeviceChanged,"isDeviceChanged"); m31516i() → success.
  • AbstractC16257t1.m31616A(isJoin, c19644b1, alreadySocialAuth, false) is called before dispatch — the anti-fraud / emulator telemetry hook.
4.5 SMS Retriever API — OBSERVEDh5
  • com\eyecon\global\Registration\SMSRetrieverBroadcast (1,128 bytes) is the BroadcastReceiver. It is the Play-Services SMS Retriever path that auto-reads the server-sent MO SMS code (details{ send_to, timeout, body, code } from cli_auth/gettransport) instead of asking the user.
  • The two prefs that gate the flow are written in the failure branches: sendSmsEnded = true and isSmsValidEnded = true (q7.l0.mo308k() case 4, q7.n.mo308k() case 2). In q7.l0.mo309m() case 3 and q7.n.mo309m() case 1 the app reads isSmsValidEnded; if already true it re-enters the error branch instead of the success branch — i.e. a single incorrect code attempt permanently latches the flow into the error path for that screen.
  • ERROR CODE GATE (OBSERVED, q7.l0.mo308k() case 3): if isEmulator is true or mo36098a() (the q7.e1 code) is 3, 4 or 7, the app calls RegistrationActivity.m10506B0() + m10550k0() (hard reset) instead of letting the user retry. Codes 3/4/6/7/8 are SERVER_ERR_CLI_IS_NOT_VALID, SERVER_ERR_MANUAL_AUTH, SERVER_ERR_UNSUPPORTED_FLOW per q7.e1; the same triple gate exists in q7.n.mo308k() case 1.
4.6 Social (Google / Facebook) registration path — OBSERVEDh5
  • p494q7\EnumC19659g1 (q7.g1) contains FACEBOOK_LOGIN(12) and GOOGLE_LOGIN(12) — the two constants share the value 12. Any switch on q7.g1 cannot distinguish Google from Facebook; both hit the same arm. (This is an obfuscation artefact, not a logic bug — the real discriminator is C19644b1/C19647c1 payload fields.)
  • p494q7\C19640a0 / C19641a1 / C19657g and p285f8\* (f8.*) carry the Facebook LoginManager wiring (com.facebook.C9154o, com.facebook.FacebookException).
  • p050ce\ce.c (C3912c) extends CustomCredential from com.google.android.libraries.identity.googleid and references GoogleIdTokenParsingException — this is the Google Sign-In credential adapter.
  • After social login, q7.l0.mo309m() case 5 fills the name/photo and writes SP_KEY_PICTURE_SOURCE = "google" (i13 == 1) or "facebook".
  • The social_auth/use_cli_validation endpoint (p607vk\RunnableC21082e0, C2-adjacent vk package) is used to check whether an already-socially-authenticated CLI still needs SMS validation.
  • A dedicated “picture source” enum C19665i1.f50624a + AbstractC16257t1.m31616A(...) records the validation channel, which later becomes SP_KEY_VALIDATION_WITH.

5. Flow 2 — CLI authorization / second-number provisioningh4

5.1 Endpoints (OBSERVED)h5
EndpointMethodCallerPurpose
https://api.eyecon-app.com/app/join.jspGETjd.z case 1 (RunnableC17614z)Register/obtain a new CLI + the clientId
https://api.eyecon-app.com/app/cli_auth/gettransportGETjd.z default branchAsk the server how to validate the CLI (transport, message body, code, timeout)
https://api.eyecon-app.com/app/cli_auth/checkcodeGETr5.s case 1 (RunnableC19875s)Submit the SMS/call code
https://api.eyecon-app.com/app/social_auth/use_cli_validationGETvk\RunnableC21082e0 (logged as "join.jsp")Re-validate an already social-authenticated CLI
https://api.eyecon-app.com/app/updateuserdetails.jspGETvk\RunnableC21082e0, r5\RunnableC19874r, w6\RunnableC21275cPush profile/registration details
https://api.eyecon-app.com/app/getoptionalpics.jspGETvk\RunnableC21082e0Suggested avatars
https://api.eyecon-app.com/app/mysettingsGET / POSTw6\RunnableC21276d case 8, u4\RunnableC20597o case 7Client ⇄ server key/value settings sync
https://api.eyecon-app.com/app/get_store_productsGETw6\RunnableC21276d case 6Store product catalogue
https://api.eyecon-app.com/app/virality/premium_status, virality/viral_id, virality/viral_id_checkGETw6\RunnableC21276d cases 3/4/9Free-premium/virality
https://api.eyecon-app.com/app/uploadpic.jspPOST multipartw6.h.m38441n()Avatar upload (writes raw clientId as msisdn)
https://api.eyecon-app.com/app/picapproval.jspGETu4\RunnableC20597o case 3Avatar moderation
5.2 Second-number provisioning path (step by step)h5
  1. PhoneNumberValidationActivity starts q7.i1 (C19665i1) with the new CLI.
  2. q7.i1.m36102a(regId) → jd.z default branch → cli_auth/gettransport.
    • The server answers which transport to use and, for mo_sms, supplies the SMS body, the destination, the expected code and a timeout, which the app then auto-reads via the SMS Retriever API (Registration\SMSRetrieverBroadcast).
    • so_flash = USSD/flash-code flow (details{timeout, pattern}).
    • so_call = missed-call / call-based validation.
  3. User (or SMS Retriever) supplies the code → r5.s case 1 → cli_auth/checkcode.
    • Success is parseInt(body) == 0 && HTTP 200 — a bare integer body, not JSON.
    • C16489q.f40981n.f40984c = true then p308gj\C16770d continues into join.jsp with the svr= value derived from q7.i1.m36103b(code).
  4. join.jsp returns is_new:
    • is_new == "1" → new account; q7.n.mo309m() case 2 writes clientId, clears SP_KEY_NOT_REGISTERED_WITH_REAL_CLI_V2.
    • otherwise → second number on the existing account; q7.l0.mo309m() case 4 writes clientId (overwriting), SP_KEY_VALIDATION_WITH, deviceName, osName, sp_join_time, SP_JOIN_VERSION, SP_JOIN_VERSION_CODE=749, SP_KEY_IS_REJOIN=true.
  5. On isEmulator or server error code ∈ {3, 4, 7} the UI hard-resets instead of allowing a retry.
5.3 PhoneAccount module — OBSERVED: IT IS A DEAD STUBh5
  • com\eyecon\global\PhoneAccount\EyeconAccountAuthService → onCreate builds p381k7\C17790b (k7.b, an AbstractAccountAuthenticator).
  • Every method of k7.b is a stub: addAccount → null; confirmCredentials → null; editProperties / getAuthToken / getAuthTokenLabel / hasFeatures / updateCredentials → throw new UnsupportedOperationException().
  • SyncService binds k7.c (AbstractThreadedSyncAdapter) whose onPerformSync is an empty body.
  • AccountWithDataSet is a 3-string Parcelable whose toString() returns "", plus a dead C17789a (k7.a) duplicate that is instantiated and immediately discarded in both constructors.
  • CONCLUSION: there is no Android AccountManager token issuance for Eyecon CLI in this build. All CLI authorisation is done by direct HTTP to cli_auth/* with the e-auth header. If a prior report described a “PhoneAccount registration flow” that mints tokens, that report is wrong for this build.

6. Flow 3 — Install attribution / referrer handlingh4

6.1 InstallReferrerReceiver (OBSERVED, full logic)h5

onReceive → C16942f.m32766c(new RunnableC17219d1(this, new Intent(intent), 12, goAsync())) (background thread, goAsync()).

m10455a(MyApplication, Intent):

  1. Delegates first to new SingularInstallReceiver().onReceive(app, intent) (Singular SDK).
  2. Logs the entire referrer bundle to System.out with tag IRR/ — every key and value of the INSTALL_REFERRER intent is printed to logcat in release builds. (Note: the print_logs_in_release remote-config key exists but this call is a hard-coded System.out.println, not gated by it.)
  3. Reads bundle.getString("referrer") (default "") → m10456b(referrer, 1).

m10456b(String str, int i3):

  • One-shot latch: if (MyApplication.m10348j().getBoolean("referrer call", false)) return; then sets referrer call = true.
  • Viral-id format A — referrer.startsWith("evid-"): vid = referrer.substring(5); accepted only if vid.length() == 12 (no charset validation, no server-side verification at this point). Then m10457c() (one-shot Invite_successfully_accepted analytics event + sendViralityEventDone=true) and AbstractC0037a.m112C(null, "SP_KEY_INVITER_ID", vid).
  • Channel format B — referrer.startsWith("eyecon_ch_"): parts = referrer.replace("eyecon_ch_","").split("_campid_"); must be exactly 2 parts; emits an Install referrer channel analytics event with the channel as a parameter. Nothing is persisted.
  • Viral-id format C (promotion) — parses the referrer as a URI (Uri.parse("fake_scheme://fake_subdirectory?" + referrer)) and reads utm_content; if it starts with evid and the remainder is 12 chars, writes SP_KEY_INVITER_ID.
    • NOTE: the referrer string is concatenated raw into a URI and parsed. No escaping/validation of the referrer text before URI parsing.
6.2 Where SP_KEY_INVITER_ID is consumed (OBSERVED)h5
  • join.jsp sends it as viral_id (jd.z case 1) — i.e. the inviter id is sent to the backend on every join.
  • j5.b (C17485b, FreePremium) calls virality/viral_id (returns viralId) and virality/viral_id_check; on success it persists SP_KEY_MY_VIRAL_ID (w6\RunnableC21276d case 9).
  • j5.c (C17487c) uses SP_KEY_MY_VIRAL_ID + SP_KEY_LAST_DATE_FOR_VIRAL_PREMIUM / SP_KEY_LAST_DATE_FOR_FREE_PREMIUM / SP_KEY_LAST_DATE_FOR_VIDEO_PREMIUM / SP_KEY_LAST_FREE_PREMIUM_TYPE to grant and display “free premium” entitlements.
  • Remote config RewardedAdGiftDays, InviteFriendPrice, daysOfTrialIdPlus control the amount.
  • y5.v (C21867v) implements InstallReferrerStateListener and p285f8, p397l5, p671y4, p700z4 hold the rest of the referrer plumbing.
6.3 Free-premium (“virality”) entitlement path (OBSERVED)h5

j5.c (C17487c):

  • m33582f(long millis) — grant: writes SP_KEY_LAST_DATE_FOR_FREE_PREMIUM = now + millis and SP_KEY_LAST_FREE_PREMIUM_TYPE ("video"/"Free Version"/…).
  • m33583h(Runnable) — enqueue on the server executor; posts a system notification (FreePremiumUserActivity intent, extras "received premium notification", tag "free premium update").
  • Triggered by rewarded ads: j5.f (C17491f) on onUserEarnedReward → C17487c.m33582f(activity.f11528X * TimeUnit.DAYS.toMillis(1)) where f11528X is the number of days granted (remote config RewardedAdGiftDays).
  • The number of granted premium days is a client-side value taken straight from the rewarded-ad callback and written to prefs. See §9.

7. Flow 4 — Premium purchase (Play Billing)h4

7.1 Architecture (OBSERVED)h5
UI Engine / manager Service proxy Play
────────────────────────────────────────────────────────────────────────────────────────────
j5.* (entitlement) g5.i C16707i "EyeconBilling" q1.c C19537c (BillingClient) Play BillingClient
w7.i C21289i dialog g5.b RunnableC16700b q1.a C19533a (builder)
j5.q C17502q dialog g5.j C16708j (product) q1.* zzj*/zzk* (proto codec)
g5.l C16710l (GoogleProduct) kh.C17847a (BillingFlowParams)
g5.d C16702d (queryPurchases) RunnableC19542e0 (service proxy)
g5.h C16706h (acknowledge)
j5.k C17496k "PaidPremium" — local entitlement cache + server sync
j5.c C17487c "FreePremium" — ad/virality entitlement (see §6.3)
7.2 Product + offer loadingh5
  • The authoritative product id list is remote config: premium_products_ids_list (AbstractC15804d.m31088i("premium_products_ids_list") → JSON {"subs":[...],"inapp":[...]}). j5.k.m33586b(String productId) is the membership test used everywhere: if the id is not in premium_products_ids_list, the purchase is NOT treated as premium.
  • Other remote-config product keys: premium_products, premium_products_v2, no_ad_lifetime_product_id, no_ad_yearly_product_id, no_ad_monthly_product_id, custom_background_theme_product_id, whatsappProductId, premium_sub_sku, golden_users_products, ttm_products_and_tokens_map, AllCustomBackgroundProductsHistory, AllWhatsappIdProductsHistory.
  • k5.a (C17749a) holds SP_KEY_STORE_PRODUCTS_IDS_JSON (default {"subs":[],"inapp":[]}); when empty it fetches get_store_products (w6\RunnableC21276d case 6) with "do_product_detail_load"=true; otherwise it calls g5.i.m32534m(...) to query details directly.
  • g5.i.m32524e(ArrayList, C16703e) → zzco.zzk(list) builds the QueryProductDetailsParams proto and calls q1.c.mo35972g(...). It rejects an empty list (IllegalArgumentException("Product list cannot be empty.")) and rejects mixed product types ("All products should be of the same product type."), excluding play_pass_subs.
  • Subscription offers are modelled by q1 DTOs: C19549l (ProductDetails), C19548k (base plan / subscription offer), C19547j (offer), mapped in WebviewPaywallActivity.m10027E0 to C5841h/C5839f/C5840g.
7.3 Purchase launchh5

g5.i.m32537q(Activity, C16708j, callback):

  • requires the product to be a C16710l (GoogleProduct) else IllegalArgumentException("showProductGoogle called with non-GoogleProduct: …");
  • → RunnableC3701s3 (in bm) case 2:
    • builds ProductDetailsParams (C17847a) using productDetails.getZza().f50198d (basePlanId) and the caller-supplied offerToken; throws IllegalArgumentException("offerToken can not be empty");
    • zzbe.zzc(productDetails, "ProductDetails is required for constructing ProductDetailsParams."), zzbe.zzc(offerToken, "offerToken is required … for subscriptions.");
    • builds BillingFlowParams (C19543f, zzco.zzk(...)) and calls q1.c.mo35971f(activity, params) = BillingClient.launchBillingFlow.
    • result: callback.m31518o(Integer.valueOf(responseCode)); callback.m31516i();
  • INFERRED: offerToken is taken verbatim from the caller. In the store flow that caller is the JavaScript page (§8), so the page chooses which offer the user is billed at. There is no cross-check of the chosen offerToken against any server-issued price or against the plan the app intends to sell. The productId is constrained (it must exist in StoreActivity.f11618X, populated from Play for the ids the app itself asked about), but the offer/base-plan variant is not.
7.4 Purchase result → acknowledgement → entitlementh5
  • q1.InterfaceC19539d.mo32528a(C19544g, List) = onPurchasesUpdated (g5.i):
    • responseCode == 1 (USER_CANCELED) → RunnableC16704f (onPurchasesCanceled);
    • g5.i.m32536p(new RunnableC16700b(this, callback, 1)) → m32522c(includeSubs=true, …);
    • billingClient.m35973h("inapp", …) and g5.i.m32529f(list, 0, new HashMap()) → acknowledge.
  • g5.i.m32521b(...):
    • skip if JSON.optBoolean("acknowledged", **true**) — note the default is true, so a missing acknowledged field means “already acknowledged”;
    • skip unless purchase.m4425a() == 1 (PURCHASED);
    • token = optString("token", optString("purchaseToken"));
    • C0162c(5) = AcknowledgePurchaseParams → q1.c.mo35969d(...).
    • There is no consumeAsync anywhere in g5/q1/j5 — nothing is consumed.
  • Acknowledgement retry: g5.i.m32529f recurses up to i3 == 10 then logs "acknowledgePurchases failed to acknowledge all purchases retryCount reached.".
  • Entitlement is computed by p008a7\C0076s0 case 12:
    for each key in {"subs","inapp"}:
    for each g5.j product:
    if (product.purchaseState == 1)
    if (j5.k.m33586b(product.productId)) j5.k.m33590d(product, now); // PAID premium
    else if (type == "inapp") { register as "inapp" product; flag=true }
    if (flag) prefs["SP_KEY_INAPP_PRODUCTS_CACHE"] = <list>
  • p034b7\C3502i case 12 does the same for the “load products” flow and, on CB_ERROR == 1 with nothing owned, calls mo308k() (error path).
  • g5.i.onBillingSetupFinished on success calls j5.k.m33587c(null) → g5.i.m32531h(callback) → m32522c(true, …) → the same entitlement refresh. It also writes SP_KEY_SUPPORT_CACHE_OWN_CACHE = whether the Play cache can be trusted.
7.5 Is there any server-side purchase verification? — NO Play purchase verificationh5
  • OBSERVED: the entire set of backend calls reachable from the billing packages is listed in §5.1. None of them carries a Play purchaseToken, productId, obfuscatedExternalAccountId, or any Google Play Developer API verification call. There is no androidpublisher/play.google.com HTTP call, no Purchases.products.get, no RTDN ( Purchases.subscriptionsv2.get) endpoint, and no server-side signature/RSA check anywhere in g5, q1, j5, w7, x, c0, k0, hh, yj.
  • The only thing pushed to the Eyecon backend about premium status is mysettings (POST) with keys premium_cache_product and premium_cache_time (j5.k.m33588e → C16464d0.m31983a() → p008a7\RunnableC0070p0 case 18 → w6.h.m38440m(...) → u4\RunnableC20597o case 7 → POST https://api.eyecon-app.com/app/mysettings with type, key, value). premium_cache_product is a client-supplied JSON blob {productType, productId, purchaseToken, purchaseState, originalJson}.
  • VERDICT: premium entitlement is 100 % client-side. The server is told “I am premium” by the client; the server never asks Google. Rewarding/stacking virality premium (virality/premium_status) is likewise decided by Eyecon’s own backend, but paid premium is not verifiable server-side from this client.
  • mysettings GET (w6\RunnableC21276d case 8) is only read back to populate SP_KEY_MY_SETTINGS_SERVER_JSON, which j5.k’s static initialiser uses purely as a “have we ever talked to the server” gate — not as an entitlement source.
7.6 Free-premium (ad/virality) vs paid premiumh5

j5.c (C17487c) exposes: m33581d(endTime, Bundle, boolean), m33582f(days→millis) (grant), m33583h(Runnable), updateViralStatus, updateVideoPremium, updateFreePremium. Persistence keys: SP_KEY_LAST_DATE_FOR_VIRAL_PREMIUM, SP_KEY_LAST_DATE_FOR_VIDEO_PREMIUM, SP_KEY_LAST_DATE_FOR_FREE_PREMIUM, SP_KEY_LAST_FREE_PREMIUM_TYPE, SP_KEY_MY_VIRAL_ID, SP_KEY_LAST_TIME_REQUESTED_CARDS. All values are plain long epoch-ms written to the custom prefs — see §9.


8. Flow 5 — The Eyecon / EyeconStore / Android JavaScript bridge surfaceh4

8.1 Where store_url comes from and whether it is validated — NOT VALIDATED. HIGH.h5

com\eyecon\global\Billing\Store\StoreActivity.onCreate (OBSERVED, verbatim):

String strM31091l = AbstractC15804d.m31091l("store_url", false); // Firebase Remote Config
this.f11622b0 = strM31091l; // the URL
this.f11621a0 = Uri.parse(strM31091l).getHost().replace("www.", ""); // host, used for error filtering
…
((WebView) this.f11615U.f49837d).getSettings().setDomStorageEnabled(true);
((WebView) this.f11615U.f49837d).getSettings().setJavaScriptEnabled(true);
CookieManager.getInstance().setAcceptThirdPartyCookies((WebView) this.f11615U.f49837d, true);
((WebView) this.f11615U.f49837d).addJavascriptInterface(new C16132d(), "Eyecon");
C17757i c17757i = new C17757i(); c17757i.f44985a = this; this.f11624d0 = c17757i;
((WebView) this.f11615U.f49837d).addJavascriptInterface(c17757i, "EyeconStore");
((WebView) this.f11615U.f49837d).setWebChromeClient(new C5845l(this, 1));
((WebView) this.f11615U.f49837d).setWebViewClient(new C5846m(this));

Findings, OBSERVED:

  1. store_url is supplied by Firebase Remote Config (AbstractC15804d.m31091l → m31092m(str) → remoteConfig.getValue(str)), with a compiled-in default of the empty string (map.put("store_url", "")). Because Uri.parse("").getHost() is null, an empty store_url makes line 2 throw an NPE — so StoreActivity is only reachable when Remote Config (or the FB_RC_<key> prefs override in m31093n) supplies a value.
  2. NO scheme check, NO host allow-list, NO certificate pinning, NO integrity check on store_url. WebviewPaywallActivity does have exactly that check (§8.5) — StoreActivity has none of it. This is the asymmetry that makes the issue real.
  3. setJavaScriptEnabled(true), setDomStorageEnabled(true) and setAcceptThirdPartyCookies(true) are all set unconditionally.
  4. Two @JavascriptInterface bridges are attached: Eyecon (p262e7.C16132d = e7.d) and EyeconStore (p379k5.C17757i = k5.i).
  5. C5846m (the shared WebViewClient) does not override shouldOverrideUrlLoading for the StoreActivity variant (f11594d == 2 falls through to default: super). Combined with the JS bridges staying attached for the lifetime of the WebView, any link the page opens — or any redirect / XSS / MITM-served page — inherits the full bridge surface.
  6. onReceivedSslError does not call sslErrorHandler.proceed() (it delegates to super.onReceivedSslError which cancels), so TLS errors are not silently accepted — but there is also no pinning, so a valid-CA interception is enough.
  7. Host matching for error reporting uses host.contains(storeActivity.f11621a0) — a substring match, and only for error reporting, not for security decisions.
8.2 Eyecon bridge — p262e7\C16132d (renamed from: e7.d) — full method listh5

Attached in both StoreActivity and WebviewPaywallActivity.

@JavascriptInterface methodSignatureReturns / effectWhat it exposes
getCid()String getCid()AbstractC9039h.m10662s() = prefs clientIdThe bearer credential used in the e-auth header for every Eyecon API call. Anyone holding it can impersonate the user to the backend.
getCli()String getCli()AbstractC9039h.m10649f() = prefs authenticatedCliThe validated phone number (MSISDN) in E.164 form. PII.
getPublicId()String getPublicId()AbstractC9039h.m10625E() = prefs SP_UUIDStable per-install public identifier used by the backend for attribution.
getAdvertisingId()String getAdvertisingId()InterfaceC19056i.f48663a.m35361a() → AdvertisingIdClient.getAdvertisingIdInfo(...), cached in Eyecon.SP_KEY_ADVIDGoogle Advertising ID. Persistently re-identifiable cross-app tracking handle.
getMyName()String getMyName()AbstractC9039h.m10663t("") = prefs usernameThe user’s registered display name. PII.
getStringSP(String key, String def)String getStringSP(String, String)AbstractC17503r.m33607k(key, def)Arbitrary read of ANY string key in the app’s custom prefs (SQLite contactdb.shared_preference_v2). Includes clientId, SP_UUID, SP_KEY_MY_SETTINGS_SERVER_JSON, SP_KEY_PREMIUM_PURCHASES_INFO_CACHE (which contains the Play purchaseToken), SP_KEY_STORE_OWN_ITEMS, SP_KEY_INVITER_ID, deviceName, osName, SP_JOIN_VERSION, etc. No key allow-list, no prefix check.
getBooleanSP(String key, boolean def)boolean getBooleanSP(String, boolean)MyApplication.m10348j().getBoolean(key, def)same, boolean.
getIntSP(String key, int def)int getIntSP(String, int)…getInt(key, def)same, int.
getFloatSP(String key, float def)void getFloatSP(String, float)void — the result is discarded; the call exists only as a probe.Dead method (dead code, not a leak).
getLongSP(String key, long def)void getLongSP(String, long)void — result discarded.Dead method.
getVersionCode()int getVersionCode()hard-coded 749App version code.
getVersionName()String getVersionName()hard-coded "4.2026.04.23.1145"App version.
getCountryISO()String getCountryISO()AbstractC0062l0.m371z1()Device/SIM country ISO.
getLanguageCode()String getLanguageCode()AbstractC16733c.m32596o()UI language.
getScreenWidth() / getScreenHeight()intAbstractC0062l0.m368w1() / m366u1()Device metrics (fingerprinting).
getUserProperty(String k, String d)String getUserProperty(String, String)returns the literal string "Events.getUserProperty(key,defaultValue)"No-op / placeholder.
reportEvent(String name, String propsJson)void reportEvent(String, String)Parses propsJson via C18416a.m34586w(str2); walks the object, fills a HashMap (Boolean/Number/String), then AbstractC3464e.m3659j(name, map, false). If propsJson is empty → AbstractC3464e.m3658i(name).Unbounded analytics injection: arbitrary event name + arbitrary key/value properties pushed into the app’s analytics pipeline (and, given the pipeline, likely off-device). There is no name allow-list and no size limit.
8.3 EyeconStore bridge — p379k5\C17757i (renamed from: k5.i) — full method listh5

Back-pointer field public StoreActivity f44985a (cleared on onDestroy). All methods post onto the main-thread executor C16945i.m32771c(...). Guard helper: private final boolean m34071a() → storeActivity == null || storeActivity.isFinishing().

@JavascriptInterface methodSignatureNative effect
close()void close()RunnableC17750b(this, 3) → storeActivity.finish().
getProductsInfo(String productsJson, String jsCallback)voidRunnableC17334k case 4: parses productsJson ({subs:[ids],inapp:[ids]}), builds C16708j(productId,"subs"/"inapp"), calls g5.i.m32534m(...) (Play QueryProductDetails), and on success calls back jsCallback with { "inapp":[{…product detail…}], "subs":[…]}. The page chooses which product ids are queried.
getPurchasedProducts(String jsCallback)voidRunnableC16111m case 28 → m31497m(): serialises StoreActivity.f11616V (subs) and f11617W (inapp) with C16710l.m32540c() → {productType, productId, purchaseToken, purchaseState, originalJson} and calls back the JS function. Leaks Play purchase tokens to the web page.
purchase(String productId, String offerToken, String p3)voidRunnableC3589a case 13 → look up productId in StoreActivity.f11618X; if absent → StoreException("failed to find product, product_id = …") + m3722q0("","STORE_3"). If present → g5.i.m32537q(storeActivity, C16708j, C17752d(...)) → RunnableC3701s3 case 2 → BillingClient.launchBillingFlow with the page-supplied offerToken. (Third arg is unused in this arm.)
getPeriodIntoDays(String isoPeriod)intg5.i.m32527n(str) → Period.parse(str).getDays() on API ≥ 26, else a hand-rolled regex parser ^P(\d*Y)?(\d*W)?(\d*D)?. Accepts "Inapp"? → throws RuntimeException on unsupported periods inside m32526k, but m32527n itself will throw DateTimeParseException on garbage.
getUiTextForSubPeriod(String isoPeriod)Stringg5.i.m32526k(ctx, str): "Inapp"/empty → R.string.lifetime; P1M→monthly, P1Y→yearly, P3M→quarterly, P4W→x4_weeks, P6M→half_yearly; anything else → RuntimeException("subPeriod unsupported, subPeriod = …").
getSelectedItems(String jsCallback)voidBuilds {theme_selected_item, ringtone_selected_item} from StoreActivity.f11619Y/f11620Z (deserialised from prefs SP_KEY_STORE_SELECTED_THEME_JO / SP_KEY_STORE_ITEM_SELECTED_RINGTONE_JO) and calls back.
itemSelected(String productId, String itemId, int itemType)voidRunnableC17756h case 0 → C15802b(type, itemId, 2) persisted via C16942f.m32766c(new RunnableC3497d(itemId, c15802b, type)), i.e. the web page can record which theme/ringtone item the user picked.
onError(int code, String message)voidAbstractC9039h.m10632L(new StoreException("error code = " + code + ", error description = " + message)) — reported to the app’s crash/exception reporter, so the web page can inject arbitrary exception text into the app’s error telemetry.
downloadItem(String productId, String itemId, int itemType)voidRunnableC17180l0 case 1: finds the purchased C16710l, creates C17755g(itemId, type), C17758j(productId, type, purchaseToken, {store_items:[itemId]}), and enqueues on C17754f (thread pool "store_download_queue"). C17754f.m34065a then GETs AbstractC15804d.m31091l("store_download_url", false) + "/getItem" with a JSON body containing cli, cid (clientId!), version_name, version_code, product, item, via C21273a.m38417a(request, true) (which adds the e-auth* headers). Response is a ZIP that is unzipped straight into getFilesDir()/store — with a correct zip-slip guard (§8.6).
getDownloadItemsState(String jsCallback)voidRunnableC16111m(29, C17754f, C17752d) on the download queue → C17752d case 0 flattens every C17758j.store_items across C17754f.f44975b and calls back the JS function with a JSON array.
8.4 Android bridge — only in WebviewPaywallActivity (C5838e)h5

public final String f11571a = AbstractC16733c.m32596o() (language, captured at construction), public final String f11572b (the serialised product list), public volatile WebviewPaywallActivity f11573c.

methodeffect
getLanguage()returns the captured language code
getProducts()returns the product-list JSON (built in m10027E0 from Play ProductDetails)
onProductSelected(String, String)RunnableC0032d(this, str, 9, str2) → launches the Play purchase flow
onFreeOptionSelected(String)RunnableC5837d → the free/ad option
onClose()RunnableC5836c case 0 → finish
onPrivacyPolicy()RunnableC5836c case 1
onTermsOfService()RunnableC5836c case 2
8.5 The paywall WebView DOES validate its URL — the contrast that proves the Store gaph5

WebviewPaywallActivity (OBSERVED, verbatim):

Uri uri = Uri.parse(this.f11559d0.f11565a);
String host = uri.getHost(); if (host == null) host = "";
String strReplace = host.replace("www.", "");
if ("https".equals(uri.getScheme()) && (strReplace.equals("eyecon-app.com") || strReplace.endsWith(".eyecon-app.com"))) {
((WebView) this.f11550U.f4651d).loadUrl(this.f11559d0.f11565a);
return;
}
AbstractC9039h.m10632L(new SecurityException("Untrusted paywall URL: " + this.f11559d0.f11565a));
finish();

The paywall URL itself comes from a remote-config placement object (AbstractC15804d.m31088i(intent.getString("placement_id")) → C5835b with url + tiers[].items[]). So the team implemented URL validation for the paywall and omitted it for the store. That is an inconsistency, not a deliberate design, and it is the strongest argument that the store gap is a bug.

8.6 Positive note — zip-slip is handled in the store downloader (OBSERVED)h5

k5.f (C17754f.m34068d) unzips into getFilesDir()/store and checks every entry:

if (!file2.getCanonicalPath().startsWith(m34067c().getCanonicalPath()))
throw new SecurityException("Zip Path Traversal Vulnerability, file path = …, dir = …");

(This check exists in C2 scope and should be credited; the concern is that the ZIP source host is remote-config controlled.)

8.7 Is this the same mechanism as C1’s a7.t.m425J0? — NO. Different mechanism.h5
  • C1’s a7.t.m425J0 injects four JavaScript bridge objects / script blobs into a remote WebView whose URL is controlled by search_url + links_script + big_url_script. That is the Google-Photos link loader / reverse-lookup WebView.
  • StoreActivity / WebviewPaywallActivity use a separate, statically-named pair of bridges (Eyecon / EyeconStore / Android) added directly with addJavascriptInterface, and the store_url remote-config key is used only in StoreActivity.onCreate (grep over the whole tree: store_url appears exactly twice — the default "" and that one call).
  • They do share one thing: C5846m (the WebViewClient) and the f7.r base class (AbstractC16490r, handles onRenderProcessGone). No script injection, no evaluateJavascript bridge builder, no shared payload.

9. Appendix A — Complete class inventory for all 50 packagesh4

Every .java file in the C2 scope with its JADX enamed from real name, the class kind (derived from the JADX file-name prefix) and the byte size. Roles are detailed in §10 for every class that carries logic, endpoints, prefs keys or secrets.

$k — module Billing (MISFILED: kotlinx.coroutines) — 129 filesh5
disk filereal classkindbytes
AbstractC3247a1.javaas.a1abstract class13617
AbstractC3248a2.javaas.a2abstract class7697
AbstractC3251b.javaas.babstract class32305
AbstractC3255b3.javaas.b3abstract class3789
AbstractC3260c3.javaas.c3abstract class4379
AbstractC3297k0.javaas.k0abstract class3124
AbstractC3306m.javaas.mabstract class3475
AbstractC3329q2.javaas.q2abstract class14561
AbstractC3333r1.javaas.r1abstract class2426
AbstractC3342t0.javaas.t0abstract class3146
AbstractC3346u.javaas.uabstract class4252
AbstractC3371z.javaas.zabstract class2203
C3245a.javaas.aclass1067
C3246a0.javaas.a0class1144
C3249a3.javaas.a3class130
C3250a4.javaas.a4class1137
C3252b0.javaas.b0class5586
C3253b1.java-class17213
C3254b2.javaas.b2class130
C3256c.javaas.cclass3226
C3257c0.javaas.c0class1643
C3258c1.javaas.c1class4952
C3259c2.javaas.c2class128393
C3261d.javaas.dclass128
C3262d0.javaas.d0class8936
C3263d1.javaas.d1class130
C3264d2.javaas.d2class130
C3265d3.javaas.d3class5535
C3266e.javaas.eclass12270
C3267e0.javaas.e0class130
C3268e1.javaas.e1class11637
C3269e2.javaas.e2class3256
C3270e3.javaas.e3class130
C3271f.javaas.fclass128
C3272f0.javaas.f0class5028
C3274f2.javaas.f2class2482
C3275f3.javaas.f3class5260
C3276g.javaas.gclass1690
C3277g0.java-class61607
C3278g1.javaas.g1class130
C3279g2.javaas.g2class77197
C3280g3.javaas.g3class14858
C3281h.javaas.hclass1067
C3282h0.javaas.h0class130
C3283h1.javaas.h1class18434
C3284h2.javaas.h2class5751
C3285h3.javaas.h3class683
C3286i.javaas.iclass4304
C3287i0.javaas.i0class8301
C3288i1.javaas.i1class1182
C3289i2.javaas.i2class3235
C3290i3.javaas.i3class4202
C3291j.javaas.jclass5254
C3292j0.javaas.j0class130
C3293j1.javaas.j1class8965
C3294j2.javaas.j2class4108
C3295j3.javaas.j3class130
C3296k.javaas.kclass128
C3298k1.javaas.k1class1070
C3299k2.javaas.k2class130
C3300k3.javaas.k3class5305
C3301l.javaas.lclass7843
C3302l0.javaas.l0class1076
C3303l1.javaas.l1class13084
C3304l2.javaas.l2class9692
C3305l3.javaas.l3class130
C3307m0.javaas.m0class4108
C3308m1.javaas.m1class12378
C3309m2.javaas.m2class1859
C3310m3.javaas.m3class1070
C3311n.javaas.nclass1067
C3312n0.javaas.n0class130
C3313n1.javaas.n1class8674
C3314n2.javaas.n2class130
C3315n3.javaas.n3class4610
C3317o0.javaas.o0class11250
C3318o1.javaas.o1class130
C3319o2.javaas.o2class75509
C3320o3.javaas.o3class8928
C3322p0.javaas.p0class1364
C3323p1.javaas.p1class5155
C3324p2.javaas.p2class130
C3325p3.javaas.p3class130
C3327q0.javaas.q0class1070
C3328q1.javaas.q1class3861
C3330q3.javaas.q3class25532
C3332r0.javaas.r0class7365
C3335r3.javaas.r3class1244
C3336s.javaas.sclass12732
C3337s0.javaas.s0class8019
C3338s1.javaas.s1class7289
C3340s3.javaas.s3class130
C3341t.javaas.tclass2491
C3343t1.javaas.t1class130
C3344t2.javaas.t2class3572
C3345t3.javaas.t3class5501
C3347u0.javaas.u0class9006
C3348u1.javaas.u1class22035
C3349u2.java-class46658
C3350u3.javaas.u3class348
C3351v.javaas.vclass128
C3352v0.javaas.v0class130
C3353v1.javaas.v1class3397
C3354v2.javaas.v2class6084
C3355v3.javaas.v3class1070
C3356w.javaas.wclass4797
C3357w0.javaas.w0class4968
C3358w1.javaas.w1class13992
C3359w2.javaas.w2class130
C3360w3.javaas.w3class7141
C3361x.javaas.xclass128
C3362x0.javaas.x0class130
C3363x1.java-class66195
C3364x2.javaas.x2class6169
C3365x3.javaas.x3class5806
C3366y.javaas.yclass1067
C3367y0.javaas.y0class1076
C3368y1.javaas.y1class1776
C3369y2.javaas.y2class130
C3370y3.javaas.y3class130
C3372z0.javaas.z0class8725
C3373z1.java-class11558
C3374z2.javaas.z2class4108
C3375z3.javaas.z3class97158
RunnableC3273f1.javaas.f1Runnable2572
RunnableC3316o.javaas.oRunnable44520
RunnableC3326q.javaas.qRunnable20479
RunnableC3334r2.javaas.r2Runnable1570
RunnableC3339s2.javaas.s2Runnable8409
$k — module Registration (MISFILED: OkHttp/Okio) — 156 filesh5
disk filereal classkindbytes
AbstractC3595b.javabm.babstract class6362
AbstractC3601c.javabm.cabstract class7502
AbstractC3607d.javabm.dabstract class1774
AbstractC3609d1.javabm.d1abstract class9561
AbstractC3615e1.javabm.e1abstract class490
AbstractC3627g1.javabm.g1abstract class2179
AbstractC3667n.javabm.nabstract class361
AbstractC3671n3.javabm.n3abstract class255
AbstractC3702s4.javabm.s4abstract class8150
AbstractC3705t1.javabm.t1abstract class527
AbstractC3711u1.javabm.u1abstract class1253
AbstractC3717v1.javabm.v1abstract class2356
AbstractC3723w1.javabm.w1abstract class9693
AbstractC3728x0.javabm.x0abstract class1438
AbstractC3734y0.javabm.y0abstract class1412
AbstractC3738y4.javabm.y4abstract class967
AbstractRunnableC3608d0.javabm.d0abstract class1782
C3590a0.javabm.a0class12208
C3591a1.javabm.a1class182
C3593a3.javabm.a3class2132
C3594a4.javabm.a4class4348
C3597b1.javabm.b1class659
C3599b3.javabm.b3class923
C3600b4.javabm.b4class724
C3602c0.javabm.c0class4564
C3604c2.javabm.c2class489
C3605c3.javabm.c3class11313
C3606c4.javabm.c4class5788
C3610d2.javabm.d2class2667
C3611d3.javabm.d3class10747
C3613e.javabm.eclass17475
C3616e2.javabm.e2class1828
C3617e3.javabm.e3class373
C3619f.javabm.fclass877
C3621f1.javabm.f1class1945
C3622f2.java-class26205
C3623f3.javabm.f3class1615
C3624f4.javabm.f4class573
C3625g.javabm.gclass1448
C3626g0.javabm.g0class3492
C3628g2.javabm.g2class3763
C3629g3.javabm.g3class1336
C3630g4.javabm.g4class2141
C3631h.javabm.hclass866
C3632h0.javabm.h0class1554
C3635h3.javabm.h3class4208
C3636h4.javabm.h4class2610
C3637i.javabm.iclass199
C3638i0.javabm.i0class2798
C3639i1.javabm.i1class4239
C3640i2.javabm.i2class2934
C3641i3.javabm.i3class1888
C3642i4.javabm.i4class395
C3643j.javabm.jclass1974
C3645j1.javabm.j1class8078
C3646j2.javabm.j2class1511
C3647j3.javabm.j3class1944
C3648j4.javabm.j4class2208
C3649k.javabm.kclass2690
C3650k0.javabm.k0class14545
C3651k1.javabm.k1class2243
C3652k2.javabm.k2class1691
C3653k3.javabm.k3class5814
C3654k4.javabm.k4class2533
C3655l.javabm.lclass1195
C3656l0.javabm.l0class9642
C3658l2.javabm.l2class13111
C3659l3.javabm.l3class2556
C3660l4.javabm.l4class706
C3661m.javabm.mclass2919
C3664m2.javabm.m2class4134
C3665m3.javabm.m3class4169
C3669n1.javabm.n1class1341
C3672n4.javabm.n4class1146
C3673o.javabm.oclass2236
C3674o0.javabm.o0class4630
C3675o1.javabm.o1class6382
C3676o2.javabm.o2class7131
C3677o3.javabm.o3class3664
C3678o4.javabm.o4class1075
C3679p.javabm.pclass5745
C3682p2.java-class20820
C3685q.javabm.qclass2915
C3687q1.java-class15867
C3688q2.javabm.q2class11688
C3689q3.javabm.q3class1274
C3690q4.javabm.q4class1624
C3691r.javabm.rclass4205
C3693r1.javabm.r1class1260
C3694r2.javabm.r2class3953
C3695r3.javabm.r3class543
C3696r4.javabm.r4class1760
C3697s.java-class20053
C3698s0.javabm.s0class19474
C3699s1.javabm.s1class9133
C3700s2.javabm.s2class628
C3704t0.javabm.t0class1450
C3706t2.javabm.t2class7204
C3707t3.javabm.t3class1225
C3708t4.javabm.t4class557
C3710u0.javabm.u0class1055
C3712u2.javabm.u2class948
C3713u3.javabm.u3class868
C3716v0.javabm.v0class2102
C3718v2.javabm.v2class6493
C3719v3.javabm.v3class1016
C3720v4.javabm.v4class3471
C3722w0.javabm.w0class2646
C3725w3.javabm.w3class919
C3726w4.javabm.w4class1062
C3727x.javabm.xclass1107
C3731x3.javabm.x3class794
C3735y1.javabm.y1class421
C3736y2.javabm.y2class3777
C3741z1.javabm.z1class4272
C3742z2.javabm.z2class10538
C3743z3.javabm.z3class2516
C3744z4.javabm.z4class22908
EnumC3603c1.javabm.c1enum3874
EnumC3680p0.javabm.p0enum1170
EnumC3709u.javabm.uenum1735
ExecutorC3634h2.javabm.h2Executor1961
ExecutorC3666m4.javabm.m4Executor2095
ExecutorC3684p4.javabm.p4Executor3814
InterfaceC3596b0.javabm.b0interface162
InterfaceC3598b2.javabm.b2interface196
InterfaceC3692r0.javabm.r0interface137
InterfaceC3703t.javabm.tinterface805
InterfaceC3714u4.javabm.u4interface269
InterfaceC3715v.javabm.vinterface528
InterfaceC3721w.javabm.winterface430
InterfaceC3724w2.javabm.w2interface137
InterfaceC3730x2.javabm.x2interface355
InterfaceC3732x4.javabm.x4interface522
InterfaceC3733y.javabm.yinterface159
InterfaceC3737y3.javabm.y3interface211
InterfaceC3739z.javabm.zinterface251
InterfaceC3740z0.javabm.z0interface501
RunnableC3589a.java-Runnable38126
RunnableC3592a2.javabm.a2Runnable1336
RunnableC3612d4.javabm.d4Runnable1354
RunnableC3614e0.java-Runnable34723
RunnableC3618e4.javabm.e4Runnable1456
RunnableC3620f0.javabm.f0Runnable2797
RunnableC3633h1.javabm.h1Runnable1302
RunnableC3644j0.javabm.j0Runnable1344
RunnableC3657l1.javabm.l1Runnable1811
RunnableC3662m0.javabm.m0Runnable1234
RunnableC3663m1.java-Runnable13997
RunnableC3668n0.javabm.n0Runnable1113
RunnableC3681p1.javabm.p1Runnable2358
RunnableC3683p3.javabm.p3Runnable1737
RunnableC3686q0.java-Runnable23255
RunnableC3701s3.java-Runnable40676
RunnableC3729x1.javabm.x1Runnable3450
ScheduledExecutorServiceC3670n2.javabm.n2ScheduledExecutorService5729
$k — module Billing — 49 filesh5
disk filereal classkindbytes
AbstractC15820h.java-abstract class21073
C15806a.javad6.aclass4358
C15807a0.javad6.a0class6655
C15808b.javad6.bclass2254
C15810c.javad6.cclass13131
C15812d.javad6.dclass7110
C15814e.javad6.eclass4835
C15817f0.javad6.f0class1484
C15818g.javad6.gclass1186
C15821h0.javad6.h0class2413
C15822i.javad6.iclass7286
C15824j.java-class15074
C15825j0.javad6.j0class2097
C15826k.javad6.kclass2506
C15827k0.javad6.k0class3378
C15828l.javad6.lclass5367
C15829l0.javad6.l0class2356
C15830m.javad6.mclass2089
C15831m0.javad6.m0class3161
C15832n.javad6.nclass1405
C15833n0.java-class61059
C15834o.java-class28132
C15837p0.javad6.p0class1547
C15838q.javad6.qclass1614
C15839q0.javad6.q0class2197
C15840r.javad6.rclass9380
C15841r0.javad6.r0class860
C15843s0.javad6.s0class2367
C15849v0.java-class20439
C15850w.javad6.wclass1483
C15851w0.javad6.w0class818
C15852x.javad6.xclass2815
C15853x0.javad6.x0class10298
C15854y.javad6.yclass12567
C15855y0.javad6.y0class9114
RunnableC15815e0.javad6.e0Runnable11150
RunnableC15819g0.javad6.g0Runnable2507
RunnableC15835o0.javad6.o0Runnable17852
RunnableC15842s.javad6.sRunnable1960
RunnableC15845t0.javad6.t0Runnable7915
RunnableC15846u.javad6.uRunnable2502
RunnableC15847u0.javad6.u0Runnable2199
RunnableC15856z.javad6.zRunnable982
ViewOnClickListenerC15809b0.javad6.b0inner listener class7442
ViewOnClickListenerC15811c0.javad6.c0inner listener class4355
ViewOnClickListenerC15813d0.javad6.d0inner listener class8607
ViewOnClickListenerC15836p.javad6.pinner listener class2520
ViewOnClickListenerC15848v.java-inner listener class7649
ViewOnTouchListenerC15816f.java-inner listener class15849
$k — module Registration — 26 filesh5
disk filereal classkindbytes
AnimationAnimationListenerC21861p.javay5.pAnimationListener2132
C21848c.javay5.cclass7381
C21849d.javay5.dclass1105
C21850e.javay5.eclass6449
C21851f.java-class100901
C21853h.javay5.hclass1561
C21854i.javay5.iclass3044
C21856k.java-class146166
C21857l.javay5.lclass1951
C21860o.javay5.oclass2545
C21862q.javay5.qclass2821
C21863r.javay5.rclass10780
C21867v.java-class42860
C21868w.javay5.wclass6370
C21869x.javay5.xclass1029
EnumC21846a.javay5.aenum1442
GestureDetectorOnGestureListenerC21858m.javay5.mGestureListener7809
InterfaceC21847b.javay5.binterface422
InterfaceC21855j.javay5.jinterface280
InterfaceC21859n.javay5.ninterface597
InterfaceC21864s.javay5.sinterface269
InterfaceC21870y.javay5.yinterface140
InterfaceC21871z.javay5.zinterface140
RunnableC21852g.javay5.gRunnable3347
RunnableC21865t.javay5.tRunnable1616
RunnableC21866u.javay5.uRunnable2157
$k — module IdPlus — 22 filesh5
disk filereal classkindbytes
C16005f.javads.fclass4087
C16006g.javads.gclass26077
C16007h.javads.hclass2430
C16008i.javads.iclass6712
C16009j.javads.jclass2206
C16010k.javads.kclass9649
C16011l.javads.lclass10113
C16012m.javads.mclass3012
C16013n.javads.nclass1436
C16014o.javads.oclass53340
C16015p.javads.pclass6620
C16016q.javads.qclass6740
C16017r.javads.rclass3741
C16018s.javads.sclass13198
C16019t.javads.tclass95102
C16020u.javads.uclass13443
C16021v.javads.vclass4104
C16022w.javads.wclass1824
RunnableC16000a.javads.aRunnable6850
RunnableC16001b.javads.bRunnable10060
RunnableC16004e.java-Runnable38831
ViewOnClickListenerC16003d.javads.dinner listener class4420
$k — module Registration — 19 filesh5
disk filereal classkindbytes
C3507a.javab8.aclass8034
C3508b.javab8.bclass154957
C3509c.javab8.cclass10170
C3510d.javab8.dclass4842
C3511e.javab8.eclass5263
C3512f.javab8.fclass1254
C3513g.java-class25061
C3514h.javab8.hclass5548
C3515i.javab8.iclass9696
C3516j.javab8.jclass1717
C3517k.javab8.kclass5144
C3518l.javab8.lclass5987
C3519m.javab8.mclass3965
C3520n.javab8.nclass3694
C3521o.javab8.oclass6441
C3522p.javab8.pclass10301
C3523q.javab8.qclass4223
C3524r.javab8.rclass5790
C3525s.javab8.sclass5727
$k — module Registration — 52 filesh5
disk filereal classkindbytes
AbstractC17562a.javajd.aabstract class6003
AbstractC17567b1.javajd.b1abstract class3333
AbstractC17578h.javajd.habstract class4829
AbstractC17584k.javajd.kabstract class3372
C17563a0.javajd.a0class302
C17564a1.javajd.a1class2393
C17565b.javajd.bclass3143
C17566b0.javajd.b0class3998
C17568c.javajd.cclass12957
C17570d.javajd.dclass5310
C17571d0.javajd.d0class4144
C17572e.javajd.eclass6188
C17573e0.javajd.e0class6944
C17575f0.javajd.f0class1791
C17576g.javajd.gclass692
C17577g0.javajd.g0class14440
C17579h0.javajd.h0class8191
C17582j.javajd.jclass14954
C17583j0.javajd.j0class7178
C17585k0.javajd.k0class7107
C17586l.javajd.lclass4381
C17587l0.javajd.l0class1136
C17588m.javajd.mclass545
C17589m0.java-class33177
C17590n.javajd.nclass8068
C17591n0.javajd.n0class984
C17592o.javajd.oclass2607
C17593o0.javajd.o0class1872
C17594p.javajd.pclass2521
C17595p0.javajd.p0class6814
C17596q.javajd.qclass9400
C17597q0.javajd.q0class2177
C17598r.javajd.rclass851
C17599r0.javajd.r0class10592
C17601s0.javajd.s0class596
C17603t0.javajd.t0class563
C17604u.javajd.uclass2276
C17605u0.javajd.u0class30644
C17608w.javajd.wclass830
C17611x0.javajd.x0class2408
C17613y0.javajd.y0class4223
C17615z0.javajd.z0class3661
InterfaceC17569c0.javajd.c0interface787
InterfaceC17600s.javajd.sinterface330
InterfaceC17602t.javajd.tinterface805
InterfaceC17606v.javajd.vinterface255
InterfaceC17607v0.javajd.v0interface374
InterfaceC17609w0.javajd.w0interface322
InterfaceC17610x.javajd.xinterface301
RunnableC17581i0.javajd.i0Runnable1332
RunnableC17612y.javajd.yRunnable2222
RunnableC17614z.java-Runnable20572
$k — module Registration — 58 filesh5
disk filereal classkindbytes
AbstractC19642b.javaq7.babstract class3159
C19640a0.javaq7.a0class5399
C19641a1.javaq7.a1class871
C19644b1.javaq7.b1class13762
C19647c1.javaq7.c1class4093
C19650d1.javaq7.d1class1315
C19651e.javaq7.eclass1559
C19655f0.javaq7.f0class4016
C19657g.javaq7.gclass3493
C19663i.javaq7.iclass520
C19665i1.javaq7.i1class2679
C19666j.javaq7.jclass2529
C19667j0.javaq7.j0class4031
C19670k0.javaq7.k0class1628
C19673l0.java-class26321
C19674l1.javaq7.l1class10538
C19675m.javaq7.mclass2477
C19677n.javaq7.nclass14091
C19678n0.javaq7.n0class6722
C19681p.javaq7.pclass4236
C19682p0.javaq7.p0class1247
C19683q.javaq7.qclass516
C19684q0.javaq7.q0class135
C19685r.javaq7.rclass2967
C19686r0.javaq7.r0class3484
C19687s.javaq7.sclass3753
C19689t.javaq7.tclass4578
C19690t0.javaq7.t0class5042
C19692u0.javaq7.u0class1811
C19698x0.javaq7.x0class5884
C19699y.javaq7.yclass594
C19701z.javaq7.zclass6083
C19702z0.javaq7.z0class1933
DialogInterfaceOnClickListenerC19679o.javaq7.oDialog526
DialogInterfaceOnClickListenerC19680o0.javaq7.o0Dialog544
EnumC19653e1.javaq7.e1enum3581
EnumC19659g1.javaq7.g1enum802
EnumC19662h1.javaq7.h1enum1207
InterfaceC19639a.javaq7.ainterface275
InterfaceC19656f1.javaq7.f1interface226
InterfaceC19668j1.javaq7.j1interface1133
InterfaceC19696w0.javaq7.w0interface242
InterfaceC19700y0.javaq7.y0interface212
RunnableC19643b0.javaq7.b0Runnable9494
RunnableC19645c.javaq7.cRunnable2195
RunnableC19648d.javaq7.dRunnable1117
RunnableC19660h.javaq7.hRunnable3457
RunnableC19661h0.javaq7.h0Runnable3101
RunnableC19672l.javaq7.lRunnable6104
RunnableC19676m0.javaq7.m0Runnable1331
RunnableC19688s0.javaq7.s0Runnable1837
RunnableC19693v.javaq7.vRunnable5804
RunnableC19695w.javaq7.wRunnable236
RunnableC19697x.javaq7.xRunnable1239
ViewOnClickListenerC19646c0.javaq7.c0inner listener class7200
ViewOnClickListenerC19669k.javaq7.kinner listener class3087
ViewOnClickListenerC19691u.javaq7.uinner listener class316
ViewOnFocusChangeListenerC19664i0.javaq7.i0inner listener class2423
$k — module IdPlus — 16 filesh5
disk filereal classkindbytes
AbstractC19843n.java-abstract class77381
C19830a.javar4.aclass3191
C19832c.javar4.cclass1229
C19833d.java-class31177
C19835f.javar4.fclass9017
C19836g.javar4.gclass7127
C19837h.javar4.hclass28214
C19838i.javar4.iclass151
C19839j.javar4.jclass6091
C19840k.javar4.kclass8516
C19841l.javar4.lclass5830
C19844o.javar4.oclass817
C19845p.javar4.pclass4376
RunnableC19831b.javar4.bRunnable1732
RunnableC19834e.javar4.eRunnable5674
RunnableC19842m.javar4.mRunnable1333
$k — module IdPlus — 25 filesh5
disk filereal classkindbytes
AbstractC15918p.javadf.pabstract class4236
C15906d.javadf.dclass1663
C15907e.javadf.eclass11789
C15908f.javadf.fclass1266
C15909g.javadf.gclass1043
C15910h.javadf.hclass1879
C15911i.javadf.iclass2950
C15913k.javadf.kclass11145
C15914l.javadf.lclass1044
C15915m.javadf.mclass1510
C15917o.javadf.oclass22235
C15919q.javadf.qclass2501
C15920r.javadf.rclass1079
C15921s.javadf.sclass13058
C15922t.javadf.tclass1553
C15923u.javadf.uclass3614
C15924v.javadf.vclass14964
C15925w.javadf.wclass3915
C15926x.javadf.xclass9902
C15927y.javadf.yclass2242
C15928z.javadf.zclass4451
InterfaceC15903a0.javadf.a0interface142
RunnableC15905c.java-Runnable60956
ViewOnAttachStateChangeListenerC15916n.javadf.ninner listener class2842
ViewOnFocusChangeListenerC15902a.javadf.ainner listener class4878
$k — module Registration — 30 filesh5
disk filereal classkindbytes
AbstractC19534a0.javaq1.a0abstract class3505
AbstractC19535b.javaq1.babstract class478
AbstractC19538c0.javaq1.c0abstract class10004
BinderC19560w.javaq1.wBinder775
C19533a.javaq1.aclass3329
C19537c.java-class54912
C19540d0.javaq1.d0class5311
C19541e.javaq1.eclass774
C19543f.javaq1.fclass815
C19544g.javaq1.gclass951
C19545h.javaq1.hclass133
C19546i.javaq1.iclass4668
C19547j.javaq1.jclass1133
C19548k.javaq1.kclass2898
C19549l.javaq1.lclass5891
C19553p.javaq1.pclass558
C19558u.javaq1.uclass1324
C19559v.javaq1.vclass1938
C19561x.javaq1.xclass12553
C19562y.javaq1.yclass427
C19563z.javaq1.zclass540
CallableC19554q.javaq1.qCallable21771
InterfaceC19536b0.javaq1.b0interface765
InterfaceC19539d.javaq1.dinterface235
InterfaceC19550m.javaq1.minterface250
InterfaceC19551n.javaq1.ninterface252
InterfaceC19552o.javaq1.ointerface252
RunnableC19542e0.java-Runnable33197
ServiceConnectionC19557t.javaq1.tServiceConnection6595
ThreadFactoryC19556s.javaq1.sThreadFactory986
$k — module Billing — 13 filesh5
disk filereal classkindbytes
AbstractActivityC3498e.java-abstract class42971
C3495b.javab7.bclass1681
C3500g.javab7.gclass2432
C3502i.java-class40512
C3504k.java-class21183
C3506m.javab7.mclass1703
RunnableC3494a.javab7.aRunnable6692
RunnableC3496c.javab7.cRunnable1583
RunnableC3497d.java-Runnable22079
RunnableC3499f.javab7.fRunnable1481
RunnableC3501h.javab7.hRunnable1652
RunnableC3503j.javab7.jRunnable1795
RunnableC3505l.java-Runnable29144
$k — module Billing — 25 filesh5
disk filereal classkindbytes
AbstractC3871c.javac7.cabstract class8399
AbstractC3872d.javac7.dabstract class3470
AbstractC3873e.javac7.eabstract class592
C3869a.javac7.aclass4608
C3877i.javac7.iclass1610
C3878j.javac7.jclass15701
C3879k.javac7.kclass283
C3883o.javac7.oclass2892
C3886r.javac7.rclass12232
C3888t.javac7.tclass21772
C3889u.javac7.uclass1213
C3890v.javac7.vclass6638
C3891w.javac7.wclass1408
C3892x.javac7.xclass2964
C3894z.javac7.zclass2968
EnumC3876h.javac7.henum1775
RunnableC3870b.java-Runnable20920
RunnableC3882n.javac7.nRunnable2174
ViewOnClickListenerC3874f.javac7.finner listener class1513
ViewOnClickListenerC3875g.javac7.ginner listener class1318
ViewOnClickListenerC3880l.java-inner listener class20754
ViewOnClickListenerC3884p.javac7.pinner listener class1428
ViewOnClickListenerC3885q.javac7.qinner listener class1276
ViewOnClickListenerC3893y.javac7.yinner listener class1736
ViewOnLongClickListenerC3881m.javac7.minner listener class3521
$k — module Registration — 16 filesh5
disk filereal classkindbytes
C18487a.javam5.aclass12332
C18488b.javam5.bclass6443
C18489c.javam5.cclass5603
C18491e.java-class11869
C18493g.javam5.gclass34959
C18496j.javam5.jclass15199
C18498l.javam5.lclass5522
C18499m.javam5.mclass1152
C18501o.javam5.oclass1498
C18502p.javam5.pclass26377
C18503q.javam5.qclass3658
C18505s.javam5.sclass5139
InterfaceC18495i.javam5.iinterface228
RunnableC18490d.javam5.dRunnable1117
RunnableC18500n.javam5.nRunnable3746
RunnableC18504r.javam5.rRunnable2547
$k — module Billing — 29 filesh5
disk filereal classkindbytes
AbstractC17503r.javaj5.rabstract class13770
C17483a.javaj5.aclass1352
C17484a0.javaj5.a0class2211
C17485b.javaj5.bclass5318
C17487c.javaj5.cclass7295
C17488c0.javaj5.c0class3961
C17491f.javaj5.fclass3500
C17492g.javaj5.gclass9450
C17493h.javaj5.hclass930
C17495j.javaj5.jclass5300
C17496k.javaj5.kclass8186
C17498m.javaj5.mclass2700
C17500o.javaj5.oclass2110
C17501p.javaj5.pclass1129
C17502q.java-class18571
C17504s.javaj5.sclass473
C17506u.javaj5.uclass3940
C17507v.javaj5.vclass5817
C17510y.javaj5.yclass1450
C17511z.javaj5.zclass4105
EnumC17486b0.javaj5.b0enum1385
EnumC17505t.javaj5.tenum3008
RunnableC17508w.javaj5.wRunnable978
ViewOnClickListenerC17489d.javaj5.dinner listener class1716
ViewOnClickListenerC17490e.javaj5.einner listener class3511
ViewOnClickListenerC17494i.javaj5.iinner listener class3618
ViewOnClickListenerC17497l.javaj5.linner listener class5607
ViewOnClickListenerC17499n.javaj5.ninner listener class2052
ViewOnTouchListenerC17509x.javaj5.xinner listener class1754
$k — module Registration — 14 filesh5
disk filereal classkindbytes
AbstractC16507i.java-abstract class42027
AbstractC16508j.javaf8.jabstract class1912
C16500b.javaf8.bclass1506
C16501c.javaf8.cclass2884
C16502d.javaf8.dclass1758
C16503e.javaf8.eclass1097
C16505g.javaf8.gclass4834
C16510l.javaf8.lclass5901
C16511m.javaf8.mclass4431
C16512n.javaf8.nclass1816
CallableC16509k.javaf8.kCallable33176
ViewOnClickListenerC16499a.javaf8.ainner listener class1770
ViewOnTouchListenerC16506h.javaf8.hinner listener class1882
ViewTreeObserverOnGlobalLayoutListenerC16504f.javaf8.fclass16156
$k — module Registration — 40 filesh5
disk filereal classkindbytes
C21073b0.javavk.b0class1419
C21097j0.javavk.j0class1290
C21119u.javavk.uclass669
DialogInterfaceOnClickListenerC21121v.javavk.vDialog1867
RunnableC21069a.javavk.aRunnable1270
RunnableC21071a1.javavk.a1Runnable1728
RunnableC21072b.javavk.bRunnable1037
RunnableC21075c.javavk.cRunnable2537
RunnableC21077c1.javavk.c1Runnable1198
RunnableC21079d0.javavk.d0Runnable1390
RunnableC21080d1.javavk.d1Runnable1272
RunnableC21081e.javavk.eRunnable1195
RunnableC21082e0.java-Runnable30111
RunnableC21083e1.javavk.e1Runnable1367
RunnableC21084f.javavk.fRunnable1253
RunnableC21086f1.javavk.f1Runnable1688
RunnableC21087g.javavk.gRunnable1037
RunnableC21088g0.javavk.g0Runnable2751
RunnableC21091h0.javavk.h0Runnable1859
RunnableC21093i.java-Runnable33676
RunnableC21094i0.javavk.i0Runnable1228
RunnableC21096j.javavk.jRunnable1214
RunnableC21099k.javavk.kRunnable1319
RunnableC21103m.javavk.mRunnable1342
RunnableC21105n.javavk.nRunnable1399
RunnableC21107o.javavk.oRunnable1461
RunnableC21108o0.javavk.o0Runnable1228
RunnableC21110p0.javavk.p0Runnable1935
RunnableC21112q0.javavk.q0Runnable1256
RunnableC21113r.javavk.rRunnable1131
RunnableC21114r0.javavk.r0Runnable1253
RunnableC21115s.javavk.sRunnable1250
RunnableC21116s0.javavk.s0Runnable1400
RunnableC21117t.javavk.tRunnable1253
RunnableC21120u0.javavk.u0Runnable1325
RunnableC21126x0.javavk.x0Runnable1307
RunnableC21128y0.javavk.y0Runnable1848
RunnableC21129z.javavk.zRunnable1695
RunnableC21130z0.javavk.z0Runnable1134
ViewOnTouchListenerC21127y.javavk.yinner listener class1384
$k — module Billing — 30 filesh5
disk filereal classkindbytes
AbstractC21520b.javax.babstract class142
AbstractC21524d.javax.dabstract class1208
AbstractC21527e0.javax.e0abstract class1718
AbstractC21529f0.javax.f0abstract class2703
AbstractC21539m.javax.mabstract class19683
C21519a0.javax.a0class1516
C21521b0.javax.b0class875
C21523c0.javax.c0class4910
C21525d0.javax.d0class461
C21526e.javax.eclass1395
C21532h.javax.hclass2250
C21533h0.javax.h0class429
C21534i.javax.iclass4304
C21535i0.javax.i0class682
C21540n.javax.nclass1227
C21541o.javax.oclass1221
C21543q.javax.qclass2244
C21546t.javax.tclass974
C21548v.java-class35826
C21550x.javax.xclass925
CallableC21536j.javax.jCallable10762
EnumC21518a.javax.aenum1409
EnumC21530g.javax.genum2216
EnumC21531g0.javax.g0enum1600
EnumC21549w.javax.wenum1183
InterfaceC21522c.javax.cinterface138
InterfaceC21547u.javax.uinterface155
InterfaceC21551y.javax.yinterface170
InterfaceC21552z.javax.zinterface4950
RunnableC21545s.javax.sRunnable2281
$k — module IdPlus — 16 filesh5
disk filereal classkindbytes
C16918a.javah6.aclass3595
C16919b.javah6.bclass5640
C16920c.javah6.cclass5795
C16924g.javah6.gclass15181
C16925h.javah6.hclass4697
C16926i.javah6.iclass2133
C16927j.javah6.jclass4884
C16930m.javah6.mclass2261
C16931n.javah6.nclass1063
C16932o.javah6.oclass1063
C16933p.javah6.pclass10054
C16934q.javah6.qclass1524
C16935r.javah6.rclass12626
C16936s.javah6.sclass8528
ViewOnClickListenerC16928k.javah6.kinner listener class7960
ViewOnClickListenerC16929l.javah6.linner listener class2500
$k — module IdPlus — 22 filesh5
disk filereal classkindbytes
AbstractC19051d.javao4.dabstract class6569
AbstractC19054g.java-abstract class23767
AbstractC19055h.javao4.habstract class738
AbstractC19058k.javao4.kabstract class212
C19052e.javao4.eclass507
C19053f.javao4.fclass780
C19057j.javao4.jclass3810
C19061n.javao4.nclass1822
C19062o.javao4.oclass2029
C19063p.javao4.pclass1960
C19064q.javao4.qclass8212
C19066s.javao4.sclass4162
C19067t.javao4.tclass469
C19068u.javao4.uclass7338
C19069v.javao4.vclass5524
C19070w.javao4.wclass2056
C19071x.javao4.xclass3355
InterfaceC19056i.javao4.iinterface501
RunnableC19059l.javao4.lRunnable4374
RunnableC19065r.javao4.rRunnable423
ViewOnClickListenerC19049b.javao4.binner listener class2064
ViewOnTouchListenerC19050c.javao4.cinner listener class2243
$k — module IdPlus — 5 filesh5
disk filereal classkindbytes
AbstractActivityC3899e.java-abstract class46623
C3897c.javac8.cclass7837
C3898d.javac8.dclass1508
RunnableC3896b.javac8.bRunnable1884
ViewOnClickListenerC3895a.javac8.ainner listener class22759
$k — module IdPlus — 11 filesh5
disk filereal classkindbytes
AbstractC16137c.java-abstract class50358
AbstractC16140f.javae8.fabstract class6625
AbstractC16143i.javae8.iabstract class1786
C16136b.javae8.bclass7690
C16138d.javae8.dclass3377
C16139e.javae8.eclass3323
C16141g.javae8.gclass2011
C16142h.javae8.hclass1087
EnumC16135a.javae8.aenum1000
EnumC16144j.javae8.jenum900
EnumC16145k.javae8.kenum967
$k — module Billing — 14 filesh5
disk filereal classkindbytes
C21282b.javaw7.bclass1672
C21283c.javaw7.cclass2092
C21284d.javaw7.dclass1606
C21289i.java-class32705
C21293m.javaw7.mclass6310
C21294n.javaw7.nclass6603
C21295o.javaw7.oclass13442
C21296p.javaw7.pclass1614
DialogC21290j.javaw7.jDialog2444
EnumC21291k.javaw7.kenum1043
EnumC21292l.javaw7.lenum495
RunnableC21281a.javaw7.aRunnable4540
RunnableC21285e.javaw7.eRunnable3732
RunnableC21288h.javaw7.hRunnable1031
$k — module Billing — 12 filesh5
disk filereal classkindbytes
AbstractC16699a.javag5.aabstract class2815
C16702d.javag5.dclass13592
C16703e.javag5.eclass2012
C16706h.javag5.hclass5994
C16707i.java-class21983
C16708j.javag5.jclass5352
C16710l.javag5.lclass4359
InterfaceC16709k.javag5.kinterface277
RunnableC16700b.javag5.bRunnable1395
RunnableC16701c.javag5.cRunnable9515
RunnableC16704f.javag5.fRunnable1512
RunnableC16705g.javag5.gRunnable1628
$k — module Billing — 4 filesh5
disk filereal classkindbytes
C16125a.javae6.aclass2927
C16127c.javae6.cclass1670
C16128d.java-class41191
RunnableC16126b.java-Runnable19471
$k — module Billing — 11 filesh5
disk filereal classkindbytes
C3842a.javac5.aclass11897
C3844c.javac5.cclass1208
C3845d.javac5.dclass3566
C3847f.javac5.fclass5438
C3851j.javac5.jclass3533
C3852k.javac5.kclass150
DialogInterfaceOnKeyListenerC3843b.javac5.bDialog1932
RunnableC3848g.javac5.gRunnable1308
RunnableC3849h.javac5.hRunnable3622
RunnableC3850i.javac5.iRunnable10183
ViewOnClickListenerC3846e.java-inner listener class20962
$k — module Registration — 9 filesh5
disk filereal classkindbytes
AbstractC0149e.javaaj.eabstract class1281
AbstractC0153i.javaaj.iabstract class1816
C0145a.javaaj.aclass7059
C0146b.javaaj.bclass3522
C0147c.javaaj.cclass16143
C0148d.javaaj.dclass18825
C0150f.javaaj.fclass9235
C0151g.javaaj.gclass1576
C0152h.javaaj.hclass2417
$k — module Billing — 16 filesh5
disk filereal classkindbytes
AbstractC20320g.javat4.gabstract class1000
AbstractC20321h.javat4.habstract class645
AbstractC20328o.javat4.oabstract class6557
AbstractC20330q.javat4.qabstract class6114
C20315b.javat4.bclass1152
C20316c.javat4.cclass1941
C20318e.javat4.eclass2951
C20319f.javat4.fclass4648
C20322i.javat4.iclass2028
C20324k.javat4.kclass223
C20325l.javat4.lclass1489
C20326m.javat4.mclass3935
C20327n.javat4.nclass9549
C20329p.javat4.pclass3573
EnumC20314a.javat4.aenum2001
RunnableC20323j.javat4.jRunnable6845
$k — module Billing — 1 filesh5
disk filereal classkindbytes
C18416a.java-class45646
$k — module Billing — 2 filesh5
disk filereal classkindbytes
C17064a.javahh.aclass6115
C17065b.java-class38052
$k — module Registration — 7 filesh5
disk filereal classkindbytes
C18297c.javal5.cclass3449
C18298d.javal5.dclass9049
C18299e.javal5.eclass3311
C18302h.javal5.hclass11023
RunnableC18295a.javal5.aRunnable1830
RunnableC18300f.javal5.fRunnable3593
RunnableC18301g.javal5.gRunnable9746
$k — module IdPlus — 6 filesh5
disk filereal classkindbytes
AbstractC19568c.javaq4.cabstract class948
AbstractC19571f.javaq4.fabstract class1099
C19566a.javaq4.aclass8385
C19567b.javaq4.bclass6065
C19569d.javaq4.dclass10594
C19570e.javaq4.eclass5649
$k — module Registration — 8 filesh5
disk filereal classkindbytes
C19888d.javar7.dclass334
C19889e.javar7.eclass2529
C19891g.javar7.gclass2849
C19892h.javar7.hclass1557
RunnableC19885a.javar7.aRunnable2474
RunnableC19886b.javar7.bRunnable7462
ViewOnClickListenerC19890f.javar7.finner listener class2749
ViewOnTouchListenerC19887c.javar7.cinner listener class10895
$k — module Registration — 7 filesh5
disk filereal classkindbytes
C18306d.javal6.dclass10727
C18308f.javal6.fclass1196
C18309g.javal6.gclass4445
EnumC18303a.javal6.aenum3678
EnumC18310h.javal6.henum6465
InterfaceC18305c.javal6.cinterface209
ViewOnClickListenerC18307e.javal6.einner listener class3647
$k — module Billing — 1 filesh5
disk filereal classkindbytes
C21940a.java-class27261
$k — module PhoneAccount — 5 filesh5
disk filereal classkindbytes
C16961a.javahd.aclass10901
C16962b.javahd.bclass9942
C16963c.javahd.cclass586
C16964d.javahd.dclass475
C16965e.javahd.eclass2433
$k — module IdPlus — 6 filesh5
disk filereal classkindbytes
C16914c.javah5.cclass3289
C16915d.javah5.dclass1424
C16916e.javah5.eclass13342
InterfaceC16917f.javah5.finterface233
RunnableC16913b.javah5.bRunnable2407
ViewOnClickListenerC16912a.javah5.ainner listener class2247
$k — module IdPlus — 5 filesh5
disk filereal classkindbytes
C17152a.javai6.aclass5734
C17153b.javai6.bclass2831
C17154c.javai6.cclass1988
C17155d.javai6.dclass6922
ViewOnClickListenerC17156e.javai6.einner listener class4743
$k — module Registration — 2 filesh5
disk filereal classkindbytes
C21845b.javay4.bclass8867
RunnableC21844a.javay4.aRunnable3973
$k — module Billing — 6 filesh5
disk filereal classkindbytes
AbstractC16130b.javae7.babstract class3080
AbstractC16134f.javae7.fabstract class559
C16132d.javae7.dclass5595
InterfaceC16131c.javae7.cinterface203
InterfaceC16133e.javae7.einterface244
RunnableC16129a.javae7.aRunnable3653
$k — module Billing — 9 filesh5
disk filereal classkindbytes
C3814a.javac0.aclass1432
C3815b.javac0.bclass1620
C3816c.javac0.cclass706
C3817d.javac0.dclass1399
C3818e.javac0.eclass4712
C3820g.javac0.gclass686
C3821h.javac0.hclass590
C3822i.javac0.iclass1417
InterfaceC3819f.javac0.finterface450
$k — module Billing — 1 filesh5
disk filereal classkindbytes
AbstractC0037a.javaa5.aabstract class11590
$k — module Registration — 3 filesh5
disk filereal classkindbytes
C22071c.javaz4.cclass4834
RunnableC22069a.javaz4.aRunnable2546
RunnableC22070b.javaz4.bRunnable2785
$k — module Registration — 5 filesh5
disk filereal classkindbytes
C18506a.javam6.aclass402
C18509d.javam6.dclass2741
C18510e.javam6.eclass3861
InterfaceC18508c.javam6.cinterface215
ViewOnClickListenerC18507b.javam6.binner listener class1855
$k — module Billing — 4 filesh5
disk filereal classkindbytes
C17729a.javak0.aclass6538
C17730b.javak0.bclass604
C17731c.javak0.cclass1393
C17732d.javak0.dclass724
$k — module Registration — 3 filesh5
disk filereal classkindbytes
C3910a.javace.aclass939
C3911b.javace.bclass735
C3912c.javace.cclass3625
$k — module PhoneAccount — 3 filesh5
disk filereal classkindbytes
C17789a.javak7.aclass1787
C17790b.javak7.bclass2705
C17791c.javak7.cclass588
$k — module IdPlus — 3 filesh5
disk filereal classkindbytes
C17512a.javaj6.aclass530
C17513b.javaj6.bclass1466
ViewOnClickListenerC17514c.javaj6.cinner listener class1968
$k — module Billing — 5 filesh5
disk filereal classkindbytes
InterfaceC19944a.javarj.ainterface348
InterfaceC19945b.javarj.binterface326
InterfaceC19946c.javarj.cinterface327
InterfaceC19947d.javarj.dinterface287
InterfaceC19948e.javarj.einterface287
$k — module Registration — 1 filesh5
disk filereal classkindbytes
AbstractC16045a.javae.aabstract class907

10. Appendix B — Package-by-package detail with key classesh4

Format per package: role, then the classes that matter (real name, kind, purpose, key methods, endpoints/tables/pref-keys, strings/secrets). Classes that are pure listener/Runnable/anonymous shims are summarised in one line each; Appendix A lists all of them exhaustively.

10.1 q7 (p494q7, 58 files / 204 KB) — Registration controller layer — APP CODEh5

The heart of registration. All registration network flows land in one of these callback objects and are switched on by an int discriminator.

real classkindpurpose / key members
q7.l0 = C19673l0final class, 3 ctorsRegistrationActivity callbacks. Fields int f50643f (discriminator), RegistrationActivity f50644g. Methods: void mo415j(boolean), void mo308k() (error), void mo309m() (success). case 4 in mo309m() = the clientId write site (§4.3). Discriminator values seen: 0 short-onboarding-contacts-done, 1 avatar picked, 2 code-validation failure, 3 code-validation failure (second variant), 4 join success / join failure, 5 social profile loaded, 6 short-onboarding core ready, 7 contacts prepared, 8 1 s handler ping, 9 cancel pending activity, 10/11 finish flags.
q7.n = C19677nfinal classPhoneNumberValidationActivity callbacks. case 2 in mo309m() = second clientId write site (§4.3). case 2 in mo308k() = failure (-123 special-cased to m10468E0).
q7.i1 = C19665i1final classPer-Activity registration state (§4.3 step 1). Field int f50625b = 30000 (default timeout). void m36102a(String regId), void m36103b(int).
q7.b1 = C19644b1class, 13.7 KBJoin request context; field f50539o is the svr value posted to join.jsp.
q7.c1 = C19647c1classcli_auth/gettransport callback (AbstractC16130b subclass); f44796b switch int, error keys V_ERROR_STATUS, V_CLI, V_TIMEOUT, V_SMS_MESSAGE, V_CODE.
q7.e1 = EnumC19653e1enum, 41 constantsThe whole registration error taxonomy (§4.5). Notable: JOIN_RESPOND_CODE_200_WITHOUT_CID(-16), CLIENT_FORMATTED_CLI_EMPTY(-13), SERVER_ERR_CANT_VALIDATE(9), SERVER_ERR_ALREADY_VALID(10), SERVER_COUNTRY_NOT_SUPPORTED(101), SERVER_DELETE_ME_OVER_LIMIT(102), SERVER_BANNED_BY_TRAFFIC_ANALYZER(103). Note the duplicated names SERVER_ERR_UNSUPPORTED_FLOW at −6, −3, 2, 5, 7 and SERVER_ERR_CLI_IS_NOT_VALID at 1 and 3 — obfuscation artefacts; EnumC19653e1.m36099b(int) is the value→constant lookup.
q7.g1 = EnumC19659g1enum, 15 constants”verb” for every registration network call: JOIN(0), GET_ACTION(1), MO_TIMEOUT(2), MO_NOTIFICATION(3), MO_SENT_SMS(4), MO_START(5), GENERATE_CLI(6), IS_CODE_VALID(7), PHONE_AND_COUNTRY_CHECK(8), SO_TIMEOUT(9), GET_TRANSPORT(10), VALIDATION_TOKEN(11), FACEBOOK_LOGIN(12), GOOGLE_LOGIN(12).
q7.h1 = EnumC19662h1enum, 8 constantsCLI validation transport, sent as the tr= query parameter: MO_SMS("mo_sms"), SO_SMS("so_sms"), SO_CALL("so_call"), SO_FLASH("so_flash"), HUAWEI("huawei"), FB("fb"), SOCIAL_LOGIN("social_login"), AUTO("auto"). Has a fromString (m36101a).
q7.f1 = InterfaceC19656f1interfaceint mo36098a() — the error-code accessor used by every failure branch.
q7.d/q7.j1/q7.l1/q7.k/q7.n0/q7.o/q7.p/q7.q/q7.r/q7.s/q7.t/q7.u/q7.v/q7.w/q7.x/q7.y/q7.z/q7.z0miscRegistration UI plumbing: photo picking (ViewOnClickListenerC19646c0), focus handling (ViewOnFocusChangeListenerC19664i0), dialogs (DialogInterfaceOnClickListenerC19679o/80o0), runnables for the 1 s ping / delayed transitions, C19698x0, C19699y, C19701z, C19702z0.
q7.b = AbstractC19642b, q7.h0=InterfaceC19668j1, q7.j1=InterfaceC19668j1 hostabstract / interfaceAbstractC16130b host interface (InterfaceC19668j1) is what q7.i1 casts to AppCompatActivity.
10.2 jd (p365jd, 52 files / 268 KB) — join.jsp + cli_auth/gettransport — APP CODEh5
real classkindpurpose
jd.z = RunnableC17614zsynthetic Runnable, 20.5 KBThe single most important class in this cluster. case 1 = join.jsp; default = cli_auth/gettransport; case 0 = network-failure fan-out for jd.b0. Full detail in §4.4.
jd.b0 = C17566b0classFailure payload {f44269a, f44270b} handed to mo33684f(...) in jd.z case 0.
jd.c0 = InterfaceC17569c0interfaceSuccess/failure listener pair for jd.b0.
jd.m0/jd.u0/jd.c/jd.d/jd.eclassesResponse models for join.jsp / cli_auth payloads (synthetic accessors generated by the obfuscator).
jd.s = C17598r, jd.r = C17598rinterface/classError/response pair for jd.b0.
jd.j = C17582jclass, 15 KBReflection-based ExoPlayer media-source factory — Class.forName("com.google.android.exoplayer2.source.dash.DashMediaSource$Factory"), .hls., .smoothstreaming., .rtsp.. This is media glue, misfiled as Registration.
jd.k0, jd.k, jd.l, jd.l0, jd.n, jd.n0, jd.o, jd.o0, jd.p, jd.p0, jd.q, jd.q0, jd.r0, jd.s0, jd.t0, jd.t, jd.v, jd.v0, jd.w, jd.w0, jd.x, jd.x0, jd.y, jd.z0, jd.a, jd.a0, jd.a1, jd.b, jd.b1, jd.e0, jd.f0, jd.g, jd.g0, jd.h, jd.h0, jd.i0, jd.j0classesResponse DTOs + coroutine/Runnable plumbing for the two endpoints.
10.3 r5 (p516r5, 36 files / 696 KB) — cli_auth/checkcode + contact models — APP CODEh5

(Not in the C2 TSV but it is the cli_auth/checkcode client the briefing names; documented here.)

real classkindpurpose
r5.s = RunnableC19875sRunnable, 10.8 KBcase 1 = cli_auth/checkcode (§4.3 step 3). case 0 = block-contact confirm dialog. default = “DynamicAreaServerDa..” cards-update throttle (cards_update_threshold, SP_KEY_LAST_TIME_REQUESTED_CARDS).
r5.aclass, 9.2 KBRegistration/profile helper.
r5.b, r5.c, r5.c0, r5.d, r5.d0, r5.e0, r5.f, r5.f0, r5.g, r5.g0, r5.h, r5.h0, r5.j, r5.j0, r5.l, r5.m, r5.n, r5.o, r5.p, r5.u, r5.w, r5.x, r5.y, r5.vclassesContact DTOs (r5.i = 46 KB, r5.i0 = 43 KB — the big contact/DB models), EnumC19861h0 (9.9 KB enum), coroutine plumbing.
r5.e = C19854eclass, 395 KBGiant DTO/serialisation mega-class. Single largest file in the whole C2 + C3 region.
r5.v(= InterfaceC19878v)interfacemarker.
r5.q = RunnableC19873q, r5.r=RunnableC19874rRunnablesRunnableC19874r GETs updateuserdetails.jsp.
r5.i0 = C19863i0class, 43 KBSocial-profile deep-linking (opens http://m.facebook.com/profile.php?id=, http://instagram.com/_u/…, http://new.vk.com/id…).
10.4 g5 (p303g5, 12 files / 69 KB) — Play Billing engine — APP CODEh5
real classkindpurpose
g5.i = C16707ifinal class, 21.9 KB, singleton f41454hTAG "EyeconBilling". Implements q1.InterfaceC19552o (onPurchasesUpdated) and q1.InterfaceC19539d (onBillingSetupFinished, onBillingServiceDisconnected). Constants f41455i = "CB_KEY_OWNED_PURCHASE", f41456j = "CB_KEY_OWNED_PRODUCT", f41457k = TimeUnit.DAYS.toMillis(30). Methods: m32521b, m32522c, m32523d, m32524e, m32525i(int) (response-code → name), m32526k(Context,String), m32527n(String) (ISO-8601 period → days), m32529f, m32530g, m32531h, m32532j, m32533l, m32534m, m32535o, m32536p, m32537q.
g5.j = C16708jclassProduct descriptor: (productId f41470e, productType f41472g) + price f41466a, subPeriod f41469d, subDiscountPeriod f41468c, subDiscountPrice f41476k, currencyCode f41467b, microsPrice f41471f, purchaseToken f41473h, purchaseState f41475j, originalJson f41474i. m32538a(JsonObject) (from cache), m32539b() (to product JSON), m32540c() (to purchase-info JSON). equals/hashCode on productId only.
g5.l = C16710lclass extends g5.jWraps a live com.android.billingclient.api.Purchase: ctor reads purchase.m4425a() (state), purchase.f5127c.optString("token", optString("purchaseToken")), purchase.f5125a (products), optLong("purchaseTime").
g5.d = C16702dclassqueryPurchasesAsync("inapp"/"subs") → List<C16710l>; logs "queryPurchases return with responseCode = …".
g5.h = C16706hclass extends AbstractC16130bBulk acknowledge with retry.
g5.c = RunnableC16701cRunnableJoins the inapp+subs queries.
g5.b = RunnableC16700bRunnablem32522c(includeSubs, callback).
g5.e = C16703e, g5.f=C16702d cb, g5.k=InterfaceC16709kinterface/classqueryProductDetails callback; listener registration for m32535o.
g5.a = AbstractC16699aabstract classcallback base.
10.5 j5 (p358j5, 29 files / 122 KB) — entitlement + premium UI — APP CODEh5
real classkindpurpose
j5.k = C17496kfinal class, singleton f43985e, TAG "PaidPremium"The paid-premium entitlement cache (§7.5). Reads SP_KEY_LAST_PREMIUM_UPDATE_CACHE, SP_KEY_PREMIUM_OWN_CACHE, SP_KEY_PREMIUM_PURCHASES_INFO_CACHE. m33586b(String productId) = membership test against remote-config premium_products_ids_list. m33587c(cb), m33588e(long, String) (pushes premium_cache_product/premium_cache_time to the server), m33589a(Boolean), m33590d(C16708j, long).
j5.c = C17487cfinal class, singleton f43961e, TAG "FreePremium"Ad/virality premium (§6.3). Prefs SP_KEY_LAST_DATE_FOR_VIRAL_PREMIUM, SP_KEY_LAST_DATE_FOR_VIDEO_PREMIUM, SP_KEY_LAST_DATE_FOR_FREE_PREMIUM, SP_KEY_LAST_FREE_PREMIUM_TYPE, SP_KEY_MY_VIRAL_ID, SP_KEY_LAST_TIME_REQUESTED_CARDS.
j5.b = C17485bclass, TAG "FreePremium"getViralLink / updateViralStatus (virality/viral_id, virality/premium_status, virality/viral_id_check). Persists SP_KEY_MY_VIRAL_ID.
j5.a = C17483aRunnableupdateVideoPremium_#onSuccess/#onFailed.
j5.q = C17502qclassPremiumFeatureDialog — loads products (g5.i.m32534m), renders the plan list; drives C3502i case 13.
j5.r = AbstractC17503rabstract class, 13.7 KBThe app-wide prefs accessor: m33607k(key, def), m33613q(...), m33614r(String). Used by the Eyecon.getStringSP JS bridge.
j5.p = C17487c UI, j5.o=C17495j, j5.k0=C17488c0 (WeakRef holder for PremiumPurchasingDefaultActivity), j5.f=C17491f, j5.g=C17492g, j5.h=C17493h, j5.i=C17495j, j5.j=C17496k…classesRewarded-ad → free-premium grant wiring (onRewarded, onUserEarnedReward, onAdDismissedFullScreenContent).
j5.s,j5.t,j5.u,j5.v,j5.w,j5.x,j5.y,j5.z, EnumC17505t, EnumC17486b0classes/enumsFree-premium Activity/Fragment glue.
10.6 q1 (p489q1, 30 files / 173 KB) — Play Billing service proxy — APP CODE (thin wrapper over Play BillingClient)h5
real classkindpurpose
q1.c = C19537cclass, 54.9 KB, extends q1.bBillingClient facade. Methods: mo35953a()(build), mo35959a()(endConnection), mo35960b(C0162c, q1.n)(queryPurchasesAsync), mo35961c(q1.d)(startConnection), mo35969d(C0162c, …)(acknowledgePurchase), mo35971f(Activity, C19543f)(launchBillingFlow), mo35972g(C18622a, …)(queryProductDetailsAsync), m35970e()(ready), m35973h("inapp",…), m35977n/m35978o/m35988z/m35966B (logging), m35978z. Line 180: Class.forName("com.android.billingclient.ktx.BuildConfig").getField("VERSION_NAME") — reflection on an optional class (would throw ClassNotFoundException if the ktx artifact is absent).
q1.a = C19533aclassBillingClient.Builder. Field f50131a is the purchases-updated listener (the app passes new y5.f(23)), f50132b = billing listener.
q1.b = AbstractC19535b, q1.c0=AbstractC19538c0, q1.a0=AbstractC19534a0abstractlog/error constants.
q1.d = C19540d0, q1.i, q1.j, q1.k, q1.l, q1.p, q1.y, q1.z, q1.d0, q1.xclassesServiceConnectionC19557t, ThreadFactoryC19556s, BinderC19560w, CallableC19554q, RunnableC19542e0 (33 KB), RunnableC19554q (21.8 KB) — the async plumbing that calls com.google.android.gms.internal.play_billing.zz* protobufs.
q1.e0=C19543f, q1.f=C19544g, q1.g=C19546i, q1.h=C19547j, q1.i=C19548k, q1.l=C19549l, q1.m=C19553p, q1.o=InterfaceC19552o, q1.n=InterfaceC19551n, q1.d=DTOs/interfacesBillingFlowParams, BillingResponse, ProductDetailsResponse, ProductDetails, offer DTOs, QueryProductDetailsParams.Product.
10.7 e7 (p262e7, 6 files / 13 KB) — the callback base + the Eyecon bridge — APP CODEh5
real classkindpurpose
e7.d = C16132dfinal class, the Eyecon JS bridge§8.2 — 19 @JavascriptInterface methods.
e7.b = AbstractC16130babstract classThe universal async-callback base used by every network flow in the app (registration, billing, store, remote config). Fields Handler c, boolean d, boolean e; ctors (), (boolean), (boolean, long timeout). m31515h() = error, m31516i() = success, m31518o(Object) = set DEFAULT_RESULT.
e7.a = RunnableC16129aRunnableTimeout dispatch for e7.b.
e7.c = InterfaceC16131c, e7.e = InterfaceC16133einterfacescallback markers.
e7.f = AbstractC16134fabstract classsecondary callback base.
10.8 b7 (p034b7, 13 files / 171 KB) — base Activity — APP CODEh5

b7.e = AbstractActivityC3498e (42.9 KB) is the base class of every MICO Activity (RegistrationActivity, PhoneNumberValidationActivity, StoreActivity, WebviewPaywallActivity, c8.e, …). Line 581–583: on (re)create it calls C17496k.f43985e.getClass(); C17496k.m33587c(null); → refreshes the paid-premium entitlement. Contains RunnableC3497d, C3495b–C3506m, RunnableC3501h, RunnableC3503j, RunnableC3505l (29 KB, OnBackPressed/animation), RunnableC3497d (22 KB, shared event dispatcher), RunnableC3494a, C3500g.

10.9 c7 (p047c7, 25 files / 140 KB) — dialog framework — APP CODEh5
  • c7.j = C3878j — the app’s generic AlertDialog descriptor (title f4339f, message f4340g, positive f4344k, EnumC17169g f4345l colour theme, Runnable f4346m, cancel f4349p, cancel Runnable f4352s, colour f4351r, code f4340g/f4359z). Used for the store errors ("store_error_dialog"), block-contact ("blockContactDialog"), etc.
  • c7.t = C3888t (21.7 KB) extends c7.d — premium feature/purchase dialog.
  • c7.c/c7.d — base DialogFragment classes; c7.l = ViewOnClickListenerC3880l (20.9 KB) — profile UI incl. the cleartext privacy-policy link http://www.eyecon-app.com/docs/private_policy.html.
10.10 w7 (p621w7, 14 files / 77 KB) — premium purchase dialogs — APP CODEh5

w7.i = C21289i (32.7 KB) extends c7.d — the multi-network purchase sheet; owns its own WebView (C5846m case 6), network selector EnumC21291k (incl. TIKTOK, Amazon, FB, AdMob), DialogC21290j, and the share link templating ("[xx]" → http://tinyurl.com/eyecon-app).

10.11 x (p640x, 30 files / 107 KB) — Lottie/animation orchestration — APP CODEh5

x.v = C21548v (35.8 KB) extends Drawable implements Drawable.Callback, Animatable; x.m = AbstractC21539m (19.7 KB); x.j = CallableC21536j; x.z = InterfaceC21552z; enums x.g,x.g0,x.a,x.a0. Drives Choreographer frame callbacks for premium-screen animation.

10.12 c0 (p040c0, 9 files / 13 KB), k0 (p376k0, 4 files / 9 KB)h5

Small animation/Lottie helpers used by x: c0.e,c0.b,c0.a,c0.i,c0.d,c0.c,c0.g,c0.h, InterfaceC3819f; k0.a(C17729a), k0.c,k0.d,k0.b.

10.13 lo (p410lo, 1 file / 45 KB) — lo.a = C18416a — APP CODE, god utility classh5

public final class C18416a — used by ~every package. Members observed: m34578o/m34579p/m34582s/m34586w/m34588y (log at 4 levels), m34580q(ArrayList<C21872a>) (query-string builder), m34580q(ArrayList, String), m132t(int, String, String), m119j, m122j, m129q(StringBuilder, String, String) (URL join), m176Y, m173V, m34744e (bitmap-from-path), m463n0, m39065f, m39334e. No secrets.

10.14 a5 (p006a5, 1 file / 11 KB) — a5.a = AbstractC0037ah5

Static utility facade used across billing/registration: m112C(Object, String key, Object value) (= the “write prefs” helper — this is what writes SP_KEY_INVITER_ID from the referrer flow), m129q URL join, m132t, m122j, m119j.

10.15 h6/h5/i6/j6 (p314h6,p313h5,p339i6,p359j6) — IdPlus screens — APP CODEh5
  • h6.g = C16924g (15.2 KB) extends f8.i — the Notification-Reader premium-gated screen.
  • h6.r = C16935r extends AdListener; h6.p = C16933p extends AbstractC16130b; h6.s = C16936s extends ConstraintLayout; h6.k = ViewOnClickListenerC16928k.
  • h5.e = C16916e (13.3 KB) extends c7.k — the premium upsell inside Notification Reader; h5.c = C16914c implements g5.k (queryProductDetails listener); h5.d = C16915d extends y5.v.
  • i6.d=C17155d (ConstraintLayout), i6.a=C17152a (RecyclerView.Adapter) — Telegram list UI.
  • j6.b=C17513b, j6.a=C17512a, j6.c= click listener — Telegram window click wiring.
10.16 o4 (p448o4, 22 files / 81 KB) — IdPlus + advertising ID — APP CODEh5
  • o4.i = InterfaceC19056i — static holder f48663a = new o4.j().
  • o4.j = C19057j — the advertising-ID provider: m35361a() uses com.google.android.gms.ads.identifier.AdvertisingIdClient.getAdvertisingIdInfo(app).getId(), caches it in prefs Eyecon.SP_KEY_ADVID, and has a main-thread fallback that returns the cached value while logging "AdsIds", "getAdvertisingId, can't get the adid using API because you run this on the main thread and it can creates deadlock". Also m35362b() (remote config home_feed_google_admob_banner_unit_ids), m35363c() (incall_feed_middle_ad_enabled / incall_feed_middle_gam_ad_id).
  • o4.g = AbstractC19054g (23.8 KB), o4.q, o4.u, o4.d, o4.v, o4.l, o4.h, o4.k, o4.i.
10.17 r4 (p515r4), t4 (p553t4), q4 (p491q4), ds (p252ds), e8 (p263e8) — ads/FB — APP CODEh5
  • r4.n = AbstractC19843n (77.4 KB — a big constant/table class); r4.d = C19833d extends AdListener implements OnPaidEventListener, AppEventListener; r4.h,r4.f,r4.g,r4.k,r4.l,r4.m,r4.n, r4.p,RunnableC19834e. AdMob native/preload loader.
  • t4.* — AdMob Preload (PreloadCallbackV2) + AdManagerAdRequest wrapper.
  • q4.* — Facebook Audience Network native ads (com.facebook.ads.NativeAd, AdError).
  • ds.* — 5 coroutine builders extends AbstractC21478j implements InterfaceC16320o plus RunnableC16004e — mediation orchestration (Mbridge/Crackle/TikTok/Amazon bid adapters).
  • e8.* — Facebook App Events / Graph JSON helpers (AbstractC16137c is the premium-owns registry: C17151a.f42620b.m32991a(productId), SP_KEY_INAPP_PRODUCTS_CACHE).
10.18 vk (p607vk, 40 files / 115 KB) — network event dispatchers — APP CODE + SDK glueh5

40 near-identical small Runnables. The two that matter:

  • RunnableC21082e0 (30.1 KB) — GETs getoptionalpics.jsp, social_auth/use_cli_validation (logged as "join.jsp"), updateuserdetails.jsp, resetupdatedstatus.jsp.
  • RunnableC21093i (33.7 KB) — GETs getupdatedcontactsinfo.jsp.
  • RunnableC21088g0 — GETs sethistory.jsp; RunnableC21129z — POSTs eventreport.jsp.
  • C21119u = vk.u — GoogleApiClient.OnConnectionFailedListener → InMobi. (com.inmobi.media.* imports throughout = SDK glue, misfiled as Registration.)
10.19 m5 (p418m5, 16 files / 134 KB) — telephony/identity — APP CODEh5
  • m5.p = C18502p (26.4 KB) singleton f46692k. Method m34691f() = collect TelephonyManager.getDeviceId() over up to 3 SIM slots via {"getDeviceId","getDeviceIdGemini","getSimSerialNumberGemini","getDeviceIdDs"}, dedupe, join with , → this is the imei parameter sent in join.jsp. Also reflection on SubscriptionManager.getSubId(int) (setAccessible(true)), dual-SIM TelecomManager handling, PhoneAccountHandle creation. Field f46696d lists 26 OEM intent extras probed for the SIM slot.
  • m5.s = C18505s — reflection call-hacking: telecomManager.getClass().getDeclaredMethod("endCall", null).invoke(telecomManager, null) and TelephonyManager → getITelephony() (setAccessible) → endCall(), with a getITelephonyMSim() variant. (This is the class behind C3’s note about ending calls.)
  • m5.a = C18487a — Class.forName("com.android.phone.OutgoingCallBroadcaster").
  • m5.j,m5.e,m5.b,m5.c,m5.l,m5.i,m5.n,m5.o,m5.q,m5.r,m5.s.
10.20 df (p244df, 25 files / 182 KB) — widget library + giant dispatcher — APP CODEh5
  • df.c = RunnableC15905c (61 KB) — the app’s mega multi-case Runnable; used by StoreActivity (case 24), NotificationReaderWindowActivity, TelegramWindowActivity (clipboard, InputMethodManager, window-insets, dialogs).
  • df.v = C15924v extends AppCompatAutoCompleteTextView; df.o = C15917o, df.x = C15926x extend LinearLayout (Material text-field scaffolding); df.s = C15921s; df.a–df.u listener shims.
10.21 b8 (p035b8, 19 files / 271 KB) — programmatic form builders — HAND-WRITTEN APP CODEh5

b8.b = C3508b (155 KB, implements ViewBinding) — the mega register/paywall form builder; b8.g = C3513g; b8.p = C3522p (ViewPager/CardView binder); b8.d=C3510d; b8.l=C3518l; b8.o=C3521o; b8.p… all inflate EyeButton/CustomTextView/EyeAvatar/EyeEditText/ RoundedCornersFrameLayout/LottieAnimationView. See §2.4.

10.22 d6 (p237d6, 49 files / 359 KB) — Store / social WebView UI — APP CODEh5

d6.n0 = C15833n0 (61 KB) extends AbstractC15820h implements h5.InterfaceC20338h — owns the social-store WebView (C21289i, C5846m case 6, EnumC21291k, DialogC21290j, C21293m/n/o, C21284d) and the EyeButton row. d6.o=C15834o (28 KB), d6.h=AbstractC15820h (21 KB), d6.f=ViewOnTouchListenerC15816f (15.8 KB), d6.j=C15824j, d6.c=C15810c, d6.y/y0=C15854y/C15855y0, d6.x/x0=C15853x0/C15852x.

10.23 c5 (p045c5, 11 files / 62 KB) — premium bottom sheet — APP CODEh5

c5.a=C3842a, c5.f=C3847f extends c7.c, c5.d=C3845d extends AbstractC16490r (a WebViewClient), ViewOnClickListenerC3846e (21 KB), DialogInterfaceOnKeyListenerC3843b.

10.24 e6 (p261e6, 4 files / 64 KB) — APP CODEh5

e6.d = C16128d (41.2 KB) — DBContacts/ContactsContract bulk updater. e6.b = RunnableC16126b (19.5 KB, 30+ cases) — the app’s other mega dispatcher; its case 19 is what j5.k’s static initialiser enqueues to wait for SP_KEY_MY_SETTINGS_SERVER_JSON. e6.a=C16125a (Drawable), e6.c=C16127c.

10.25 y5 (p672y5, 26 files / 347 KB) — see §2.3. y5.k(146 KB), y5.f(101 KB), y5.v(43 KB)h5

are hand-written god-classes; y5.m= GestureDetectorOnGestureListenerC21858m, y5.g, y5.e,y5.i,y5.q,y5.o,y5.w,y5.r,y5.x,y5.y,y5.z, interfaces y5.n,y5.s,y5.j,y5.b.

10.26 Remaining small packages — APP CODE unless notedh5
pkgclassesrole
c8 (p048c8, 5)c8.e=AbstractActivityC3899e extends b7.e implements h5.i, c8.c, c8.a, c8.b, c8.dIdPlus Activity base + click handler + e7.b callback.
hd (p321hd, 5)hd.a=C16961a (also the AccountWithDataSet.CREATOR, case 6), hd.b=C16962b, hd.e,hd.c=C16963c,hd.dExoPlayer Metadata.Output (SCTE-35 SpliceInsert/SpliceSchedule/TimeSignal/SpliceNull/Private commands) + Parcelable creators. Third-party ExoPlayer glue, misfiled as PhoneAccount.
k7 (p381k7, 3)k7.a,k7.b,k7.cPhoneAccount stubs — see §5.3.
ce (p050ce, 3)ce.c=C3912c extends CustomCredential (Google Identity), ce.a/ce.b extend GetCustomCredentialOptionGoogle Sign-In credential adapter for registration.
aj (p014aj, 9)aj.a=InputStream wrapper, aj.b=OutputStream wrapper, aj.c=HttpURLConnection, aj.d=HttpsURLConnection, aj.f=trace holder, aj.h=OkHttp InterceptorFirebase Performance HTTP instrumentation. THIRD-PARTY (Firebase SDK), misfiled as Registration.
hh (p325hh, 2)hh.b=C17065b (implements ViewBinding, InterfaceC20997c), hh.a=C17064a (file/zip IO)Media-feed share/save helper (file + OutputStreamWriter + Base64).
l5 (p397l5, 7)l5.h=C18302h, l5.g=RunnableC18301g, l5.d=C18298d extends c7.j, l5.f, l5.c=C18297c extends AbstractC20288j, l5.e=C18299ePhoto picker + contact-block confirm dialog.
l6 (p398l6, 7)l6.d=C18306d extends c7.d, l6.h=EnumC18310h, l6.g=C18309g (RecyclerView.Adapter), l6.a=EnumC18303aOnboarding card list / onboarding ad.
r7 (p518r7, 8)r7.c=touch listener, r7.b=RunnableC19886b, r7.g=C19891g (RecyclerView.Adapter), r7.f, r7.e=C19889e extends ViewPager2.OnPageChangeCallback, r7.aOnboarding ViewPager2 (OnboardingActivity, FakeCallActivity) + fake-incoming-call UI.
m6 (p419m6, 5)m6.e=C18510e extends c7.d, m6.d=C18509d, m6.b, m6.a, m6.cContact row / search UI.
y4 (p671y4, 2)y4.b=C21845b (8.9 KB), y4.a=RunnableC21844a (4.0 KB)Singular SDK config: new SingularConfig("instabridge_ffcbcb0b", "ddddafd404a9a4d918bae58070b136d7") — hard-coded Singular dev-key + SDK key pair, .withCustomUserId(publicId), .withBrandedDomains(["http://join.eyecon-app.com/"]), .withSingularDeviceAttribution(new C21233a(4)). See §11.
z4 (p700z4, 3)z4.c=C22071c (m39334e("onJoinSuccess", null)), z4.b, z4.aJoin-success event fan-out.
j6/i6/h5/h6see 10.15IdPlus.
rj (p527rj, 5)rj.a,rj.b,rj.c,rj.d,rj.e — all interface (250–348 bytes)Marker interfaces only; no logic.
e (p254e, 1)e.a=AbstractC16045a (907 bytes)Trivial abstract helper.
yj (p685yj, 1)yj.a=C21940a (27 KB) implements ViewBinding, SuccessContinuation, …Premium/store payment-sheet UI binder.
10.27 Third-party packages misfiled into C2 (restated)h5
  • as = kotlinx.coroutines internals (129 files / 1.16 MB) — as.b1 even contains AES/GCM/NoPadding (SecretKeySpec + GCMParameterSpec), which is kotlinx-serialization’s encrypted-payload support used by ad SDKs, not app crypto.
  • bm = OkHttp + Okio internals (156 files / 688 KB) — bm.RunnableC3589a case 13 and bm.RunnableC3701s3 case 2 happen to carry the store purchase launch, but the classes themselves are Play-Billing / media glue living in the OkHttp-named package because of how ProGuard re-assigned names.
  • aj = Firebase Performance instrumentation (9 files / 60 KB).


11. Security-relevant findingsh4

Severity is my assessment. “Requires remote control of store_url” means: compromise of the Firebase Remote Config project, or a TLS interception with a CA the device trusts, or an XSS on the store origin.

11.1 CRITICAL / HIGHh5

H-1 — StoreActivity loads an unvalidated remote-config URL into a JS-enabled WebView that exposes the account credential, the advertising ID and an arbitrary SharedPreferences reader.

  • store_url (Firebase Remote Config, default "") → Uri.parse(str).getHost().replace("www.","") with no scheme check, no host allow-list, no pinning.
  • setJavaScriptEnabled(true), setDomStorageEnabled(true), CookieManager.setAcceptThirdPartyCookies(…, true).
  • Bridges attached for the WebView’s whole lifetime: Eyecon = e7.d (reads clientId, authenticatedCli, SP_UUID, GAID, username, and any string/bool/int key in shared_preference_v2) and EyeconStore = k5.i (lists owned products with Play purchase tokens, launches Play purchases with a page-supplied offerToken, downloads ZIPs from a second remote-config URL store_download_url).
  • C5846m does not override shouldOverrideUrlLoading for this Activity, so navigation and redirects keep the bridges.
  • Contrast proof: WebviewPaywallActivity performs exactly the missing check ("https".equals(scheme) && (host.equals("eyecon-app.com") || host.endsWith(".eyecon-app.com")) else SecurityException("Untrusted paywall URL: …") + finish()).
  • Impact: full account takeover (the clientId is the e-auth bearer token for every Eyecon API call), persistent tracking via the advertising ID, and PII (validated MSISDN, display name).
  • Also: Eyecon.getFloatSP / getLongSP are declared void — dead code, harmless.

H-2 — Hard-coded static client secret in the APK: rc_key = "PgdtSBeR0MumR7fO". p620w6\C21273a (w6.a) sends it as e-auth-k on every request to api.eyecon-app.com. It is identical on every install and trivially extractable. Treat as a public value; if the server treats it as an authenticator, that is a server-side defect.

H-3 — Premium entitlement is entirely client-side; no server-side Play purchase verification.

  • No purchases.subscriptionsv2.get / RTDN / androidpublisher call anywhere in g5, q1, j5, w7, x, c0, k0, hh, yj.
  • No Play signature (RSA) verification of Purchase.getSignature() anywhere.
  • The only server signal is POST /app/mysettings with client-authored premium_cache_product ({productType, productId, purchaseToken, purchaseState, originalJson}) and premium_cache_time.
  • The set of “premium” product ids itself comes from Firebase Remote Config (premium_products_ids_list) — so the definition of what counts as premium is also remotely controllable, and j5.k.m33586b() gates the whole entitlement on it.
  • Consequence: a rooted/hooked client, a repackaged APK, or a patched remote-config value can produce premium (and therefore free premium days, RewardedAdGiftDays, daysOfTrialIdPlus) with no Google-side proof.
11.2 MEDIUMh5

M-1 — e-auth-c “checksum” is not a MAC. w6.a.m38421e(String) sums Character.getNumericValue(firstChar) over the --separated segments of the clientId. Any party that can compute the clientId can compute the header. No key, no length-extension exposure, but it gives zero integrity guarantee.

M-2 — The credential is an opaque blob stored in plaintext. clientId = the entire join.jsp response body, written unencrypted into the app’s custom prefs (SQLite contactdb, table shared_preference_v2). On a non-encrypted device, or with root / adb-backup, the bearer token is a plain string. Additionally, k5.f.m34065a sends the raw clientId (key cid) plus cli (the phone number) in a JSON POST body to store_download_url.

M-3 — Play purchase tokens are exposed to remote web content. EyeconStore.getPurchasedProducts returns {productType, productId, purchaseToken, purchaseState, originalJson} for every owned in-app and subscription to JavaScript running in StoreActivity’s WebView.

M-4 — Price/offer manipulation surface. EyeconStore.purchase(productId, offerToken, …) passes offerToken verbatim into BillingFlowParams.ProductDetailsParams. productId is constrained to ids the app itself queried via Play, but the offer / base-plan variant is chosen by the web page, with no comparison against a server-computed price and no allow-list. A hostile or compromised store page can drive the user into an unintended offer.

M-5 — Arbitrary analytics-event injection from the store page. Eyecon.reportEvent(name, json) accepts an unrestricted event name and an unrestricted property map and pushes it into the app’s analytics pipeline (AbstractC3464e.m3659j). EyeconStore.onError(code, message) lets the page inject arbitrary text into the app’s exception/crash reporter as StoreException("error code = …, error description = …"). Both allow telemetry poisoning.

M-6 — Install referrer bundle logged to logcat unconditionally. InstallReferrerReceiver.m10455a System.out.printlns every key/value of the INSTALL_REFERRER intent under the tag IRR/, including the user’s viral/inviter id. This is a hard-coded println, not gated by the print_logs_in_release remote-config key. On Android ≤ 4.1 and on any device with ADB/log-reader access this leaks attribution + campaign identifiers.

M-7 — SMS/CLI code delivered by the server and read back by the app. cli_auth/gettransport returns details{body, code, send_to, timeout} — i.e. the server sends the user the OTP and also tells the client what the OTP is. The app stores that value (V_CODE) and the SMS-Retriever path compares against it. Any attacker who can observe the SMS or the app’s memory learns the OTP; more importantly, it means the client holds the expected code before the user types it, which is a weak-by-design validation loop.

M-8 — Reflection-based access to hidden telephony APIs (in scope, Registration/IdPlus).

  • m5.s = C18505s: TelecomManager.endCall() and TelephonyManager → getITelephony() (setAccessible) → endCall(), plus getITelephonyMSim().
  • m5.p = C18502p: SubscriptionManager.getSubId(int).setAccessible(true); probes 26 OEM-specific intent extras to discover the SIM slot.
  • m5.a = C18487a: Class.forName("com.android.phone.OutgoingCallBroadcaster").
  • q1.c line 180: Class.forName("com.android.billingclient.ktx.BuildConfig"). These are unstable/hidden APIs; they also bypass the platform’s permission model in spirit. Not a new vulnerability, but they are the mechanism behind “how does the app end calls?”.

M-9 — IMEI/MEID collection sent on every join. join.jsp?…&imei=\<getDeviceId() across up to 3 SIM slots, comma-joined> (m5.p.m34691f()), plus os_id, adv_id, mc, public_id. On Android 10+ getDeviceId() returns "" for third-party apps, so in practice this is "" on modern devices and a real hardware identifier on older/privileged ones — but the intent is there and the field is always sent.

M-10 — Store item downloader writes a remote ZIP into getFilesDir()/store. k5.f.m34068d does implement the correct zip-slip guard (if (!file.getCanonicalPath().startsWith(dir.getCanonicalPath())) throw new SecurityException("Zip Path Traversal Vulnerability…")), so this is not currently exploitable — but the ZIP origin (store_download_url, another unvalidated remote-config string, hard-coded scheme "https" only) is remote-controlled, so the app will happily install and extract arbitrary themed content from any host.

11.3 LOW / INFORMATIONALh5
  • L-1 — Cleartext HTTP. No cleartext API traffic in C2. The only http:// literals are outbound browser intents (m.facebook.com, instagram.com, new.vk.com, tinyurl.com/eyecon-app, www.eyecon-app.com/docs/private_policy.html) and Singular’s withBrandedDomains(["http://join.eyecon-app.com/"]).
  • L-2 — Hard-coded third-party SDK credentials in C2 scope. y4.b: new SingularConfig("instabridge_ffcbcb0b", "ddddafd404a9a4d918bae58070b136d7") — a Singular SDK key and its dev-key/integration id in plaintext. (Other SDK keys — AdMob unit ids, Mbridge, Facebook app ids — are remote-config values, which is better.)
  • L-3 — Version identity hard-coded in two places. SP_JOIN_VERSION = "4.2026.04.23.1145", SP_JOIN_VERSION_CODE = 749, cv=vc_749_vn_4.2026.04.23.1145_a, Eyecon.getVersionCode() → 749, Eyecon.getVersionName() → "4.2026.04.23.1145", and store_download_url payload version_name/version_code. Six independent copies.
  • L-4 — Reflection/serialization. The only AES in scope is as.b1 AES/GCM/NoPadding (kotlinx-serialization, third-party). No TrustManager/HostnameVerifier overrides, no ALLOW_ALL_HOSTNAME_VERIFIER, no MD5/DES, no Base64-as-encryption.
  • L-5 — Dead / no-op bridge surface. Eyecon.getFloatSP, getLongSP, getUserProperty return nothing useful; AccountWithDataSet.toString() returns "".
  • L-6 — Emulator/fraud heuristics ship in the client. AbstractC16257t1.m31616A(isJoin, ctx, alreadySocialAuth, …) and the "Emulator suspicion" / "code validation failed" analytics strings in q7.l0.mo308k(). Client-side anti-fraud is advisory only.
  • L-7 — SP_KEY_VALIDATION_WITH / SP_KEY_PICTURE_SOURCE are provenance strings only ("Phone number" | "Google" | "Facebook" | "Not set", "google" | "facebook"); they are written from local state, not verified server-side.
11.4 Exfiltration surface summarisedh5

Data that leaves the device because of C2 code, with destination:

DataDestinationWhere
clientId (bearer credential), authenticatedCli, public_id, adv_id, mc, imei, os_id, deviceName, os, user_lang, viral_id, svr, tr, cli_cc, generated_cliGET api.eyecon-app.com/app/join.jspjd.z case 1
formatted cli, code, tr, is_already_social_auth, apsversionGET …/cli_auth/checkcoder5.s case 1
cli, reg_id, tr, is_already_social_auth, last_errorGET …/cli_auth/gettransportjd.z default
cli, reg_idGET …/social_auth/use_cli_validationvk\RunnableC21082e0
country, language, cli, cid, version_name, version_code, product, itemPOST <store_download_url>/getItemk5.f.m34065a
premium_cache_product (incl. purchaseToken), premium_cache_timePOST …/mysettingsj5.k.m33588e
arbitrary event name + propsanalytics busEyecon.reportEvent
install-referrer bundlelogcat IRR/InstallReferrerReceiver
publicId as custom user idSingulary4.b

12. Corrections to previously reported claimsh4

#Prior claimVerdictProof
1”clientId is the join.jsp response body, stored in the app’s custom prefs; read via AbstractC9039h.m10662s(), presence via m10665v().”CORRECTm10662s() = (String) AbstractC17503r.m33607k("clientId", ""); m10665v() = !isEmpty(prefs["clientId"]). jd.z does c19673l0.m31518o(strM38423g) with strM38423g = raw body; q7.l0.mo309m() case 4 does m32076c((String) m32061b(), "clientId"). §3.4.
2”Both write sites are in q7: C19673l0 case 4 and C19677n case 2.”CORRECT, with a precision fixq7.l0 = C19673l0, join success is mo309m() case 4 (not mo308k()); q7.n = C19677n, mo309m() case 2. Both also have a failure arm with the same case number in mo308k(). §4.3.
3”C19677n case 2 also clears SP_KEY_NOT_REGISTERED_WITH_REAL_CLI_V2.”CORRECTm32077d("SP_KEY_NOT_REGISTERED_WITH_REAL_CLI_V2", false). §4.3 step 6.
4”Both obtain the CID from a helper m32061b() reading the callback map.”CORRECTC16484n0.m32061b() = ((HashMap) f40952a).get("DEFAULT_RESULT"). §3.4.
5”p429n4 (248 KB / 4 files) is large — determine generated vs hand-written.”RESOLVED: 100 % GENERATEDn4.d is 253 KB of public static final int resource IDs importing only com.eyecon.global.R. It is the obfuscated R class. §2.1.
6”p672y5 is large — determine generated vs hand-written.”RESOLVED: HAND-WRITTENy5.k/y5.f/y5.v are god-classes with real SQLite/Telephony/MediaMetadataRetriever logic. §2.3.
7”p708zd is large — determine generated vs hand-written.”RESOLVED: THIRD-PARTY (ExoPlayer)zd.f imports MediaCodec/MediaCrypto/ExoPlaybackException; zd.g builds EGL14/GLES20. §2.2.
8”Play Billing maps to g5 (engine) and j5 (PaidPremium/FreePremium entitlement).”CORRECT but incompleteThe three-layer split is j5 (entitlement) → g5 (engine/manager, TAG "EyeconBilling") → q1 (the actual BillingClient facade, 30 files / 173 KB). q1 is the biggest of the three and was not named. §10.6.
9”Investigate whether the billing/paywall WebViews share C1’s a7.t.m425J0 bridge-injection mechanism.”NO — different mechanismstore_url appears exactly twice in the whole tree (its "" default and StoreActivity.onCreate). No evaluateJavascript/script-blob builder is involved; the bridges are statically named objects added with addJavascriptInterface. They share only the WebViewClient (C5846m) and its base (f7.r). §8.7.
10”Call the spam-report endpoints only via the documented classes; do not assume TelecomManager.endCall() — it is actually Call.reject()/disconnect().”PARTIALLY CORRECTED — C3’s caution is right, but C2 contains the reflection pathIn C2 the class that calls the hidden end-call API is m5.s = p418m5\C18505s, which does telecomManager.getClass().getDeclaredMethod("endCall", null).invoke(telecomManager) and getITelephony().endCall(). So both mechanisms exist: the modern android.telecom.Call.reject()/disconnect() used elsewhere, and a reflection fallback in m5.s. §10.19, M-8.
11”p516r5 = r5.* = cli_auth / checkcode (36 files, 696 KB).”CORRECT — but r5 is not in the C2 TSVIt is a Registration-family package that the TSV omitted. It contains cli_auth/checkcode (r5.s), updateuserdetails.jsp, the contact DTOs, and one 395 KB file (r5.e). §10.3.
12”p429n4, p672y5, p708zd are in scope.”p429n4 and p708zd are NOT in the C2 package list.The C2 TSV has 50 rows; n4 and zd are not among them. They were named in the briefing’s prose only.
13”Billing wrappers: Store\StoreActivity, Billing\Premium\*, WebViewPaywall\WebviewPaywallActivity.”CORRECT, with one important correctionStoreActivity uses C5846m and C5845l, both of which live in the Billing.Premium.WebViewPaywall package — the store screen and the paywall share their WebView plumbing. Also StoreActivity’s download queue (k5.*) lives outside Billing. §8.1.
14Implicit: “the store page is trusted because it comes from Eyecon.”REFUTEDWebviewPaywallActivity implements an explicit https + eyecon-app.com host check and throws SecurityException("Untrusted paywall URL: …") on failure. StoreActivity has no equivalent check. The trust asymmetry is real and demonstrable. §8.5.
15Implicit: “premium is verified server-side because mysettings is synchronised.”REFUTEDmysettings is a client→server write of the client’s own cache. No backend endpoint in C2 scope validates a Play token, and no Google API is called. §7.5, H-3.

13. Coverage notes / limitsh4

  • Every .java file in the 50 C2 directories plus the directly-referenced out-of-scope packages (p516r5 = r5, p620w6 = w6, p379k5 = k5, p308gj = gj, p236d5 = d5, p284f7 = f7, p008a7 = a7, p284f7, p632/p620w6, com\facebook\appevents\AbstractC9039h) was opened. Appendix A enumerates all 962 files with their real class names.
  • Not decompiled line-by-line: the largest god-classes y5.k (146 KB), y5.f (101 KB), y5.v (43 KB), b8.b (155 KB), r5.e (395 KB), d6.n0 (61 KB), df.c (61 KB), e6.b (19.5 KB), q1.c (54.9 KB), w7.i (32.7 KB), vk.RunnableC21082e0/21093i (30/34 KB), RegistrationActivity (210 KB), PhoneNumberValidationActivity (83 KB). For these I characterised the class, its public surface and every string/endpoint/pref-key that appears in it, but did not trace every branch.
  • Decompiler artefacts flagged in-line: throw null in StoreActivity.onDestroy, q7.i1.m36102a, WebviewPaywallActivity error paths, k5.d.m34064q; and super call-hoisting warnings in q7.l0, q7.n, jd.z, r5.s, C5846m — in each of those the super(...) constructor was moved to the top of the synthetic constructor by JADX, which is safe for AbstractC16130b because its body only assigns fields, but should not be trusted blindly.
  • Not resolved: where store_url is authored. It is a Firebase Remote Config key with an empty compiled default; the value only exists in the console. Resolving it requires RC project access, not the APK.
  • Not resolved: whether AbstractC16257t1.m31616A(...) (the p268eh “t1” class, called on every join) performs additional device attestation. p268eh is outside C2 scope; only its call site was read here.

Cluster C3 - Dialer, Calls, SMS, After-Callh3

Target: com.eyecon.global (MICO - Caller ID / Spam Blocker / Default Dialer / AI SMS Assistant) - v4.2026.04.23.1145 (build stamp vc_749_vn_4.2026.04.23.1145_a, versionCode 749) Sources tree: C:\Users\btc_e\Downloads\mico-analysis\src\sources Manifest: C:\Users\btc_e\Downloads\mico-analysis\src\resources\AndroidManifest.xml Scope: 42 ProGuard-renamed packages, 843 .java files, 4.1 MB (briefing docs/briefings/C3-dialer-call-sms.txt) Owning modules (per briefing): DefaultDialer, Call, Sms, AfterCall, InCallCallerId, CanTalk

How to read the namesh5

Every disk directory pNNNxxx maps 1<1> onto a short ProGuard package. Every class carries a /* JADX INFO: renamed from: X */ header that recovers the original (pre-obfuscation) name - those originals are used throughout this document. Where a file has no renamed from (ProGuard inlined/merged unrelated library classes into the same short package), the JADX file name is the only identifier and the class is flagged [LIB] - it contains no MICO business logic.

OBSERVED = directly readable in the decompiled source at the cited file/line. INFERRED = reasoned from call sites / naming / cross-references; the evidence is stated.


0. Executive summary and corrections to the briefingh4

0.1 Headline security findingsh5
#FindingSeverity
S1The AI SMS assistant POSTs the full SMS transcript (up to the last 50 messages, both content://sms/inbox AND content://sms/sent), the contact’s name, the phone number, and the user’s own display name to http://numberlookup.instabridge.com/respond-sms over CLEARTEXT HTTP. No auth header. The app globally permits cleartext (android:usesCleartextTraffic="true" + @xml/network_security_config). wo.f = C21474f(Context) line 295; x5.a = InterfaceC21626a.HIGH
S2com.eyecon.global.Call.MyPhoneReceiver is android:exported="true", no android:permission, android:priority="2147483647", and honours two app-private actions (com.eyecon.global.action.CALL_SCREENING_NEW_CALL, com.eyecon.global.action.INTENT_ACTION_START_CALL_WHATSAPP) plus PHONE_STATE / NEW_OUTGOING_CALL. state and incoming_number are taken verbatim from the broadcast extras and fed straight into MICO’s internal call-state machine, DB writes and analytics.HIGH
S3The assistant sends real SMS from the device via SmsManager.sendMultipartTextMessage(destination, null, smsManager.divideMessage(body), null, null) - no per-message user confirmation, no delivery/sent PendingIntents. x5.p.d = C21641p.m38847d line 87.MEDIUM
S4Hard-coded shared API secret rc_key = "PgdtSBeR0MumR7fO" shipped in w6.a = C21273a line 47 and sent as the e-auth-k header on every *.eyecon-app.com request; e-auth-c is a trivially reversible checksum of the CID (w6.a.m38421e, line 130).MEDIUM
S5Common request builder w6.h.m38430c (C21280h, lines 194-270) attaches device_id, adv_id, imei (Mbridge keyspace), installer_name, n_sims, time_zone, device_manu, device_model, client_cc, is_sms_sending_available, is_whatsapp_installed to server calls.MEDIUM
S6Incoming SMS body and sender are written in cleartext into the app’s private file <filesDir>/eye_log.txt on every new external SMS (u7.b = RunnableC20680b line 74).LOW-MEDIUM
S7Documents/Eyecon/RecordedCalls and Documents/Eyecon/RecordedNotes live in shared external storage (pre-Android-Q path uses the legacy _data column; the app also sets android:requestLegacyExternalStorage="true"). Audio in those directories is indexed into the recorded_notes SQLite table and uploaded (Google Drive backup).MEDIUM
S8Two three-long-press “Ad stats” debug AlertDialogs are reachable in the after-call UI (x4.q, x4.u).LOW
S9MICO keeps plaintext, unencrypted local copies of caller-ID names/photos/spam decisions, a plaintext sms_table of externally-received SMS, and a plaintext TTM voicemail transcript table ttm.LOW (standard for the category)
0.2 The open question: does MICO record phone calls? - NO (OBSERVED, exhaustively)h5

Exhaustive whole-APK searches (src/sources/**/*.java) produced:

  • Exactly one new AudioRecord(...) call site in the entire APK: p599v7/RunnableC20908f.java:508 (merged v7.f case 11) AudioRecord audioRecord = new AudioRecord(1 /* MediaRecorder.AudioSource.MIC */, 8000, 16 /* CHANNEL_IN_MONO */, 2 /* ENCODING_PCM_16BIT */, 16000); followed by audioRecord.startRecording(); (line 510) and io.kvh.media.amr.AmrEncoder.init(0). The owner is p633wl/RunnableC21441d (real name wl.RunnableC21441d) - the voice-note recorder; playback is wl.RunnableC21439b through AudioTrack + AmrDecoder. It captures MIC, not call audio.
  • Zero MediaRecorder, zero MediaProjection, zero createAudioCapture, zero VOICE_CALL / VOICE_DOWNLINK / VOICE_UPLINK, zero setAudioSource, zero getMaxAmplitude anywhere in the decompiled tree. (VOICE_COMMUNICATION appears only as a constant declaration inside androidx.media.AudioAttributesCompat and com.mbridge.msdk...C13867C - both library code, never used.)
  • Documents/Eyecon/RecordedCalls is a storage / import / archive location, not a capture location:
    • p260e5/AbstractC16098a.m31460c(int) (e5.a) merely resolves and mkdirs() the directory (RecordedNotes when i3==0, RecordedCalls otherwise) - MyApplication.f12172q (external Documents) if AbstractC0042b0.m178a0(), otherwise getFilesDir()/Eyecon/....
    • p008a7/RunnableC0070p0.m409b() (a7.d1 merged runnable) creates both directories on SDK_INT <= 29 and then calls e5.d.m31467a(...), which scans pre-existing files matching .+__.+__[0-9]+(__s|__c)?(__-1|__1|__2)?__(ern|erc)\..* and bulk-inserts them into the recorded_notes SQLite table.
    • p282f5/RunnableC16439w (f5.w case 2) then zips/uploads those files with progress key PROGRESS_RECORDS; case 1 zips the TTM audio dir to ttm.zip with progress key PROGRESS_KEY_TTM.
  • Conclusion (OBSERVED): MICO contains no call-audio capture code path. RecordedCalls is populated by some external producer (another recorder app, or a system/OEM recorder) and MICO indexes, archives and backs it up. Any consent obligation for call recording therefore rests entirely outside MICO - MICO is a consumer of recordings it did not make, but it does read, index, display and upload them.
0.3 Corrections to previously reported claimsh5
Prior claimStatus
GET https://api.eyecon-app.com/app/pic?cli=<number>&is_callerid=true&size=<small|big|null>&type=0&src=<n>&cancelfresh=<0|1>&cv=vc_749_...CONFIRMED with refinements. Built literally in w6.h.m38434g (p620w6/C21280h.java:317-338). size = small when i3==1, big when i3==2, else the literal string null. type=0 is a literal. src is the caller’s tag ("DefaultDialer", "AfterCall", "CanTalkActivity", "BlockAdapter", "SpamReportHandler", "StatisticBars", …). cancelfresh is hard-coded 0 at this call site - the <0|1> variant is not produced here. The URL is gated by C18312b.m34437h().m34452q("+"+cis) (libphonenumber isValidNumberWithReason); on failure the callback gets code 812 and no network call is made. The base URL constant is p008a7/AbstractC0039a.f168L1 = "https://api.eyecon-app.com/app/".
CanTalk/TTM endpoints on voice2text.eyecon-app.com: /init-ttm, /start-ttm, /ack, /getText, /ttm-call-forwarding-result, /ttm-get-tokens-amount, /no-ttmALL SEVEN CONFIRMED. Five of the seven are inside this cluster: /init-ttm and /ack in w6.b (RunnableC21274b.java:49,70), /getText, /ttm-call-forwarding-result, /ttm-get-tokens-amount in w6.d (RunnableC21276d.java:90,145,243). /start-ttm is in p572u4/RunnableC20597o.java:178 and /no-ttm in as/RunnableC3316o.java:150 (both out of scope). Main API cantalk.jsp (in p474p5), getcantalkstatus.jsp (in p474p5), cantalkreceived.jsp (string referenced from p451o7/AbstractC19089d.java:197). cantalksaw.jsp was NOT found anywhere in the tree.
AI assistant posts to numberlookup.instabridge.com over cleartext HTTP at /respond-sms, with the DTOs listedFULLY CONFIRMED, field-for-field in section 5. The Retrofit interface is x5.a with @POST("respond-sms") returning RespondSmsResponse. MessageAssistantSource has exactly 10 subclasses and MessageAssistantSourceKey has 10 constants - the briefing list is complete and correct.
Spam model maps to x7 + w9; TtmException in com\eyecon\global\Call\TalkToMe\CONFIRMED. x7 (20 files) + w9 (2 files). TtmException thrown at n5.c.m35046a lines 164/171 (p430n5/C18704c.java).
Call recordings/voice notes written to shared Documents/Eyecon\{RecordedCalls,RecordedNotes}CONFIRMED - see 0.2 and 11.
MyPhoneReceiver … taking state and incoming_number straight from the broadcast … and drive MICO’s call-state machine into TelecomManager.endCall()PARTIALLY WRONG. state/incoming_number are indeed taken verbatim (im/RunnableC17334k.java:900-901) - confirmed. But the telephony action is not TelecomManager.endCall(). CallStateService.m10185f() routes to w0.a.m38343q(call, "REJECT"|"DISCONNECT") which is android.telecom.Call.reject(false,"") / Call.disconnect() (p614w0/AbstractC21232a.java:687-694). TelecomManager is never used for ending calls. Because these are InCallService APIs they only take effect on a call MICO actually owns (i.e. while it is the default dialer). The exported-receiver abuse is still real (state-machine injection, attacker-chosen numbers written into MICO’s DB, analytics pollution, forced after-call/assistant/TTM pipeline) - see S2 and section 14.
From cluster C1: “am/RunnableC0215t1 logs body/sender to logcat”WRONG ON BOTH COUNTS. (a) am/RunnableC0215t1.java contains no SMS body/sender logging at all - it is a ProGuard-merged runnable with unrelated branches. (b) The real site is p575u7/RunnableC20680b.java (real name u7.b), tag "ExternalSmsDB", lines 74 and 122: C18416a.m34578o("ExternalSmsDB", "Insert new ExternalSms: body: " + str2 + " sender: " + str3 + " messageId: " + str);. (c) The destination is not logcat: lo.a.m34578o -> lo.a.m34588y -> lo.a.m34565a(String) which appends to new File(MyApplication.f12163h.getFilesDir(), "eye_log.txt") (rotating at 5 242 880 bytes down to the last 50 lines). There is no android.util.Log call on this path. See S6.
From cluster C1: “SMS bodies and senders are stored in plaintext in the sms_table”CONFIRMED. p008a7/C0041b.java:424 (CREATE TABLE IF NOT EXISTS sms_table) and :1070. Writes at u7.b lines 84/140 via insertWithOnConflict("sms_table", ..., 5). Columns resolved through p008a7/AbstractC0039a: f150F1=body, f153G1=sender, f239m1=messageId, f144D1=date, f147E1=unread counter, f156H1=text/type.
0.4 Package inventory (42 packages / 843 files, verified)h5
Disk dirReal pkgFilesOwnerResponsibility
p620w6w68DefaultDialerServer - all HTTP transport to api.eyecon-app.com and voice2text.eyecon-app.com; the pic caller-ID URL builder; the common device-parameter request builder; the retrying API-request worker.
p646x5x520DefaultDialerAI SMS assistant: settings fragment, initial-message templates, auto-use policy, work-request enqueuer, Retrofit service interface, follow-up network coroutine.
p636wowo10DefaultDialer[LIB] kotlin.coroutines coroutine-state-machine base classes; plus wo.f = the assistant’s Retrofit/Moshi container hard-coding http://numberlookup.instabridge.com/.
p511r0r01AfterCallProGuard-merged god-class r0.a = C19813a; the assistant-relevant constructor is C19813a(Context, 22) = MessageAssistantStorage (SharedPreferences("MessageAssistantStorage"), key STARTED_FLOW_CONTACTS, Moshi List<MessageAssistantFlow>).
p648x7x720AfterCallSpam/block model + spam_list SQLite DAO + spam-report & block dialogs + Spam&Block pager/adapter.
p623w9w92AfterCallProGuard-merged runnables: spam_list read/write/replace and SP_KEY_VERSION_SPAM_LIST.
p399l7l75DefaultDialerPhoneNumberHelper (libphonenumber wrapper), PhoneNumbersCache (results cache persisted to SP_KEY_PHONE_NUMBERS_CACHE_V4), DiskLruCache wrapper.
p554t5t512CallPersonDataLoader - the caller-ID name/photo/spam fetch-and-display engine used by the dialer, the caller-ID window and the after-call screen.
p380k6k629DefaultDialerCallerIdWindow - the incoming-call full-screen UI, its call-scene enum, bubble, ad slots and the AI-assistant button.
p573u5u542DefaultDialerCallActivity support: Call.Callbacks, in-call WebView ads, dialer/call-screening role notifications, call bubble window, TTM “redirect call” button, window insets.
p404lglg18Call[LIB] Guava ListenableFuture/executors + merged notification-channel/manifest helpers (lg.n).
p421m8m819DefaultDialer[LIB] Facebook in-app-purchase / Google Play Billing bridge + string helpers (m8.s).
p575u7u726SmsSMS list UI adapters/view-holders, the sms_table DAO (u7.b), ExternalSmsDB manager (u7.s), message comparator (u7.q), SMS-window click handlers.
p619w5w516DefaultDialerIn-call feed RecyclerView view-holders (ad slots, history, statistics, note/reminder, extra actions); the AI-assistant action-button holder is w5.c.
p645x4x422AfterCallAfter-call feedback engine (x4.b), after-call activity launcher (x4.c), per-call JSON model (x4.o), after-call action runnable (x4.e), user-feedback & spam dialogs (x4.a0), tag picker (x4.z), debug long-click dialogs (x4.q/x4.u).
p599v7v720AfterCallProGuard-merged runnable carrying reportBlock -> spamreport, PhotoPicker getnames.jsp, the single AudioRecord (MIC) voice-note recorder, TTM audio playback, table_share_media / dynamic_area_tickets DB ops, theme background rendering.
p268eheh92AfterCall[LIB] Firebase Crashlytics proto models (CrashlyticsReport, Session, Device, Thread, RolloutsState, …). No MICO logic.
p438njnj51DefaultDialer[LIB] Firebase Sessions (sessionSdkVersion=2.0.8) + merged Crashlytics hook in nj.o (after_call_switch_to_compact, SP_AFTERCALL_TYPE).
p605vhvh32AfterCall[LIB] Firebase In-App Messaging proto models + vh.q merged Crashlytics build-id reader.
p637wpwp13DefaultDialer[LIB] kotlinx.coroutines Dispatchers / worker-pool executor.
p484popo28DefaultDialer[LIB] kotlin stdlib (Result = po.m/po.n, Pair = po.q, Lazy = po.f, unsigned arrays). po.k = the key/value pair used for analytics maps.
p485pppp88DefaultDialer[LIB] kotlinx.coroutines (Dispatchers, Job, CancellableContinuation, withContext).
p587uouo11DefaultDialer[LIB] kotlin.coroutines (Continuation = uo.d, CoroutineContext = uo.i, SafeContinuation = uo.k).
p275epep22DefaultDialer[LIB] kotlin function types (ep.a = Function0, ep.o = Function2, …).
p609vovo6DefaultDialer[LIB] kotlin intrinsics (COROUTINE_SUSPENDED = vo.a).
p715zkzk36DefaultDialer[LIB] Moshi (JsonReader=zk.q, JsonWriter=zk.w, JsonAdapter=zk.l, Moshi=zk.e0, Util=zk.a, JsonEncoding=zk.p) - used by every assistant DTO.
p038blbl6DefaultDialer[LIB] Moshi internal type adapters (bl.f = Util, bl.b = nullSafe).
p662xoxo3DefaultDialer[LIB] kotlin EnumEntries (xo.b, xo.c, xo.a).
p344icic80DefaultDialer[LIB] ExoPlayer2 core.
p385kckc37Call[LIB] ExoPlayer2 audio (AAC/AC-3/AC-4/AudioTrack) + merged DB/analytics callbacks in kc.k.
p379k5k510Call[LIB]/[MIXED] Google Play store item loader (k5.f, k5.i) + merged CanTalkActivity/location/Blowfish branches in k5.b.
p449o5o510CallTTM window UI - RecyclerView adapters, Observer, click handlers (paywall_noti_ttm, TTM window).
p716zlzl3Call[LIB] Google Play Billing v6 client + TtmAudioPlayer interface zl.a (progressive download with retry).
p430n5n58CallTTM (Talk To Me) core: call-forwarding config (n5.c), SIM/operator mapping (n5.b), TTM message DAO (n5.d + n5.f), TTM message polling (n5.g), TTM token balance (n5.h).
p540s4s46AfterCallAfter-call carousel-ads adapter/view-holder/scroller.
p441nqnq7AfterCall[LIB] ViewPager2 helpers.
p472p3p33DefaultDialer[LIB] Room (p3.a comparator, p3.c enum).
p476p7p73DefaultDialer[LIB] TextToSpeech wrapper + layout helper.
p230dd1AfterCalld.a = AbstractC15790a, a merged utility bag; the C3-relevant functions are m31044c(spamType, cli) / m31045d(spamType, call) (spam-type -> UI tag) and m31046e(C19862i).
p597v5v52DefaultDialerIn-call feed ordering adapter (v5.b) + v5.a callbacks.
p614w0w01DefaultDialerw0.a = AbstractC21232a (merged): m38343q(Call,"REJECT"/"DISCONNECT"), m38322G(Call) (CLI), m38321F, m38318C (private number), m38324I, m38317B, m38319D.

1. Package w6 (disk p620w6, 8 files) - Server / network transporth4

One-line responsibility: the single HTTP layer for every MICO backend call, plus the caller-ID (pic) URL builder, the common device-parameter request builder, the failing-request retry worker, and the TTM init/ack/getText/tokens/call-forwarding calls.

w6.a -> C21273a (p620w6/C21273a.java) - NetworkHelper (singleton)h5

Constants / secrets:

  • TAG = "NetworkHelper"
  • private static final String rc_key = "PgdtSBeR0MumR7fO"; <- hard-coded shared API secret
  • instance = singleton; httpClient = oq.C19227c0 built over C19225b0 (OkHttp)

Key methods:

SignatureBehaviour
static C19251o0 m38417a(C19231e0 request, boolean authenticated)OkHttp execute. If authenticated: throws RuntimeException("Error missing CID") when the CID is empty; else sets headers e-auth-v=e1, e-auth=<CID>, e-auth-c=<m38421e(CID)>, e-auth-k=PgdtSBeR0MumR7fO. Wrapped in FirebasePerfOkHttpClient.execute(...).
static Map m38418b(HttpURLConnection)Flattens response headers to first-value-per-key.
static int m38420d(HttpURLConnection, String url)Gets the response code; on 401 with an eyecon-app.com URL and a non-empty E-Mark header it re-saves the CID (AbstractC9039h.m10641U(getHeaderField("CID"), "401 1 from url = "+url)). Otherwise returns 401.
static String m38421e(String cid)Weak “checksum”: splits the CID on -, sums Character.getNumericValue(firstCharOfEachSegment), returns the decimal sum. Trivially reversible / constant.
static String m38422f(ArrayList params, String url, String method, int[] rc, boolean json, String tag, HashMap headers, boolean withAuth, boolean gzip, HashMap respHeaders)Wrapper over m38423g with rid="".
static String m38423g(ArrayList params, String url, String method, int[] rc, boolean json, String tag, HashMap headers, boolean withAuth, boolean gzip, HashMap respHeaders, String rid)The core HttpURLConnection GET/POST. 60 s connect/read timeouts, fixed desktop Chrome UA (Chrome/87.0.4280.88 Safari/537.36), content-type: application/x-www-form-urlencoded; charset=utf-8, optional Accept-Encoding: gzip, optional rid header, setInstanceFollowRedirects(true). Calls m38420d (CID refresh on 401) then m38426j.
static void m38424h(ArrayList params, String url, int[] rc, String callFunc, String rid)POST with a gzip request body (GZIPOutputStream).
static String m38425i(int[] rc, boolean withAuth, String body, String prefix, String url, String callFunc, String contentType, String method, String rid)POST with an explicit body - used for the JSON TTM calls. If prefix is non-empty the body becomes prefix + "=" + body.
static String m38426j(HttpURLConnection, boolean gzip, HashMap sink)Reads the (optionally gzipped) response into a String.
static void m38427k(HttpURLConnection)Injects the four e-auth* headers when the URL starts with AbstractC0039a.f168L1 (https://api.eyecon-app.com/app/) or contains eyecon-app.com.
w6.h -> C21280h (p620w6/C21280h.java) - Server facadeh5

Statics: f56466b = base (https://api.eyecon-app.com/app/), f56467c = .../join.jsp, f56468d = .../uploadpic.jsp, f56469e = random boolean, f56470f = singleton, f56471g = HashSet, f56472a = C16945i(1,1,"Server",true) executor.

SignatureBehaviour
static void m38428a(String contacts, boolean init)POSTs the whole address book to addcontacts.jsp with contacts, cli_cc (SIM country ISO), init, possible_langs, version. Then, if contactLangNeedUpdate or >20 contacts or >5 % churn, GETs getcontactslang.jsp and stores contactLang.
static String m38429b(String name)Spam-name scrubber: returns "" for empty, "spam" or "maybe spam" (case-insensitive) - i.e. MICO never uploads the literal strings spam / maybe spam as a user-suggested name.
static void m38430c(ArrayList params)Common device-parameter builder. Appends: version=vc_749_vn_4.2026.04.23.1145_a, installer_name, n_sims, time, is_sms_sending_available, is_whatsapp_installed, mc, device_id (AbstractC0042b0.m172U()), adv_id (advertising id), imei (Constants.DeviceIdentifierKeyspaceKeys.IMEI_KEYSPACE_KEY, the Mbridge keyspace value), client_cc (Locale country), time_zone, device_manu, device_model. On Xiaomi it probes AppOpsManager SEND_SMS before computing is_sms_sending_available.
static void m38431d(ArrayList, String, String, String[])Posts on the Server executor.
static void m38432e(HashMap respHeaders)Reads the sms response header; if > 0 schedules a messages/get fetch and sets sp_new_external_sms_in_server=true.
static void m38433f(String tag, boolean onServer, String cis, boolean gzip, w6.g cb)getnames.jsp fetch (tagged, authed, gzip).
static void m38434g(String src, int size, String cli, m7.o cb)The caller-ID photo/name endpoint. Validates "+"+cli via l7.b.m34452q; on failure calls cb.mo590e(null, 812) and returns. Otherwise builds .../app/pic?cli=<cli>&is_callerid=true&size=<small|big|null>&type=0&src=<src>&cancelfresh=0&cv=vc_749_vn_4.2026.04.23.1145_a and hands it to m7.p.m34736e.
static void m38435h(e7.b, String, String, q7.h1, boolean)Async post.
static void m38436i(String name, String type, JSONObject attributes)Builds {name, type, attributes, timestamp} and posts it.
static void m38437j / m38438k / m38439lResult/error mapping helpers (HTTP_RESPOND_CODE, CLIENT_UNEXPECTED_RESULT, CLIENT_RETURN_WITH_NO_ERROR, CLIENT_NETWORK_EXCEPTION).
static void m38440m(String method, String tag, Object body, boolean highPriority, e7.b cb)Generic POST/GET.
static int m38441n()Uploads the profile photo to uploadpic.jsp as multipart/form-data with field msisdn; clears SP_KEY_NEED_UPDATE_PROFILE_PIC on 200.
w6.b -> RunnableC21274b (p620w6/RunnableC21274b.java) - TTM init & ackh5
  • case 0 - initTtm (line 49): POST https://voice2text.eyecon-app.com/init-ttm, application/json, body {"mycli": <authenticatedCli>, "purchases": <gson>, "freeTrialCode": <remote "ttm_free_trial_code">}. Skipped when identical to SP_KEY_LAST_INIT_TTM_JSON. On 200 it stores the JSON and feeds the response into n5.h.m35053a (token balance).
  • case 1 - ttmAck (line 70): POST https://voice2text.eyecon-app.com/ack, same auth/content-type. Both paths require AbstractC9039h.m10665v() (a clientId exists).
w6.c -> RunnableC21275c (p620w6/RunnableC21275c.java) - 22-case merged runnableh5
  • case 1 GET .../app/addfbtoken.jsp (token, fbid, version)
  • case 2 GET .../app/updateuserdetails.jsp?adv_id=...
  • case 3 GET .../app/updateuserdetails.jsp?mc=...
  • case 4 GET .../app/updateuserdetails.jsp -> caches appVersion
  • case 6 GET .../app/upgrade_pic2eyecon.jsp
  • case 10 - prunes SP_AFTER_CALL_LOGS (JSON array) to entries newer than now - AbstractC19089d.f48805a and back-fills the array length.
  • case 11 - MyApplication.m10341c(app); Toast.makeText(app, app.getString(R.string.assistant_has_sent_a_message), 1) - the toast shown after the assistant sends.
  • case 12 - ApiRequestWorker pass: reads table_api_requests, replays each entry, deletes on 200 or after ApiRequestServerRetryCount, otherwise increments the retry counter and re-enqueues ApiRequestWorker with delay apiWorkRepeatIntervalMinutes minutes, BackoffPolicy.LINEAR 1h, NetworkType.CONNECTED.
  • cases 13-21 - ad-SDK / Play-Services bootstrapping.
w6.d -> RunnableC21276d (p620w6/RunnableC21276d.java) - 30-case merged runnableh5

TTM endpoints (in scope):

  • case 1 - checkMessagesTtm (line 90): GET https://voice2text.eyecon-app.com/getText?mycli=<cli> -> success 2xx means C15802b.m31518o(res); m31516i(), else m32066g(code,"CB_ERROR"); m31515h(). Logs elapsed time.
  • case 3 - ttmCheckTestCallForwardingResult (line 145): GET .../ttm-call-forwarding-result?mycli=<cli>, only for Eyecon users.
  • case 7 - getTtmTokensAmount (line 243): GET .../ttm-get-tokens-amount?mycli=<cli>, JSON accepted. Other cases: virality/premium_status (2), virality/viral_id_check (4), messages/get (5), get_store_products (6), mysettings (8), virality/viral_id (9), webview/share (11-18), case 19/20 -> CompactAfterCallActivity.m9970l1() / DefaultAfterCallActivity.m9970l1(), case 21 dismisses x4.a0, case 22 SpamAndBlockFragment.m10572t0(), case 23 adds a number to the spam list UI, case 26 parses the reportSpam remote JSON and reads spamType.
w6.e -> C21277e (p620w6/C21277e.java) - merged ValueCallbackh5

case 0 = WebView Facebook-profile probe (NOT_A_FB_PROFILE_ERROR); default = share-video HTML scrape with up to 20 retries.

w6.f -> RunnableC21278f (p620w6/RunnableC21278f.java)h5
  • case 0 - sendCallReport: gzips a JSON payload and POSTs to the remote server_report_url.
  • case 1 - sendNewExternalSmsIdsToServer: POSTs the list of external SMS ids (not bodies) to .../app/messages/received (application/json) and clears sp_new_external_sms_in_server.
w6.g -> InterfaceC21279g (p620w6/InterfaceC21279g.java)h5

void mo34917g(String[] strArr, x7.d spamEntry, int i3) - the getnames.jsp result callback.


2. Package x5 (disk p646x5, 20 files) - AI SMS assistanth4

Responsibility: the entire AI SMS assistant - settings UI, message templates, per-trigger-type auto-use policy, work-request enqueuer, the Retrofit service declaration, and the two network coroutines (start-flow and follow-up).

x5.a -> InterfaceC21626a (p646x5/InterfaceC21626a.java) - the assistant backend contracth5
@POST("respond-sms")
@Nullable Object m38828a(@NotNull @Body RespondSmsBody respondSmsBody,
@NotNull Continuation<? super RespondSmsResponse> c);

Kotlin suspend function. The Kotlin metadata string Lx5/a; confirms the original class name.

x5.h -> AbstractC21633h (p646x5/AbstractC21633h.java) - AutoUseOptionsHandler (also the message-template resolver)h5
MethodBehaviour
static String m38833a(String type)Initial message for type. Reads the user’s display name from AbstractC9039h.m10663t("") (SharedPreferences key android:hint_username). If empty -> R.string.eyecon_assistant_initial_message_without_name, else R.string.eyecon_assistant_initial_message_with_name with the name interpolated. Then delegates to m38834b.
static String m38834b(String type, String fallback)Looks up SP_KEY_AI_ASSISTANT_INITIAL_MESSAGE_FOR_<TYPE> in SharedPreferences with fallback as the default; if the stored value is empty returns fallback. Wrapped in try/catch.
static String m38835c(String type) throwsMaps DEFAULT / DECLINED_CALL / UNKNOWN_CALLER / MISSED_CALL / SPAM_CALL -> SP_KEY_AI_ASSISTANT_INITIAL_MESSAGE_FOR_DEFAULT / _DECLINED_CALL / _UNKNOWN_CALLER / _MISSED_CALL / _SPAM_CALL. Any other value -> Exception("Unknown type, type = "+type).
static String m38836d(String type) throwsSame mapping -> SP_KEY_IS_AI_ASSISTANT_DEFAULT_AUTO_USE_ENABLE / _DECLINED_ / _UNKNOWN_CALLER_ / _MISSED_CALL_ / _SPAM_CALL_AUTO_USE_ENABLE.
static String m38837e(String type)"manual" when type is empty or "DEFAULT", else "auto". Sent as the type analytics field.
static boolean m38838f(String type)Auto-use gate. Requires the remote flag use_ai_assistant and SP_KEY_IS_AI_ASSISTANT_<TYPE>_AUTO_USE_ENABLE.
static void m38839g(String number, String type, MessageAssistantSource src)Builds C20343m("AutoUseOptionsHandler", number, new f6.f(new C16937a(16, number, src, type), 2)) - i.e. resolves the contact name for the number first, then C16937a case 16 starts the flow.
static void m38840h(long callTimeMs, String cli)onCallDeclined. No-op unless the declined auto-use flag is set; no-op if now - callTime >= 2 min; else m38839g(cli,"DECLINED_CALL", new MessageAssistantSource.CallDeclinedAutoUse()).
static void m38841i(long callTimeMs, String cli)onCallMissed - same 2-minute window, source CallMissedAutoUse.
static boolean m38842j(String cli, String name)onSpamCall. If the spam auto-use flag is set -> x5.v.m38851a(app, new MessageAssistantStartFlowArgs(cli,name,new SpamCallAutoUse()), "SPAM_CALL") and returns true; else false.
static void m38843k(String type, boolean value)Writes SP_KEY_IS_AI_ASSISTANT_<TYPE>_AUTO_USE_ENABLE.
x5.v -> AbstractC21647v (p646x5/AbstractC21647v.java) - the single entry point for every assistant starth5
public static final void m38851a(Context ctx, MessageAssistantStartFlowArgs args, String type)
  1. Fires analytics ai_assistant_button_click{source=...}.
  2. If type == "DEFAULT" shows the toast R.string.ai_assistant_manually_clicked.
  3. Serialises args to JSON with the assistant’s Moshi instance (f57243c -> zk.e0 with the bl.f config).
  4. Builds WorkManager OneTimeWorkRequest(MessageAssistantStartFlowWorker.class) with Data{ args_bundle: <json>, type: <type> } and Constraints{ requiredNetworkType = CONNECTED }.

Call sites of m38851a (whole tree):

SiteSourceType
p380k6/C17781s.java:1593,1656,1720 (k6.s, CallerIdWindow AI button)IncomingCallPopupDEFAULT
p315h7/C16937a.java:321 (case 16, AutoUseOptionsHandler)declined / missed / unknown-callerdecl/missed
p646x5/AbstractC21633h.java:158 (onSpamCall)SpamCallAutoUseSPAM_CALL
p397l5/C18297c.java:57 (permission-gated launcher)MissedCallPopup, AfterCallDEFAULT
p619w5/C21258c.java (10 merged call sites - the in-call feed AI action)variousDEFAULT
x5.p -> C21641p (p646x5/C21641p.java) - assistant context + SMS sendingh5
  • Fields: Context f57877a, wo.f f57878b (Retrofit container), r0.a f57879c (C19813a, the MessageAssistantStorage).
  • static final String m38844a(C21641p) -> device locale language tag (configuration.locales[0].toLanguageTag() on API >= 24).
  • static final void m38845b(self, String error, String messagetype, String number, MessageAssistantSource src, String type) -> analytics ai_assistant_error{error?, number, messagetype, source, type}.
  • static final void m38846c(self, String initialMsg, String normalizedNumber, String rawNumber, MessageAssistantSource src, String type) -> startConversation: writes new MessageAssistantFlow(now-10 000, normalizedNumber, now, type) via x5.s.m38849b, then m38847d(initialMsg, rawNumber, src), then C16945i.m32771c(new w6.c(11)) (the “assistant has sent a message” toast).
  • static void m38847d(String body, String destination, MessageAssistantSource src):
SmsManager smsManager = SmsManager.getDefault();
smsManager.sendMultipartTextMessage(destination, null, smsManager.divideMessage(body), null, null);

catch -> analytics ai_assistant_platform_sending_failed{error, source}. No sentIntent / deliveryIntent, no user confirmation, no per-message prompt. (S3)

x5.n -> C21639n (p646x5/C21639n.java) - the follow-up network coroutineh5

JADX could not decompile invokeSuspend (JadxOverflowException, regions limit reached); the JADX raw bytecode listing is preserved verbatim in the comment body of the file and is quoted here. Everything below is OBSERVED, not inferred.

AbstractC9039h.m10664u(ctx) -> if false: analytics "ai_assistant_missing_sms_permission"{source=FollowUpKey}; return
for (IncomingSMSDTO sms : messages) { // list from MessageAssistantReplyWorker
flow = sms.getStartedFlow(); // MessageAssistantFlow
num = flow.getNormalizedNumber();
type = flow.getStartType(); // "DEFAULT"|"MISSED_CALL"|"DECLINED_CALL"|"SPAM_CALL"|"UNKNOWN_CALLER"
auto = flow.getTypeAutoOrManual(); // "manual"|"auto"
addr = sms.getOriginatingAddress();
startMs = flow.getStartMS();
// (1) read the SENT folder
ctx.getContentResolver().query(Uri.parse("content://sms/sent"),
new String[]{"_id","address","date","body"},
"address LIKE ? AND date > ?",
new String[]{ "%" + mp.AbstractC18646q.m34976n1(7, addr) + "%", String.valueOf(startMs) },
"date DESC")
// (2) read the INBOX folder (identical projection / selection / sortOrder)
ctx.getContentResolver().query(Uri.parse("content://sms/inbox"), ... )
// (3) if more than one message, sort the merged list with u7.q(3)
// (4) map each row -> x5.i Message(body, x5.l sender, date)
// x5.j.toString() == "Assistant" -> RespondSmsBody.Message.Role.System
// x5.k.toString() == "Caller" -> RespondSmsBody.Message.Role.User
// otherwise -> throw kotlin.NoWhenBranchMatchedException
// (5) if the newest message text != sms.getBody(), append
// new RespondSmsBody.Message(Role.User, sms.getBody())
// (6) truncate to the LAST 50 messages
new RespondSmsBody(
contactName = <register r18>,
number = flow.getNormalizedNumber(), // r19 <- r11
messages = (<=50 entries),
locale = <register r21>,
messagesInSession = messages.size(),
userName = AbstractC9039h.m10663t("") )
api.m38828a(body, this) // POST http://numberlookup.instabridge.com/respond-sms
...
RespondSmsResponse.EndConversation -> x5.s.m38850c(flow, storage)
analytics "ai_assistant_ended_on_following_up"{number, historysize, type}
RespondSmsResponse.NewMessage -> analytics "ai_assistant_following_up_message_sent"{number, historysize, type}
flow = MessageAssistantFlow.copy$default(flow, 0L /*startMS*/, null, now /*lastMessageMS*/, null /*startType*/, 11 /*mask*/, null)
x5.s.m38849b(flow, storage)
C21641p.m38847d(newMessage.getMessage(), num, new MessageAssistantSource.FollowUp())
on throwable -> C21641p.m38845b(self, throwable.getLocalizedMessage(), "follow up message", num,
new MessageAssistantSource.FollowUp(), auto)
}

Caveat (OBSERVED ambiguity, stated rather than guessed): in the raw listing, registers r18 and r19 of the RespondSmsBody.<init> call are both loaded from r11 (normalizedNumber), so the decompiled view appears to feed the normalized number into contactName. This is exactly the register reuse JADX cannot disambiguate for an overflowing method. What is certain: the constructor shape is 6 args in order contactName, number, messages, locale, messagesInSession, userName; messages is the merged, sorted, <=50 list; messagesInSession is messages.size() (r22 <- r8); userName is AbstractC9039h.m10663t(""); and the POST is to x5.a = respond-sms.

x5.o -> C21640o (p646x5/C21640o.java) - the start-flow coroutine (fully decompiled, lines 100-226)h5
  1. AbstractC9039h.m10664u(ctx) false -> analytics ai_assistant_missing_sms_permission{source} and return.
  2. C18312b.m34437h().m34443d(3, number) -> normalize; strip every non-digit character into a digits-only string (str). null -> abort.
  3. C21644s.m38848a(str, storage.m36434B()) -> look for a live flow for that number.
    • If found and now - (lastMessageMS == 0 ? startMS : lastMessageMS) <= TimeUnit.MINUTES.toMillis(remote "messageAssistantIdleDurationMin") -> abort ("sendStartingMessage canceled, conversion already alive").
    • Else C21644s.m38850c(oldFlow, storage) (remove it) and continue.
  4. initialMessage = AbstractC21633h.m38834b(type, AbstractC21633h.m38833a(type)) - the personalised template from prefs, else the resource default.
  5. Build RespondSmsBody(args.getContactName(), normalizedDigits, emptyList(), x5.p.m38844a(self), 0, AbstractC9039h.m10663t("")) - history is empty and messagesInSession = 0 on the opening call.
  6. api.m38828a(body, this) -> POST http://numberlookup.instabridge.com/respond-sms.
  7. Response handling:
    • EndConversation -> analytics ai_assistant_send_finish_on_start_flow{number, source}; nothing is sent.
    • NewMessage -> analytics ai_assistant_starting_message_sent{number, source, type}; then C21641p.m38846c(self, AbstractC21633h.m38834b(type, newMessage.getMessage()), normalizedDigits, args.getNumber(), args.getSource(), type) - sends the AI’s opening SMS immediately from the device.
  8. On any of JsonDataException / IOException / IllegalArgumentException / HttpException:
    • Log.e("invokeApiCall","error happened while loading", throwable)
    • C21641p.m38845b(..., "starting message", ...) -> analytics ai_assistant_error
    • analytics ai_assistant_starting_fallback_sent{error, number, source, type}
    • C21641p.m38846c(self, localFallbackMessage, ...) - sends the hard-coded fallback SMS from the device even when the network call failed.
x5.s -> C21644s (p646x5/C21644s.java) - assistant flow storeh5
  • Singleton f57883a, observers f57884b (ArrayList<C20689j> = the u7.j listener interface).
  • static MessageAssistantFlow m38848a(String number, List flows) - libphonenumber comparison (the x5.r when-map) over the stored flows; returns the first whose normalizedNumber matches.
  • static void m38849b(MessageAssistantFlow, r0.a storage) - add/update: removes any existing flow for the same normalizedNumber, inserts the new one, sorts with u7.q(4), truncates to 50 entries, serialises with zk.l and writes SharedPreferences("MessageAssistantStorage").putString("STARTED_FLOW_CONTACTS", json). Notifies observers via RunnableC19318r(..., 25).
  • static void m38850c(MessageAssistantFlow, r0.a storage) - remove: filters out the matching normalizedNumber and rewrites STARTED_FLOW_CONTACTS.
Remaining x5 classesh5
FileRealKindPurpose / key members
C21635jx5.jKotlin objecttoString() == "Assistant" - marks an SMS row the assistant itself sent.
C21636kx5.kKotlin objecttoString() == "Caller" - marks a row from the human caller.
InterfaceC21637lx5.lmarker interfaceSender marker for x5.i.
C21634ix5.idata classMessage(body: String, sender: x5.l, date: Long); toString() = "Message(body=..., sender=..., date=...)".
AbstractC21638m / AbstractC21643rx5.m / x5.rsynthetic when-mapsKotlin when dispatch tables over x5.l and over the 5 start-types.
C21631fx5.fFragment (R.layout.fragment_assistant_settings)Settings screen. Switches ES_spam/ES_missed_call/ES_declined + ET_spam/ET_missed_call/ET_declined/ET_default/ET_demo. m38829t0(type) returns "on" / "on_personalized" / "off". Analytics ai_assistant_settings_pageview / ai_assistant_settings_save. Toggling ES_spam also drives the block-spam-calls toggle (assistant_spam_blocking_enable_msg / assistant_spam_blocking_disable_msg, RunnableC19874r(16) / RunnableC20632l0(...,19)).
C21629dx5.dTextWatcher500 ms debounce on the personalised-message edit boxes.
C21630ex5.eAnimator.AnimatorListenerRestores maxLines=1 / wrap-content after the grow animation.
C21628cx5.clambdamo37220X() closes the assistant settings fragment.
C21645tx5.tCoroutineScope for MessageAssistantReplyWorker.doWork
C21646ux5.uCoroutineScope for MessageAssistantStartFlowWorker.doWork
InterfaceC21632gx5.g@interface (SOURCE retention)String[] f57852b = {"DEFAULT","MISSED_CALL","DECLINED_CALL","SPAM_CALL","UNKNOWN_CALLER"} - the canonical trigger-type enum.

3. Package wo (disk p636wo, 10 files) - coroutine bases + the assistant Retrofit containerh4

wo.f -> C21474f (p636wo/C21474f.java) - CRITICALh5

public C21474f(Context context) (lines 230-338) builds the whole assistant HTTP stack:

  • C19225b0 (OkHttpClient): 1-minute connect/read/write timeouts, retryOnConnectionFailure(true).
  • C19794b(context) - an OkHttp Interceptor (logging/diagnostics).
  • C15950q(3) - Moshi.Builder registering:
    • RespondSmsResponse.class -> new RespondSmsAdapter(moshiProvider)
    • MessageAssistantSource.class -> new MessageAssistantSourceAdapter(moshiProvider)
    • MessageAssistantSourceKey.class -> al.C0155a (enum adapter)
  • C22248e0(moshiBuilder) - the Moshi instance (also used for the storage JSON).
  • c18533l.m34728g("http://numberlookup.instabridge.com/", null); - CLEARTEXT HTTP base URL (line 295).
  • Validates the trailing /, builds the dynamic proxy for x5.a (Proxy.newProxyInstance(..., new m8.j(invocationHandler))).
  • Fields: f57242b / f57243c = Moshi, f57244d = the x5.a proxy.

Also contains non-assistant merged branches: a file-identity migration (m38665a, m38666b) and CrackleRtbInterstitialAdListener callbacks (onAdClicked/onAdDismissed/onAdDisplayed/onAdFailedToDisplay/onAdImpression/onAdLoadFailed/onAdLoadSucceeded).

Remaining wo classesh5
FileRealPurpose
AbstractC21469awo.aContinuationImpl base for all assistant suspend functions (create(Object, Continuation), invokeSuspend).
AbstractC21471cwo.cScopeCoroutine-like base used by the two workers.
AbstractC21475gwo.gabstract job base.
AbstractC21476hwo.hAbstractC21469a + Job support.
AbstractC21477iwo.ibase for DispatchedTask continuations.
AbstractC21478jwo.jbase for the assistant’s x5.n / x5.o coroutines.
C21470bwo.bKtor CoroutineContext element dispatch (vo.b).
InterfaceC21472dwo.dDisposableHandle.
InterfaceC21473ewo.eCoroutineExceptionHandler.

4. Package r0 (disk p511r0, 1 file) - MessageAssistantStorage (merged god-class)h4

r0.a -> C19813a (p511r0/C19813a.java) - relevant constructor & methodh5
  • C19813a(Context ctx, int 22) (lines 2106-2124):
    • this.f50969b = ctx.getSharedPreferences("MessageAssistantStorage", 0)
    • this.f50970c = new zk.e0(new p246dh.C15950q(3)).m39629a(MessageAssistantFlow.class, bl.f.f3151a, null) - a Moshi JsonAdapter<MessageAssistantFlow>
  • public List m36434B() (line 450):
    • reads STARTED_FLOW_CONTACTS; if null returns an empty list
    • deserialises with the adapter
    • normalises any entry with lastMessageMS == 0 via MessageAssistantFlow.copy$default(..., 0L, null, startMS, null, 11, null)
    • returns List<MessageAssistantFlow>
  • The remaining ~2200 lines are a ProGuard merge of ~25 unrelated types (BillingClient, WebView, Bitmap, transport, ads…). Only the two members above matter to C3.

5. AI SMS assistant - first-party DTOs (verified field by field)h4

Source: com\eyecon\global\DefaultDialer\assistant\ (19 files). These are readable (not obfuscated) so they are the ground truth for the wire format.

5.1 RespondSmsBody (.../dto/RespondSmsBody.java)h5

Kotlin data class. Moshi @Json(name=...) on every field:

Wire fieldTypeSource at startSource at follow-up
contactNameString (non-null)MessageAssistantStartFlowArgs.getContactName()(register reuse ambiguity - see the x5.n caveat in section 2)
numberString (non-null)digits-only normalised numberflow.getNormalizedNumber()
messagesList<RespondSmsBody.Message> (non-null)C19750a0.f50848a = empty listlast <= 50 messages from content://sms/inbox + content://sms/sent
localeString (non-null)x5.p.m38844a(self) = configuration.locales[0].toLanguageTag()same
messagesInSessionint0messages.size()
userNameString (non-null)AbstractC9039h.m10663t("") (SharedPreferences key android:hint_username)same

Nested RespondSmsBody.Message(@Json("role") Role, @Json("message") String) with enum Role { User, System }.

5.2 RespondSmsResponse (.../dto/RespondSmsResponse.java)h5

Sealed interface with two implementations:

  • NewMessage(@Json("message") String message)
  • EndConversation (empty)

RespondSmsAdapter.fromJson(q) discriminates on the kind field:

  • "finishConversation" -> EndConversation
  • "newMessage" -> NewMessage
  • anything else -> throw new JsonDataException()
5.3 MessageAssistantFlow (.../dto/MessageAssistantFlow.java)h5

Kotlin data class: startMS: Long, normalizedNumber: String, lastMessageMS: Long, startType: String. getTypeAutoOrManual() derives "manual" when startType is null or "DEFAULT", else "auto" - the same rule as x5.h.m38837e.

5.4 MessageAssistantSource (.../dto/MessageAssistantSource.java)h5

Sealed interface getKey(): MessageAssistantSourceKey. 10 subclasses, each with a no-arg constructor defaulting key:

SubclassDefault key
IncomingCallFullscreenIncomingCallFullscreenKey
IncomingCallNotificationIncomingCallNotificationKey
IncomingCallPopupIncomingCallPopupKey
AfterCallAfterCallKey
MissedCallPopupMissedCallPopupKey
FollowUpFollowUpKey
CallDeclinedAutoUseCallDeclinedAutoUseKey
CallMissedAutoUseCallMissedAutoUseKey
SpamCallAutoUseSpamCallAutoUseKey
UnknownCallerAutoUseUnknownCallerAutoUseKey

MessageAssistantSourceAdapter.fromJson dispatches MessageAssistantSourceKey.valueOf(str).ordinal() -> cases 1-10; anything else throws NoWhenBranchMatchedException. toJson dispatches by instanceof (cases: AfterCall, FollowUp, IncomingCallFullscreen, IncomingCallNotification, IncomingCallPopup, MissedCallPopup, CallDeclinedAutoUse, CallMissedAutoUse, SpamCallAutoUse, UnknownCallerAutoUse, else null).

5.5 IncomingSMSDTO / IncomingSMSContainerDTOh5
  • IncomingSMSDTO(originatingAddress: String, body: String, startedFlow: MessageAssistantFlow)
  • IncomingSMSContainerDTO(messages: List<IncomingSMSDTO>)
  • MessageAssistantStartFlowArgs(number: String, contactName: String, source: MessageAssistantSource)
5.6 Orchestration classesh5
ClassRole
MessageAssistantBroadcastReceiveronReceive for android.provider.Telephony.SMS_RECEIVED (manifest: android:permission="android.permission.BROADCAST_SMS", exported="true"). Extracts SmsMessage[], and for each message builds an IncomingSMSDTO only if a tracked flow exists for that sender (x5.s.m38848a(originatingAddress, storage.m36434B())), wraps the list in IncomingSMSContainerDTO, serialises it to the sms_bundle work-data key and enqueues MessageAssistantReplyWorker (network CONNECTED).
MessageAssistantReplyWorker (CoroutineWorker)Reads sms_bundle, deserialises, builds C21641p + wo.f, then AbstractC19432e0.m35769N(wp.d /* Dispatchers.IO */, new x5.n(assistant, messages, null), scope) -> returns success() / failure().
MessageAssistantStartFlowWorker (CoroutineWorker)Reads type (default "DEFAULT") + args_bundle, builds the same, runs x5.o on Dispatchers.IO.
OnboardingActivityOne-shot tutorial. Sets SP_KEY_IS_AI_ASSISTANT_NEED_SHOW_TUTORIAL=false on create; shows R.layout.activity_assistant_onboarding; on destroy fires ai_assistant_tutorial_save_settings{state, spam, declined, missed}. Static C0(Activity, String source).
GeneratedJsonAdaptersMoshi codegen output for the DTOs.

6. Package l7 (disk p399l7, 5 files) - phone numbersh4

Correction of a common assumption: l7 is NOT MyPhoneReceiver. MyPhoneReceiver is the readable first-party class com\eyecon\global\Call\MyPhoneReceiver.java. l7 is the libphonenumber wrapper + cache.

FileRealKindPurpose & key members
C18311al7.avalue classCached number info: a = cli, b = cis (m34445f), c = national format (m34441b), d = isPossibleMobile (m34450o). Constructors: (String cli, String iso) and from Gson {cli, cis, formatByNational, isPossibleMobile}.
C18312bl7.bPhoneNumberHelper (ThreadLocal singleton via C16466e0(7))m34437h() returns the per-thread instance. Instance methods: m34440a(String) national format in the default ISO; m34441b(String cli, String iso) national format (honours a PrivateNumber* prefix, consults the l7.e cache, then libphonenumber); m34442c(String) = m34440a("+"+cli); m34443d(int fmt, String); m34444e(String) CIS/E.164; m34445f = m34444e(cli, defaultIso); m34446k country code; m34447l geocoded display name (via yj.a); m34448m isMobile; m34449n/m34450o isPossibleMobile (libphonenumber type 2 or 3); m34451p isValidNumber; m34452q(String) = isValidNumberWithReason (the gate in front of every pic call); m34453r parse into the C21782j carrier object. Statics: m34436g(int) region for a country code, m34438i(String) pretty “Name, Country”, m34439j(String) call type (1 = VOICE, 2 = SMS).
C18313cl7.cDiskLruCacheWrapper250 MB (262144000L) DiskLruCache wrapper plus a job counter. m34454a() lazily opens the cache; mo34455b(key) read; mo34456c(key, sink) write with in-flight dedup; m34457d(HashMap) decrements the counter and, at zero, installs the map and persists it.
C18314dl7.dlambdal7.e.m34459b(...) on two executor variants.
C18315el7.ePhoneNumbersCache singletonConstructor aborts if READ_CALL_LOG or READ_CONTACTS is missing ("< > canceled, missing permission"). Otherwise stores used_iso = AbstractC0062l0.m371z1() and kicks off a background scan. m34458a(self, map) persists {iso, data:[{cli,cis,formatByNational,isPossibleMobile}]} to SharedPreferences key SP_KEY_PHONE_NUMBERS_CACHE_V4. m34460c(String cli) = in-memory lookup. m34461e(String iso) = reset when the SIM country changes. m34462d(cursor,...) batches call-log numbers in chunks of max(200, count/5 or count/10).

7. Package t5 (disk p554t5, 12 files) - PersonDataLoader (caller-ID engine)h4

FileRealKindPurpose
C20343mt5.mloaderConstructors (tag, cli, callback) / (tag, cli, cis, callback) / (tag, C19862i, callback). Normalises cli -> cis via l7.b.m34444e and throws RuntimeException("cis is null") on failure. State: boolean[] f52907g = {false,false,true} = {name-done, photo-done, done}; int f52909i = pic size selector (1 = small, 2 = big/null); long f52917q = hard deadline (elapsedRealtime + TooltipDuration, 10 000 ms); boolean f52905e/f52906f = fetch-name / fetch-photo; C17802b f52911k = crop config. Methods: m37278c(bool), m37279d(bool), m37280e(int, key) (bitmap memory-cache lookup), m37277b(Bitmap) (crop/round), m37281f() (release + unregister the C9080e screen receiver), m37282g() (remaining time budget), m37283h() (fire mo722s() when all three flags are set), m37284i() (kick off).
RunnableC20341kt5.kthe kick-off runnablecase 0: if a C19862i exists and is not “unknown”, immediately deliver the cached avatar; else, if f52912l (address-book path), use the C19854e executor via RunnableC19542e0(24,...), else DBContacts.f11707J.m10142n(cis, useAddressBook, C18716e(...,15,...)). case 2 (default): with no local contact -> mo716Q(null) then w6.h.m38433f(tag, !useAddressBook, cli, true, C18622a(...,29)) (the getnames.jsp name lookup), then the photo from the memory cache or w6.h.m38434g(tag, size, cis, t5.j).
C20340jt5.jm7.o callbackmo590e(Bitmap, int code) - caches the bitmap, crops it, then posts RunnableC17219d1(this, cacheKey, 17, bitmap) after m37282g() ms.
C20335et5.et5.h implementation = the per-call caller-ID recordFields: a = loader, c = C19862i (address-book contact), d/e = cli/cis, f = received name, h = received photo, g = isContact, i = photo-pending flag, j = x7.d spam entry, k = Facebook id, l = social ids. Methods: m37266b(boolean capitalize) (display name), m37267c() (fall back to the formatted number), m37268d(EnumC19861h0) (social id), m37269e() -> spam_type int (-1 when there is no spam entry), m37265a/m37270f (attach/detach a view holder). Callback mo721c0(AbstractC16130b) extracts the resolved name column and CB_KEY_SPAM -> x7.d - this is where the caller-ID result and the spam verdict land.
C20337gt5.gPersonDataCacheSQL over the names_photos_cache table: columns cli_in_server, modify_time, spam_type, DATA_TYPE_{NAME,SMALL_PHOTO,LARGE_PHOTO} (EnumC20336f).
EnumC20336ft5.fenumDATA_TYPE_LARGE_PHOTO, DATA_TYPE_NAME, DATA_TYPE_SMALL_PHOTO.
C20342lt5.lAbstractC16130bphoto-thumbnail (photo_uri, photo_thumbnail) load.
RunnableC20331a / RunnableC20332bt5.a / t5.bmerged runnablesnotify the attached view holders when name/photo/spam arrive.
AbstractC20333ct5.cabstract holderbase for in-call feed holders (u5.q0, u5.n0, u5.r0, u5.j0).
InterfaceC20334d / InterfaceC20338ht5.d / t5.hinterfacesholder callback / data callbacks (Q,U,V,a,c0,s,w).

8. Flow 1 - Incoming-call interception -> caller-ID lookup -> display -> spam decisionh4

Chain, with real class and method names:

  1. com.eyecon.global.Call.MyPhoneReceiver.onReceive(Context, Intent) (MyPhoneReceiver.java:37-54) - exported, priority MAX_INT, no permission.
    • Accepted actions: android.intent.action.NEW_OUTGOING_CALL, com.eyecon.global.action.INTENT_ACTION_START_CALL_WHATSAPP, android.intent.action.PHONE_STATE, com.eyecon.global.action.CALL_SCREENING_NEW_CALL.
    • On match: C16945i.m32771c(new im.RunnableC17334k(action, ctx, extras, false, 7)).
  2. im.RunnableC17334k case 7 (im/RunnableC17334k.java:868-1111):
    • CALL_SCREENING_NEW_CALL and INTENT_ACTION_START_CALL_WHATSAPP are remapped to android.intent.action.PHONE_STATE (lines 871-873).
    • CallStateService.m10199x() (default dialer installed) -> the INTENT_KEY_CALLER_ID_NOTIFICATION_ENABLE extra.
    • Builds Intent(ctx, CallService.class) action EYECON_INTENT_ACTION_CALL, copies the attacker’s extras verbatim, adds EYECON_INTENT_KEY_ACTION, then starts CallService.
    • Reads the state machine purely from broadcast extras (lines 900-918): String string3 = extras.getString("state"); string = extras.getString("incoming_number"); EXTRA_STATE_IDLE -> 0, EXTRA_STATE_OFFHOOK -> 2, EXTRA_STATE_RINGING -> 1; anything else -> RuntimeException("Not expected call state = "+state3) which is only logged.
    • cis = C18312b.m34437h().m34444e(incoming_number) (libphonenumber E.164 normalisation).
    • i10 == 1 (RINGING) coming from IDLE/RINGING:
      • C18302h.f45765i.m34431c(new C16937a(incoming_number, cis, 3, CallStateService), number) - caller-ID lookup kicked off
      • CallStateService.m34677e(cli, cis)
      • if remote IncomingCallPrefetchMode == "when ringing" -> BaseAfterCallActivity.m9925o1("Call started", null) + AbstractC16794a.m32647J("Call started", null)
      • CallStateService.m34675c(cli, cis), m34678f(cis, cli, EnumC17759a.RINGING_INCOMING_CALL)
    • i10 == 0 (IDLE) coming from RINGING/OFFHOOK: CallStateService.m34674b(1|2|3, cli, cis) - 1 = declined, 2 = missed, 3 = answered.
    • i10 == 2 (OFFHOOK): marks f54442d = wasIncoming, records the call via AbstractC19327e.m35649f(cis) (the in-call map) and fires m34678f(cis, cli, ACTIVE_INCOMING_CALL_ANSWERED / ...WAITING_CALL_ANSWERED).
    • Finally CallService.m10051d(intent), and for NEW_OUTGOING_CALL / RINGING / IDLE a 1-second-debounced AbstractC20656z.m37804b(false) (after-call display kickoff).
  3. C16937a case 3 (p315h7/C16937a.java) -> C18302h (p397l5/C18302h, the caller-ID orchestrator) -> t5.m / t5.k.
  4. t5.k case 0 / case 2 -> DBContacts.f11707J.m10142n(cis, useAddressBook, cb) (address book) and/or w6.h.m38433f(tag, ..., cli, true, C18622a(...,29)) -> GET https://api.eyecon-app.com/app/getnames.jsp and w6.h.m38434g(tag, size, cis, t5.j) -> GET .../app/pic?cli=<cis>&is_callerid=true&size=<small|big|null>&type=0&src=<tag>&cancelfresh=0&cv=vc_749_vn_4.2026.04.23.1145_a.
  5. t5.j.mo590e(bitmap, code) -> t5.k -> t5.e.mo723w(bitmap) / C18622a(...,29) -> t5.e.mo721c0(AbstractC16130b) which sets f52873f (name) and f52877j = the x7.d spam entry obtained from the callback map key "CB_KEY_SPAM".
  6. t5.e.mo722s() -> CallStateService.mo10213d / mo10214g / mo10215h / mo10216l (all four re-verify that C21776d still reports the same CLI) -> CallStateService.m10208I(call, cis, false) -> m10209J(c20616d0, call, cis, false), which rebuilds the ongoing notification: channel eyecon_normal_calls_v3 for a popup vs eyecon_quiet_calls_v3 otherwise, setFullScreenIntent(true), and three actions - Answer, Decline, and AI-assistant-or-Ignore.
  7. CallStateService.onCallAdded(Call) (lines 1557-1788) - the real telephony-side interception (MICO is the default dialer, so InCallService.onCallAdded fires):
    • AbstractC19642b.m36086b(cli) - if the number is queued to be flushed: w0.a.m38343q(call,"REJECT") and return.
    • m10207G(call) creates the u5.d0 Call.Callback plus the C20335e (t5.e) record; m10212c(...) adds it to callsDetailsForAfterCall.
    • spamBlockIfNeeded (lines 1604-1721) - see section 9.
    • Then AbstractC21232a.m38319D(call2) init, C3465f analytics incoming_call / outgoing_call, AbstractC16794a.m32644F(...), the call-bubble ViewOnTouchListenerC20643r, a PARTIAL_WAKE_LOCK (6 h) and a PROXIMITY_SCREEN_OFF_WAKE_LOCK (6 h).
  8. Spam decision (section 9), then AbstractC19327e.m35649f(cis) registers the live call and m10208I(call, cis, z13) renders the first notification.
  9. Display: the in-call UI is com.eyecon.global.DefaultDialer.CallActivity; the incoming-call overlay is k6.s (CallerIdWindow). k6.s.m34080B() (lines 454+) computes z11 = !isInCallScene && isIncomingCallState, z12 = remote("use_ai_assistant") && m10664u(app) && z11, shows EB_ai_assistant only when z11 && z12, and fires analytics ai_assistant_button_show{source=IncomingCallPopup} once per window. The caller-ID bubble path is k6.j (IB fullscreen ad shown/not shown for key: %s) and k6.i (waitForCallActivityOnCreate#onReceive - activity shown). Name/photo/spam unpacking on the UI side is k6.b (C17761b), which reads CB_KEY_SPAM, received_name, received_photo, suspicious_spam, contact.

9. Flow 2 - Spam classification and blockingh4

9.1 The spam model - x7.d -> C21657d (p648x7/C21657d.java)h5

implements Comparable. Fields (DB columns resolved via p008a7/AbstractC0039a):

FieldColumnMeaning
a (f57926a)contact_id (f210d)row id, nullable
b (f57927b)cli_in_server (f216f)the CIS (practical primary key of the table)
c (f57928c)cli (f213e), @Json("cli")national format (l7.b.m34442c)
d (f57929d)name (f219g), @Json("name")suggested / user name
e (f57930e)create_timestamp (f200Z0)epoch ms
f (f57931f)spam_type (f254r1)int, see 9.2

Static predicates:

  • m38861e(int t) -> t == 1 || t == 3 = “is spam”
  • m38862f(int t) -> t == 2 = “is suspicious”

m38863a() renders the row to ContentValues; m38860b(Cursor) returns the six column indices; compareTo sorts by cli_in_server; toString() returns the CIS.

9.2 Spam-type enumeration (OBSERVED, from the x7.v.m38872e analytics labels)h5
spam_typeMeaning
-1Unknown if spam
0Not a spam by server
1Spam by server
2Suspicious as spam
3Spam by user
4Not a spam by user

UI tag mapping - d.a.m31044c(int spamType, String cli) (p230d/AbstractC15790a.java:68-76) and d.a.m31045d(int, C20616d0): empty cli / private number -> 5 (unknown); isSpam -> 2; isSuspiciousSpam -> 3; otherwise 1 (not spam). d.a.m31046e(C19862i): blocked -> 4; private number -> 5; isSpam -> 2; isSuspiciousSpam -> 3; otherwise 1.

9.3 The DAO - x7.q -> C21670q (p648x7/C21670q.java)h5
  • Singleton f57971b; C16945i f57973a = new C16945i(1,1,"SpamDB",true); in-memory list f57972c.
  • static C21657d m38870b(String cis) -> select * from spam_list WHERE cli_in_server = '<cis>' (raw string concatenation into SQL; the input is libphonenumber-normalised CIS, so this is internal-only, but it is still string-built SQL).
  • C21657d m38871a(String cis) -> blocking DB read on the SpamDB executor; used by CallStateService.onCallAdded.
9.4 Block-list (distinct from spam_list)h5

DBContacts (first-party, com.eyecon.global.Contacts.Database.DBContacts) holds the block list, read as C16470g0 rows via RunnableC17820k(18, ...) on the DBContacts.f11708K executor. x7.l (C21665l) merges the two lists for the Spam&Block pager: spam rows (x7.d, spam_type == 3) and blocked rows (C16470g0), de-duplicated by cis.

9.5 Blocking decision at call time - CallStateService.onCallAdded lines 1604-1721 (OBSERVED)h5
if (AbstractC21232a.m38318C(call2)) { // private number
log("spamBlockIfNeeded canceled, private number");
} else {
direction = (SDK >= 29 ? call.getDetails().getCallDirection() mapping : c20616d0M10207G.f54441f);
if (direction == 2) {
log("spamBlockIfNeeded canceled, not incoming call");
} else {
aiEnabled = AbstractC21633h.m38838f("SPAM_CALL"); // assistant auto-use for spam
blockEnabled = MyApplication.m10348j().getBoolean("SP_KEY_BLOCK_SPAM_CALLS", false);
if (aiEnabled || blockEnabled) {
spamEntry = C21670q.f57971b.m38871a(c20616d0M10207G.f54437b); // by CIS
if (spamEntry != null && C21657d.m38861e(spamEntry.f57931f)) { // spam_type in {1,3}
if (aiEnabled) {
blockEnabled |= AbstractC21633h.m38842j(spamEntry.m38864c(), // cli (national)
spamEntry.m38865d()); // name
} // -> x5.v.m38851a(..., SpamCallAutoUse, "SPAM_CALL")
if (blockEnabled) {
c20616d0M10207G.f54445j = true; // mark "call blocked"
AbstractC21232a.m38343q(call2, "DISCONNECT"); // android.telecom.Call.disconnect()
CallActivity.m10158I0(...); // if the call UI is showing
m10202B(call2); // immediate onCallRemoved handling
log("spamBlockIfNeeded, blocked cis = " + cis);
return; // "startCall canceled, spam is blocked"
}
}
}
}
}

Blocking actions available (complete list):

  • Block number at call time - SP_KEY_BLOCK_SPAM_CALLS = true; Call.disconnect() on the incoming call. The toggle lives in x5.f.m38832w0 (assistant settings) and p397l5/C18297c case 0, which flips SP_KEY_IS_AI_ASSISTANT_SPAM_CALL_AUTO_USE_ENABLE and SP_KEY_BLOCK_SPAM_CALLS together.
  • Reject - w0.a.m38343q(call,"REJECT") -> Call.reject(false, "") (CallStateService.m10185f(); its fail-safe branch disconnects all calls when no target call can be found).
  • Number-level block - x7.v.m38873f(cli, cis, isSpam, spamType, name) writes to spam_list, to DBContacts, and posts the name/tag to the server.
  • Ring silence - onSilenceRinger() is overridden only to log; the practical silent mechanism is the notification-vs-quiet channel switch (eyecon_normal_calls_v3 / eyecon_quiet_calls_v3) plus Notification.PRIORITY_*.
9.6 Report / block flows to the serverh5
PathClassEndpointBody
Report spam (user)x7.v.m38873f(cli, cis, isSpam, spamType, name) -> C20337g.m37273d + bm.RunnableC3663m1POST https://api.eyecon-app.com/app/spamreport (tag reportSpam){"cli": <cis>, "name": <w6.h.m38429b(name)>, "is_spam": <bool>}
Report the block list (bulk)v7.f case 4 (p599v7/RunnableC20908f.java:328-345)POST .../app/spamreport (tag reportBlock){"type":"blocked", "blocked_numbers":[ ... ]}
Suggest name + spam tagx4.b.m38813f(cis, prevName, newName, tag) -> bm.RunnableC3589a.m3809aPOST .../app/resuggest_name{"cli", "previous_name"?, "suggested_name"?, "tag"?, "langs"} - tag is suppressed when "not_set" or "spam".
Block/unblock one numberInterfaceC21655b.mo3140g(str, str2, boolean) -> DBContacts(local)
Notify contacts about spamC21867v.m39106p() / m39108u() + RunnableC21865t / RunnableC20081f(local DBContacts + UI)the remote reportSpam flag gates the “add to spam” card
Bulk spam-list replacementw9.b case 10(local)wipes and reinserts spam_list, then writes SP_KEY_VERSION_SPAM_LIST
9.7 Spam tags offered by the UIh5

Spam report dialog (x7.v.m38877d, tags assigned near lines 470-490): TELEMARKETING, SCAM, ROBOCALL, SALES -> stored in f57995r and shipped as the tag field of resuggest_name. The dialog also exposes CB_block (block as well as spam) and a “suggested name” edit box, and shows a 3-second Lottie “waiting” animation (R.raw.lottie_spam_1..4).

After-call user-feedback dialog (x4.a0, tags assigned in w9.b case 5): BUSINESS, spam, other, FRIEND -> stored in x4.a0.f57735V and shipped the same way.

9.8 Remaining x7 classesh5
FileRealPurpose
EnumC21668ox7.oenum SPAM, BLOCK - the two Spam&Block pager tabs.
C21670qx7.qthe DAO, see 9.3.
C21654ax7.aReportAndShareDialogFragment (R.layout.report_and_share_dialog) showing the reported name/phone plus a Share button. Static m38858q0(cli, cis, name).
C21656cx7.cReportSpamDialogFragment (R.layout.report_spam) implementing x7.u; static m38859i0(cli, cis, name, source, spamType, FragmentManager); shown as "ReportSpamDialog".
C21661hx7.hBlockAdapter (RecyclerView) over R.layout.block_cell_layout; m38866a(int, Runnable) (remove + empty-callback), m38867b(int, C18299e) (update). Buttons read R.string.block / R.string.unblock.
C21660gx7.gViewHolder (blockLayout, blockTag, TV_name, TV_number, EA_contact_image).
C21659fx7.floader callback - pulls CB_KEY_SPAM + name from the async result and updates the avatar with d.a.m31044c(...).
C21664kx7.klambda x7.b - inserts a freshly reported number at the head of the Spam list and refreshes the empty-state view.
C21665lx7.lAbstractC16130b for the Spam&Block fragment load (spam list + blocked list merge).
C21666mx7.mViewPager.OnPageChangeListener - analytics blockSpam_section_swipe{Section: to_block|to_spam}.
C21667nx7.nadd/remove result handler for the Block/Spam adapters (toasts "Failed with error code: ..." on failure).
C21669px7.pPagerAdapter - page 0 = Block, page 1 = Spam (R.string.block / R.string.spam).
RunnableC21663jx7.jprogress-dialog show/dismiss.
ViewOnClickListenerC21662ix7.iadd-number / add-spam button handlers.
ViewOnFocusChangeListenerC21673tx7.tanalytics Spam dialog edittext, has focus = <bool>.
RunnableC21672sx7.sfocuses the “suggested name” field / dismisses the waiting dialog after 3 s.
C21675vx7.vSpamReportHandler - the whole report dialog behaviour (see 9.6 / 9.7). Analytics Spam user feedback{Source, Set as spam, Set as block, Original spam type} and SpamReportHandler view-timing events. m38879h() calls AbstractC21601b.m38813f(...) (i.e. ships the tag).
InterfaceC21674u / InterfaceC21655bx7.u / x7.bdialog->fragment / fragment->list callbacks.
9.9 w9 (disk p623w9, 2 files) - SpamDB runnablesh5
  • w9.a -> RunnableC21300a (p623w9/RunnableC21300a.java)
    • case 1: read one spam entry (C21670q.m38870b).
    • case 2: upsert - update("spam_list", contentValues, "cli_in_server = ? "); on 0 rows insert("spam_list", ...); keeps C21670q.f57972c in sync; entries with spam_type == 4 are removed from the in-memory list (i.e. “not spam by user” is not kept as a user-spam entry).
    • case 4: incrementSpamCardShownCount - increments the named SharedPreferences counter; when >= remote(counterKey) it resets to 0 and shows the report-spam card.
  • w9.b -> RunnableC21301b (p623w9/RunnableC21301b.java)
    • case 5: builds the after-call tag buttons (BUSINESS / spam / other / FRIEND).
    • case 6: enqueues a request into table_api_requests.
    • case 7 / case 8: populate the Spam&Block pager.
    • case 9: select * from spam_list order by cli_in_server (full load).
    • case 10: wipe + reinsert spam_list, then write SP_KEY_VERSION_SPAM_LIST (server-driven spam-list replacement).
    • case 11: merge the in-memory list with SELECT * FROM spam_list WHERE spam_type = 3 ORDER BY cli_in_server.

10. Flow 3 - AI SMS assistant, end to endh4

10.1 Trigger sources (all 10 MessageAssistantSource variants and where they are produced)h5
SourceProduced atGating
IncomingCallFullscreenu5.a (C20609a) cases 1/default -> CallActivity.m10167N0(...); w5.c C21258c.java:811use_ai_assistant remote flag
IncomingCallPopupk6.s (CallerIdWindow) lines 1593, 1656, 1720use_ai_assistant and SMS permissions
IncomingCallNotificationCallStateService.m10209J line 1076when !use_ai_assistant && !f11855i, the notification’s third action becomes “Ignore” instead of “AI assistant”
MissedCallPopupp492q5/RunnableC19573b.java:79, ViewOnClickListenerC19574c.java:65m10664u -> m10651h permission dialog
AfterCallx4.e (RunnableC21604e) case 4, line 129!m10666w() (tutorial already seen)
FollowUpx5.n (the assistant itself, on each follow-up)-
CallDeclinedAutoUsex5.h.m38840h <- CallStateService.m10185f line 201SP_KEY_IS_AI_ASSISTANT_DECLINED_AUTO_USE_ENABLE and call age < 2 min
CallMissedAutoUsex5.h.m38841i <- CallStateService.m10202B line 528 (DisconnectCause code 5 = missed)SP_KEY_IS_AI_ASSISTANT_MISSED_CALL_AUTO_USE_ENABLE and call age < 2 min
SpamCallAutoUsex5.h.m38842j <- CallStateService.onCallAdded line 1634SP_KEY_IS_AI_ASSISTANT_SPAM_CALL_AUTO_USE_ENABLE and the number’s spam_type in {1,3}
UnknownCallerAutoUsereferenced by x5.g’s type set and by the Moshi adapter; no in-cluster construction site found. INFERRED: produced by an out-of-scope caller-ID path that classifies numbers not in the address book.SP_KEY_IS_AI_ASSISTANT_UNKNOWN_CALLER_AUTO_USE_ENABLE
10.2 Start flowh5
UI / auto trigger
-> x5.v.m38851a(ctx, MessageAssistantStartFlowArgs(number, contactName, source), type)
analytics ai_assistant_button_click{source}
serialise args -> WorkManager Data{args_bundle, type}, constraints NETWORK CONNECTED
-> MessageAssistantStartFlowWorker.doWork (Dispatchers.IO)
-> wo.f(ctx) = Retrofit/Moshi on http://numberlookup.instabridge.com/
-> x5.o.invokeSuspend
SMS-permission gate -> ai_assistant_missing_sms_permission
normalise the number; find a live flow (idle window = remote "messageAssistantIdleDurationMin")
initial text = prefs override (SP_KEY_AI_ASSISTANT_INITIAL_MESSAGE_FOR_<TYPE>)
POST /respond-sms {contactName, number, messages: [], locale, messagesInSession: 0, userName}
|- EndConversation -> ai_assistant_send_finish_on_start_flow (nothing sent)
|- NewMessage -> ai_assistant_starting_message_sent
| -> x5.p.m38846c -> SMS sent from the device
|- throwable -> ai_assistant_error + ai_assistant_starting_fallback_sent
-> the local fallback message is sent from the device anyway
10.3 Follow-up flowh5
MessageAssistantBroadcastReceiver (android.provider.Telephony.SMS_RECEIVED, BROADCAST_SMS permission)
-> for each SmsMessage: only when a tracked flow exists for that sender
-> WorkManager Data{sms_bundle} -> MessageAssistantReplyWorker (Dispatchers.IO)
-> x5.n.invokeSuspend per IncomingSMSDTO
permission gate again
read content://sms/sent AND content://sms/inbox
projection {"_id","address","date","body"}
selection "address LIKE ? AND date > ?"
args ["%"+address+"%", String.valueOf(flow.startMS)]
sortOrder "date DESC"
sort the merged list; map sender (Assistant -> Role.System, Caller -> Role.User)
append the live message as Role.User if it is not already the newest
truncate to the last 50
POST /respond-sms {..., messages, messagesInSession: size, userName}
|- EndConversation -> x5.s.m38850c (remove the flow) + ai_assistant_ended_on_following_up
|- NewMessage -> flow.lastMessageMS = now, re-saved + ai_assistant_following_up_message_sent
| -> x5.p.m38847d -> SMS sent from the device
|- throwable -> ai_assistant_error{messagetype: "follow up message", ...}
10.4 Auto-reply conditions - complete list (OBSERVED)h5
  1. The feature must be enabled remotely - AbstractC15804d.m31081b("use_ai_assistant") (the d5 remote-config bag) gates every entry point, and x5.h.m38838f(type) gates all four auto-use variants.
  2. Runtime SMS permissions - AbstractC9039h.m10664u(ctx) requires READ_SMS, SEND_SMS and RECEIVE_SMS. Where the check fails, a FeaturePermissionsDialog (C17517c, strings sms_permission_required / sms_permission_msg, icon R.drawable.ic_messages) is shown first (AbstractC9039h.m10651h), and only after the permission does p397l5/C18297c.mo34422Z start the flow. If the user denies, ai_assistant_missing_sms_permission{source} is fired from C18297c.mo34423a0.
  3. Auto-use (no tap) variants: SP_KEY_IS_AI_ASSISTANT_{DEFAULT,DECLINED,UNKNOWN_CALLER,MISSED_CALL,SPAM_CALL}_AUTO_USE_ENABLE.
  4. Call-age window for declined/missed auto-use: System.currentTimeMillis() - callTimeMS < TimeUnit.MINUTES.toMillis(2).
  5. Spam auto-use additionally requires the spam row’s spam_type in {1, 3}.
  6. Conversation idempotence: a new start for a number that already has a live flow aborts ("sendStartingMessage canceled, conversion already alive").
  7. Caps: at most 50 flows in STARTED_FLOW_CONTACTS; at most 50 messages per request.
10.5 Assistant analytics event names (complete)h5

ai_assistant_button_click, ai_assistant_button_show, ai_assistant_settings_pageview, ai_assistant_settings_save, ai_assistant_tutorial_save_settings, ai_assistant_missing_sms_permission, ai_assistant_send_finish_on_start_flow, ai_assistant_starting_message_sent, ai_assistant_starting_fallback_sent, ai_assistant_following_up_message_sent, ai_assistant_ended_on_following_up, ai_assistant_error, ai_assistant_platform_sending_failed.

10.6 Assistant SharedPreferences keys (complete)h5

SP_KEY_IS_AI_ASSISTANT_NEED_SHOW_TUTORIAL; SP_KEY_AI_ASSISTANT_INITIAL_MESSAGE_FOR_{DEFAULT,DECLINED_CALL,UNKNOWN_CALLER,MISSED_CALL,SPAM_CALL}; SP_KEY_IS_AI_ASSISTANT_{DEFAULT,DECLINED,UNKNOWN_CALLER,MISSED_CALL,SPAM_CALL}_AUTO_USE_ENABLE; SP_KEY_BLOCK_SPAM_CALLS; SP_KEY_DID_CLICK_CALL_TTM_BTN; SharedPreferences file MessageAssistantStorage, key STARTED_FLOW_CONTACTS; the profile name at android:hint_username.

10.7 Remote-config keys used by the assistanth5

use_ai_assistant, messageAssistantIdleDurationMin.


11. Flow 4 - Call recording: capture -> storage -> uploadh4

11.1 Capture - there is no call-audio captureh5

See 0.2. The only AudioRecord in the APK is the voice-note (AMR) recorder, entered from the merged runnable v7.f case 11:

v7.f case 11:
Process.setThreadPriority(-19); // THREAD_PRIORITY_URGENT_AUDIO
io.kvh.media.amr.AmrEncoder.init(0);
AudioRecord ar = new AudioRecord(/* MIC */ 1, /* 8000 Hz */ 8000, /* CHANNEL_IN_MONO */ 16,
/* ENCODING_PCM_16BIT */ 2, /* buffer */ 16000);
runnableC21441d.f57147i = ar; // wl.RunnableC21441d
ar.startRecording();
runnableC21441d.f57145g = true;

Owned by p633wl/RunnableC21441d; playback at v7.f case 10 through AudioTrack (p633wl/RunnableC21439b, printing end of talking).

11.2 Storage - Documents/Eyecon/{RecordedNotes,RecordedCalls}h5

p260e5/AbstractC16098a (e5.a):

  • m31460c(int type) -> type == 0 ? <ExternalFiles>/RecordedNotes : <ExternalFiles>/RecordedCalls when AbstractC0042b0.m178a0() (external storage present), otherwise getFilesDir()/Eyecon/RecordedNotes|RecordedCalls; always mkdirs().
  • m31461d(String name, String dir) -> MediaStore insert; API >= 29 relative_path = Documents/Eyecon/<dir>, else legacy _data = <external>/<dir>/<name>; mime_type = audio/amr.
  • m31459b(int, Uri, String) (delete) and m31462e(Comparable src, String name, String dir) (move; returns the new Uri/File).

p260e5/C16102d (e5.d, AudioRecordsDataManager):

  • m31471e(boolean) scans MediaStore for rows under Documents/Eyecon/RecordedCalls/ or Documents/Eyecon/RecordedNotes/ (or _data LIKE '%<externalFiles>%' pre-29) whose _display_name matches .+__.+__[0-9]+(__s|__c)?(__-1|__1|__2)?__(ern|erc)\..*, opens each stream to prove non-emptiness, and parses C16104f metadata (throws ParseException on bad names).
  • m31473g(boolean) is the file-system variant (listFiles(C16100b(0/1))).
  • m31472f() -> select * from recorded_notes order by create_timestamp DESC.
  • Writes the SP_KEY_FIXED_RC_TIMESTAMP migration.

Startup scan: p008a7/RunnableC0070p0.m409b() (a7.d1 case b) creates both directories on SDK_INT <= 29 and then calls e5.d.m31467a(...).

11.3 Upload - Google Drive backup (the f5 package, out of cluster, but the sink is confirmed)h5

p282f5/C16442z (f5.z) m31962h obtains six pending Drive upload URLs via C16418b0.m31906a(...):

FieldPending nameMIMEProgress key
f40671dpending_temp_eyecon_backup_for_calls_records(per-file)PROGRESS_RECORDS
f40672epending_temp_eyecon_backup_for_notes_records(per-file)PROGRESS_RECORDS
f40673fpending_temp_eyecon_backup_for_ttm_audiosapplication/zip (<filesDir>/ttm.zip)PROGRESS_KEY_TTM
f40674gpending_temp_eyecon_backup_for_databaseapplication/x-sqlite3 (contactdb)PROGRESS_DATABASE
f40675hpending_temp_eyecon_backup_for_call_logsapplication/jsonPROGRESS_CALL_LOG
f40676ipending_temp_eyecon_backup_for_address_bookapplication/jsonPROGRESS_ADDRESS_BOOK

f5.w case 2 (records): AbstractC16098a.m31460c(1).listFiles(C16100b(4)) (RecordedCalls) plus AbstractC16098a.m31460c(0).listFiles() (RecordedNotes) - each file POSTed individually. Case 1 (TTM): zips AbstractC16733c.m32601u() into <filesDir>/ttm.zip and POSTs it as application/zip.

The Drive client is built at p282f5/C16418b0.java:134: new Drive.Builder(<scopes>, AbstractC15929a.f38989a, C19377c).setApplicationName("Eyecon").m45168build(). The destination is the user’s own Google Drive, so this is a user-initiated backup rather than exfiltration to MICO’s servers. It is nevertheless an outbound transfer of call-recording audio and of the plaintext contactdb SQLite file.

Analytics: Backup upload success, Backup upload failed{Has internet}, backup_info{account, timestamp} posted via w6.h.m38440m; the last successful timestamp is stored in SP_KEY_LAST_TIME_BACKUP.


12. Flow 5 - After-call feedback / follow-uph4

12.1 Launching the after-call screenh5

CallStateService.onCallRemoved -> m10202B(call):

  • C17164d0.f42661d.m33012k(null, true)
  • releases wake locks, cancels eyecon_quiet_calls_v3 / eyecon_normal_calls_v3, stopForeground(true)
  • m10203C(call) = showAfterCall (lines 557-691): if f11852f (dontShowAftercall, e.g. the TTM redirect number 97243727022) -> CallActivity.m10159J0(); if already shown -> skip. Otherwise it builds a C11856m JSON array of x4.o.m38816b(...) records, then calls BaseAfterCallActivity.m9928z1(this, json, jLastCallMS, isPremium, true).
  • x4.c (RunnableC21602c) picks the activity class: compact_after_call remote flag or SP_AFTERCALL_TYPE == 2 -> CompactAfterCallActivity; otherwise DefaultAfterCallActivity (or AfterCallWithNoAutoFinish when under the lock screen without a timer). Extras: INTENT_KEY_CALLER_LIST_JSON_ARR, INTENT_KEY_REQUEST_TO_START_TIME, INTENT_KEY_IS_APP_JUST_LAUNCHED, INTENT_KEY_IS_CALL_RECORD_ALREADY_SAVED, INTENT_KEY_SHOW_UNDER_LOCK_SCREEN_NO_TIMER, INTENT_KEY_IS_PREMIUM_USER. The intent is wrapped in a SplashInterstitialActivity for the app-open/interstitial ad.

x4.o.m38816b(...) JSON keys, in order: cli, name, facebookId, isContact, spam_type, contactId, callType, isWaitingCall, showAfterCallPhotoPicker, isAppJustLaunched, incomingORoutgoing, callDuration, isWhatsappCall, callStartTime, isTelegramCall (always FALSE), isUsedWithAiAssistance. x4.o.m38817a() = callType != -5 ? callType : incomingORoutgoing (fallback). x4.o.m38818c() = name, falling back to socialName for Telegram calls.

12.2 The feedback questions (x4.b.m38810c, AbstractC21601b)h5

AbstractC18259g.m34397c(i3) maps a question id to one of five layouts / handlers:

VariantLayoutYes actionNo action
0R.layout.yes_no_question_layoutafter_call_is_spam_yes_click -> start the AI assistant (OnboardingActivity if the tutorial is pending, else a MessageAssistantSource.AfterCall start)C21675v.m38873f(..., isSpam=false, spamType=4, name)
1R.layout.yes_no_question_layout (EBuserFeedbackYes / EBuserFeedbackNo)as aboveas above
2R.layout.yes_no_question_layout (YES = spam)after_call_is_spam_yes_clickafter_call_is_spam_no_click + m38873f(..., 4, ...)
3R.layout.spam_question_layout (confirm_contact_name)x4.b.m38812e(..., "correct_name_no" / "add_name", "after_call", ...) + m38813fsame
5string from AbstractC19949a.m36768f(i3) (nickname confirm)x4.b.m38812e(..., "nickname_yes", "after_call", ...) + m38813fsame

x4.b.m38812e(...) fires analytics "<src>_<event>_click", and for the five feedback events additionally starts a session replay (C22071c.m39334e("startSessionReplay", new C19867k0(20, ..., "USER_FEEDBACK"))) before showing x4.a0 (UserFeedbackDialog, tag "AC_UserFeedBac"). x4.b.m38811d(...) starts DummyActivity with action EYECON.INTENT_ACTION_THANK_YOU_DIALOG and startActivityForResult(..., 140). x4.b.m38808a(type, cis) -> af/RunnableC0127c case 13 -> adds the number to SharedPreferences("after_call_feedback_prefs") under the key "after_call_" + <question name>.toLowerCase(), so the question is never asked again for that number. x4.b.m38809b(int, AbstractC16130b, String) is the completion callback form.

12.3 The AI follow-up from the after-call screenh5

x4.e (RunnableC21604e) case 4:

if (AbstractC9039h.m10666w()) { // use_ai_assistant && SP_KEY_IS_AI_ASSISTANT_NEED_SHOW_TUTORIAL
OnboardingActivity.m10217C0(this, "aftercall");
} else if (number != null) {
AbstractC9039h.m10651h(this, new p397l5.C18297c(this,
new MessageAssistantStartFlowArgs(number, this.m9955T0(),
new MessageAssistantSource.AfterCall()),
new x4.h(this, 0)));
}

m10651h is the SMS-permission gate/dialog; C18297c.mo34422Z is x5.v.m38851a(..., "DEFAULT") followed by the follow-up Runnable. There is no automatic after-call follow-up without a user tap - the assistant is started from the after-call screen only through this button handler.

12.4 After-call to in-call transitionh5

Driven by SP_AFTERCALL_TYPE (0 = default, 2 = compact) and nj.o analytics after_call_switch_to_compact / after_call_switch_to_fullscreen. After-call ad refresh is driven by com.eyecon.global.AfterCall.AftercallAdRefreshWorker with its own broadcast action and an INTENT_KEY_SHOULD_REFRESH extra (x4.g case 2); x4.m implements the tiered refresh checkpoint.

12.5 Remaining x4 classesh5
FileRealPurpose
C21600a0x4.a0UserFeedbackDialog - tag chips (BUSINESS / spam / other / FRIEND), suggested-name field, restart checkbox, ebSave handled by x4.y.
C21606gx4.gmerged BroadcastReceiver: CLOSE_SYSTEM_DIALOGS (homekey / recentapps), ACTION_SCREEN_OFF, AftercallAdRefreshWorker.ACTION.
C21608ix4.iAbstractC16130b with a 3 s timeout that continues RunnableC21602c for the activity launch.
C21609jx4.jmerged ad-loading holder.
C21610kx4.kViewPager.OnPageChangeListener for the after-call pager (drives m9934D1/m9979x1/…).
C21611lx4.lafter-call hook dispatcher (mo9962a1 / mo9960Y0 / mo9965d1 / mo9961Z0).
C21612mx4.mAbstractC19051d tiered ad-refresh checkpoint (high / low tier).
C21613nx4.nad-slot Comparable (sorts by the acascore_<key> preference, then index).
C21617rx4.rafter-call PagerAdapter (per-call page; name/avatar binding; EB_name_edit).
RunnableC21607hx4.hcase 0 close/finish; default = share_my_location analytics + share.
RunnableC21618sx4.sbubble-options-after-call.
RunnableC21621vx4.vafter-call view helper.
RunnableC21622wx4.wSplashInterstitial ad sequencing (aftercall_interstitial_bubble, calls_since_last_ad, calls_since_last_app_open_ad, app_open_ad_feature_enabled).
ViewOnClickListenerC21603dx4.dno-op click listener (base-activity guard).
ViewOnClickListenerC21624yx4.yebSave -> validates R.string.error_message_save_name_and_tag, calls m38807r0(name), optionally requests restartEyecon.
ViewOnClickListenerC21625zx4.ztag-chip selector (sets f57735V / f57995r).
ViewOnLongClickListenerC21616q / ViewOnLongClickListenerC21620ux4.q / x4.uthree long-presses -> an “Ad stats” AlertDialog on CompactAfterCallActivity / DefaultAfterCallActivity (S8).

13. Flow 6 - TTM (Talk To Me) voicemail-to-text session lifecycleh4

13.1 Client-side components (in cluster)h5
ClassRole
n5.b -> C18703bSIM/operator descriptor. Parsed from JSON {operatorCanonicalId: Int, simAccountId: String, number: String, isConfig: Boolean}. Persisted in the remote-config blob under call_forwarding_config, keyed by simAccountId.
n5.c -> C18704c (CallForwardingManager)Singleton f47387b. Its static initialiser parses call_forwarding_config. m35046a() - disable call forwarding for every configured SIM: resolves ttm_countries_configuration -> the device’s country name (lower-cased) -> enabled_carriers[] -> match canonical_id -> read disable_code -> execute the carrier USSD via f5.C16440x.m31948s(app, disableCode, "ttm-disable") -> mark isConfig = false and persist. Throws com.eyecon.global.Call.TalkToMe.TtmException("failed to get call country configuration") and ("failed to get call forwarding disable code"). m35047b(C20616d0) answers “is this call on a TTM-configured SIM?”.
n5.d -> C18705dTtmDatabase - singleton f47389c, executor "TtmDatabase", in-memory list f47391b. Loads from SQLite on construction. Insert path: im.RunnableC17334k case 11 -> insert("ttm", null, msg.m35048a()) plus a binary-search insert into the in-memory list.
n5.f -> C18707fMessage(cis, from, date, audioUrl, messageId, isRead, isPlayed) - columns cli(f213e), cli_in_server(f266v1), date(f260t1), audio_url(f236l1), message_id(f239m1), is_read(f242n1), is_played(f227i1). Comparable sorts by cis then descending date. toString() = "Message(From cis = ..., at = ...)".
n5.g -> C18708gTtmMessagesPolling - singleton f47400e. Loads SP_KEY_PENDING_TTM_MESSAGES (JSON {messageId: {messageId, cis, cli, startWaitTime}}). m35050b() restarts the poll; m35051c(reqId, retries, delayMs) posts to the main-looper handler; m35052d(messageId, C20616d0) registers a new pending wait and restarts polling; m35049a() prunes entries older than the remote ttmMessageMaxWaitingTime.
n5.h -> C18709hTtmTokens - a ViewModel singleton backed by SP_KEY_TTM_TOKENS_AMOUNT_JSONS {paidAmount, isUpdateAfterLastPurchase, isUpdateAfterLastTtm}. On construction, if either flag is false, fires w6.d case 7 -> GET https://voice2text.eyecon-app.com/ttm-get-tokens-amount?mycli=<cli>. m35053a(json) reads tokensAmount. m35054b() - when paidAmount == 0 && isUpdateAfterLastTtm && isUpdateAfterLastPurchase -> n5.c.m35046a() (disable call forwarding).
13.2 The lifecycle (mixed in-cluster / out-of-cluster, marked)h5
StepWhereWhat happens
1. Balance checkn5.h ctor -> w6.d case 7GET .../ttm-get-tokens-amount?mycli= (JSON accept, Eyecon users only).
2. Zero balance => teardownn5.h.m35054b()paidAmount == 0 and both update flags true -> n5.c.m35046a() disables carrier call forwarding for every configured SIM (carrier USSD disable_code).
3. Session initw6.b case 0 (p620w6/RunnableC21274b.java:49)POST https://voice2text.eyecon-app.com/init-ttm with {mycli, purchases, freeTrialCode}; the response feeds n5.h.m35053a. Skipped when identical to SP_KEY_LAST_INIT_TTM_JSON.
4. Start a sessionp572u4/RunnableC20597o.java:178 [out of cluster]POST .../start-ttm with {mycli, acli (assistant CLI), calltag, purchases}; the caller is p315h7/C16937a case 6 via u5.C20609a case 0 (“Redirecting call to Talk to me…” -> CallStateService.m10185f() reject, then n5.g.m35052d(UUID.randomUUID().toString(), callDetails) and a getAllInappPurchases gather).
5. Poll for textw6.d case 1 (RunnableC21276d.java:90)GET .../getText?mycli= -> C15802b (p236d5, out of cluster) m31518o(res) / m31516i(), or m32066g(code, "CB_ERROR"); m31515h().
6. Acknowledgew6.b case 1 (RunnableC21274b.java:70)POST .../ack with the polled payload.
7. Verify call-forwarding setupw6.d case 3 (RunnableC21276d.java:145)GET .../ttm-call-forwarding-result?mycli= -> C3463d parses the boolean.
8. Teardownas/RunnableC3316o.java:150 [out of cluster]https://voice2text.eyecon-app.com/no-ttm with noTtm.
13.3 TTM audio and UIh5

zl.a (InterfaceC22275a, p716zl) is the progressive-download audio player interface used by f5.RunnableC16439w / C16440x for the PROGRESS_KEY_TTM zip. The AMR playback path is p633wl/RunnableC21439b (AudioTrack) with io.kvh.media.amr.AmrDecoder. TtmWindowActivity / TtmRecyclerView / TtmTutorialActivity are first-party under com.eyecon.global.Call.TalkToMe.p204UI; the TTM window UI itself is in package o5 (o5.a/o5.c adapters, o5.b/o5.e view-holders, o5.j Observer, o5.g/o5.i click handlers with strings Expend messages, paywall_noti_ttm, TTM window).

13.4 TTM constants and preferencesh5
  • TTM service number default "97243727022" (Israel, national 04-372-7022) - AbstractC17503r.m33607k("SP_KEY_TTM_PHONE_NUMBER", "97243727022"), and hard-coded at CallStateService.java:530,1696,1766; p315h7/C16937a case 14 opens the TTM page for "04-372-7022".
  • SP_KEY_PENDING_TTM_MESSAGES, SP_KEY_TTM_TOKENS_AMOUNT_JSONS, SP_KEY_LAST_INIT_TTM_JSON, SP_KEY_LAST_TIME_BACKUP, SP_KEY_DID_CLICK_CALL_TTM_BTN.
  • Remote: ttm_countries_configuration, ttmMessageMaxWaitingTime, ttm_free_trial_code.
  • PhoneAccountHandle role ids: CANTALK_TYPE_EYECON = 0, CANTALK_TYPE_WHATSAPP = 1, CANTALK_TYPE_SMS = 2 (p516r5/C19864j).

14. MyPhoneReceiver - full reachable surface (OBSERVED)h4

Manifest (AndroidManifest.xml:391-401):

<receiver android:name="com.eyecon.global.Call.MyPhoneReceiver"
android:enabled="true"
android:exported="true"> <!-- NO android:permission -->
<intent-filter android:priority="2147483647">
<action android:name="com.eyecon.global.action.INTENT_ACTION_START_CALL_WHATSAPP"/>
<action android:name="android.intent.action.PHONE_STATE"/>
<action android:name="android.intent.action.NEW_OUTGOING_CALL"/>
<action android:name="com.eyecon.global.action.CALL_SCREENING_NEW_CALL"/>
</intent-filter>
</receiver>

For comparison, MessageAssistantBroadcastReceiver does declare android:permission="android.permission.BROADCAST_SMS" (manifest lines 899-906) - a system signature permission, so only the platform can deliver to it.

MyPhoneReceiver.onReceive (MyPhoneReceiver.java:37-54):

  • Rejects intent == null, extras == null, or an empty action.
  • Accepts exactly the four manifest actions; anything else logs "onReceive canceled, action is not as set in the intent-filter".
  • The constructor unregisters any previously created static instance (f11651a) from MyApplication.f12163h and sets f11652b = true.
  • Hands everything to im.RunnableC17334k(action, ctx, extras, false, 7).

Reachable effects of a crafted broadcast (all in im/RunnableC17334k.java:868-1111):

  1. CallService.f11630f = 1 and startService(Intent(CallService, "EYECON_INTENT_ACTION_CALL") + putExtras(attackerBundle)) - arbitrary extras enter the service.
  2. CallStateService static fields are overwritten with attacker-chosen data: f46652n (EYECON_INTENT_KEY_IS_SCREEN_OFF), f46651m (TYPE_CALL_WHATSAPP), f46635t, f46643e, f46633r, f46644f, f46647i, f46645g, f46641c (the state machine itself).
  3. state maps to IDLE=0 / OFFHOOK=2 / RINGING=1 (any other value throws a RuntimeException that is only logged); incoming_number is normalised to cis and then used verbatim.
  4. On RINGING: C18302h caller-ID lookup is kicked off for the attacker’s number; CallStateService.m34675c / m34677e / m34678f fire; if remote IncomingCallPrefetchMode == "when ringing" the after-call pipeline is armed.
  5. On IDLE from RINGING/OFFHOOK: m34674b(1|2|3, ...) classifies the call as declined / missed / answered; the missed branch may schedule AbstractC19578g.m36035a(6, ...) (contact reminder) and a DBContacts write.
  6. NEW_OUTGOING_CALL: extras.getString("android.intent.extra.PHONE_NUMBER") becomes the outgoing CLI; AbstractC19327e.m35649f(...) and AbstractC16794a.m32647J("Call started") fire; the number is written into the in-call map.
  7. AbstractC19642b.m36086b(number) (“flush call”) -> C18505s.m34701a("flush call") -> a server round-trip for that number, and the resulting isSpam value can drive Call.disconnect() through CallStateService.onCallAdded (lines 1569-1572, which runs regardless of who triggered the broadcast).
  8. Every path ends with CallService.m10051d(intent) and the 1-second-debounced after-call kickoff AbstractC20656z.m37804b(false) for NEW_OUTGOING_CALL / RINGING / IDLE.

Assessment. Because MICO is the default dialer, some of this is already reachable through platform APIs, but the receiver as declared has no permission gate and honours app-private actions, so any app on the device can drive the state machine with attacker-chosen state and incoming_number. The concrete impact is state-machine confusion, attacker-chosen numbers entering MICO’s SQLite and analytics, forced after-call / assistant / TTM pipeline activation, and - when MICO happens to own an active call - forced Call.disconnect(). It is not an arbitrary telephony primitive, because the terminal action goes through android.telecom.Call and therefore only affects calls MICO owns.


15. Remaining cluster packages in briefh4

u7 (disk p575u7, 26 files) - SMS list UI + the plaintext sms_table DAOh5
  • u7.b -> RunnableC20680b = ExternalSmsDB.addMessage: query("sms_table", null, "messageId = ?", ...); if the row exists it returns isNewMessage=false + noReadMessages + dateMessage; otherwise it inserts {date, noReadMessages=1, body, sender, text, messageId} with insertWithOnConflict(..., CONFLICT_REPLACE) and logs "Insert new ExternalSms: body: <BODY> sender: <SENDER> messageId: <id>" (S6).
  • u7.s -> AbstractC20698s (30 KB) = the ExternalSmsDB manager (SELECT * FROM sms_table ORDER BY <date> DESC).
  • u7.q -> RunnableC20696q’s base comparator: sorts SMS by sender then by date, used both by the SMS list and by MICO’s merge logic (and referenced by C18302h).
  • u7.a,c,d,e,h,i,j,r,u,x = the SMS-list row model, adapter, view-holder and merged helpers.
  • u7.v -> ViewOnClickListenerC20701v (78 KB, heavily merged): the per-row actions - open the conversation (C20657a("Sms", contact)), Report spam (x7.c.m38859i0(cli, phone_number_in_server, private_name, "Eyecon SMS screen", isSuspiciousSpam ? 2 : isSpam ? 1 : 0, fm)), send SMS, delete the conversation, and open the web profile.
  • u7.y -> ViewOnClickListenerC20704y: SMS-window actions - Write (with the sms_permission_pre_prompt_msg pre-prompt when SEND_SMS is not granted), Reply, Copy (copied_to_clipboard), Open (eyecon://sms?source=external_sms_window).
  • u7.k,l,m,n,o,p,t,z = merged runnables for the SMS list / window.
k6 (disk p380k6, 29 files) - CallerIdWindowh5
  • k6.s -> C17781s (105 KB) = CallerIdWindow. Owns the call bubble (m34085G, resumeWebViewTimers, pauseWebViewTimers - fail-safe triggered), the _call_popup container, bubbleOptionsInCallPopup, and the AI-assistant button (EB_ai_assistant, see section 8 step 9). Implements InterfaceC20639p (bubble click) and InterfaceC20338h (person-data callbacks).
  • k6.a -> EnumC17759a call scenes: ACTIVE_ADDITIONAL_OUTGOING_CALL, ACTIVE_INCOMING_CALL_ANSWERED, ACTIVE_INCOMING_WAITING_CALL_ANSWERED, ACTIVE_OUTGOING_CALL, DISCONNECTED, RINGING_INCOMING_CALL, RINGING_INCOMING_WAITING_CALL. Drives the UI’s scene/transition matrix.
  • k6.b -> C17761b: the caller-ID result callback; reads CB_KEY_SPAM, received_name, received_photo, suspicious_spam, contact and fires eventX, updateEventCallInformationIfNeeded$onDone.
  • k6.i -> C17771i: screen-state receiver for EYECON.ACTION_CALL_ACTIVITY_CREATED (waitForCallActivityOnCreate#onReceive - activity shown).
  • k6.j -> C17772j: ad-visibility tracker (IB fullscreen ad shown/not shown for key: %s, IB popup ad shown/not shown for key: %s).
  • k6.t -> C17782t: EyeDynamicDrawable with colours #D2D5DD / #F38A2A / #F94960 and a MAYBE SPAM label.
  • k6.k -> Handler.Callback for EYECON, REFRESH_NOTIFICATION; k6.l -> the “connecting timeout” handler; k6.g -> a screen receiver.
  • k6.o -> ad listener (ac_native_ad_action_click, RESULT_KEY_HAD_IMPRESSION, Photo picker); k6.q -> MotionLayout.TransitionListener; k6.f / k6.r -> click handlers (EYECON.INTENT_ACTION_UNLOCK_KEYGUARD, EYECON.INTENT_ACTION_SHOW_NOTE, WAIT_FOR_NOTE_ACTION, busy button clicked).
  • k6.c (enum-ish merged runnable: calendar, facebook, menifa, notes, whatsapp), k6.c0 (interface), k6.e/k6.h (bubble visibility + webview timers), k6.n/k6.p (async holders), k6.a0/k6.b0/k6.v/k6.w/k6.x/k6.y/k6.z/k6.d0/k6.m/k6.u - animation and layout helpers.
u5 (disk p573u5, 42 files) - CallActivity supporth5
  • u5.d0 -> C20616d0 = the Call.Callback: fields EYECON.EXTRA_KEY_CALL_CIS, EYECON.EYECON_CALL_ACTION_STATE_UPDATE, EyeconCallDetails, plus f54436a (cli), f54437b (cis), f54438c (t5.e person record), f54439d (Call), f54440e (isIncoming), f54441f (call type), f54444i (ignored), f54445j (blocked), f54447l (answered), f54448m, f54454p. Methods m37732a() (state), m37733b() (start time), m37734c(), m37735d() (detach).
  • u5.a -> C20609a: the TTM redirect button. Case 0 sets SP_KEY_DID_CLICK_CALL_TTM_BTN=true, marks the call ignored, shows "Redirecting call to Talk to me...", rejects the call via CallStateService.m10185f(), generates UUID.randomUUID(), and registers a TTM pending message with n5.g.m35052d. Case 1/default fires CallActivity.m10167N0(new MessageAssistantSource.IncomingCallFullscreen()).
  • u5.f -> merged BroadcastReceiver for EYECON.ACTION_CALL_AUDIO_CHANGED, EYECON.ACTION_CALL_CAN_ADD_CALL_CHANGED, EYECON.ACTION_CALL_FINISH_ACTIVITY_NOW.
  • u5.z -> AbstractC20656z: the default-dialer / call-screening role nag - strings default_dialer, call_screening, must_permissions, general, default_dialer_enable, last_permission_notification_time, *_notification_interval_hours, and the deep link eyecon://open_must_permissions.
  • u5.h/u5.j -> additional Call.Callbacks (swap-call / call state); u5.g0 -> InCallService.VideoCall.Callback; u5.i -> keypad handler (keypad_close, keypad_hangup, keypad_speaker).
  • u5.d/u5.q0/u5.n0/u5.r0/u5.j0 -> in-call feed holders (reloadAdPage, startShowAdBeforeAnswerTimer, animateRevealPhoto, incall_full_screen_enable_ads, UiOneParticipant).
  • u5.r -> ViewOnTouchListenerC20643r, the call-bubble WindowManager view (addBubbleWindow, CallBubble, privateFlags).
  • u5.l0 -> a merged runnable with Action / public_profile / Scheduler_whatsapp_inapp / UserAgentAvailable / UserAgentProvider.
  • u5.b -> the reject/answer handler (EYECON.INTENT_ACTION_UNLOCK_KEYGUARD, onOutgoingCall (STATE_CONNECTING), onClickBusy missing call., REJECT).
  • Remaining small classes: u5.b0, u5.e0, u5.h0, u5.j0, u5.a0 (Drawable), u5.c0/u5.f0/u5.p (interfaces), u5.l/u5.m/u5.n/u5.o (animators), u5.p0 (ItemDecoration), u5.q (gesture), u5.t0/u5.v0/u5.y (dialogs), u5.u (Notification.Action.Extender), u5.w (ViewHolder), u5.x (RecyclerView.Adapter), u5.y (dialog), u5.m0 (TextureView.SurfaceTextureListener), u5.s, u5.k, u5.k0, u5.t, u5.e, u5.g (runnables).
w5 (disk p619w5, 16 files) - In-call feedh5
  • w5.c -> C21258c (120 KB): the in-call action-button holder. Strings include AI Assistant, call_reminder, choose_social, facebookAndInstagram, google_search, eyecon://contact?phone_number=, Fusion, dialer, DEFAULT, incall_feed, &action=openInstagramAndFacebookDialog, eyecon.INTENT_KEY_SHOW_ABOVE_LOCK_SCREEN. It holds 10 merged x5.v.m38851a call sites (the in-call-feed entry points to the assistant).
  • w5.e -> C21260e: ad holder (incall_feed_top, incall_feed_bottom, incall_feed_middle, incall_feed_small, incall_feed_big, *_admob_ad_id, *_gam_ad_id, Has_Eyecon_ad, addAdToAdContainer).
  • w5.b -> AbstractC16130b (EYECON.INTENT_ACTION_UNLOCK_KEYGUARD, restartEyecon, saveButtonClicked).
  • w5.j = history, w5.l = reminder, w5.k = note, w5.i = extra actions, w5.q = statistics (alpha, percentWidth, onNoStatistic), w5.g = holder base (onVisibilityChange, isFullyVisible, type, release), w5.n/w5.o = animator/listener, w5.d/w5.m/w5.p = merged runnables, w5.h = click listener, w5.f = scroll listener.
v7 (disk p599v7, 20 files) - merged runnable bucketh5

v7.f -> RunnableC20908f (33.8 KB) carries three C3-relevant operations:

  • case 4 reportBlock: POST https://api.eyecon-app.com/app/spamreport, tag reportBlock, body {"type":"blocked","blocked_numbers":[...]} (requires AbstractC9039h.m10665v()).
  • case 5 PhotoPicker name search: GET .../app/getnames.jsp with cli, lang, is_callerid=false, is_ic=false, version, requestApi=URLconnection, source=PhotoPicker, is_search=true.
  • case 11 the single AudioRecord(MIC) voice-note recorder start (see 11.1), and case 10 the AudioTrack playback completion. Also case 23 (dynamic_area_tickets update), case 8 (table_share_media delete), cases 26-28 (theme background rendering), case 22 (Singular consent queue). Remaining v7 files: v7.a, v7.a0, v7.b, v7.k, v7.m, v7.n, v7.o, v7.p, v7.q, v7.t, v7.u, v7.x, v7.y and runnables v7.c, v7.e.
k5 (disk p379k5, 10 files) - Play Store + merged CanTalk/geocode/Blowfishh5
  • k5.f -> C17754f: store-item downloader (application/getStoreItem, country, language, product, STORE_FOLDER).
  • k5.i -> C17757i: the WebView JS bridge (loadProducts, getPurchasedProducts, getSelectedItems, purchase product_id = %s, itemSelected, ringtone_selected_item, getDownloadItemsState, StoreJavaScriptInterface).
  • k5.g = download-state model (download_progress, download_state, item_id, item_type); k5.j = product model (product_id, product_type, purchase_token, store_items).
  • k5.d = billing flow (failed to launch billing flow, product_id =, STORE_2_); k5.a = product-id cache (SP_KEY_STORE_PRODUCTS_IDS_JSON).
  • k5.b -> RunnableC17750b (29.8 KB) is a ProGuard merge that additionally contains CanTalkActivity wiring, https://eu1.locationiq.com/v1/search.php / https://us1.locationiq.com/v1/search.php geocoding, Blowfish + cipher_key, and fb_mobile_obtain_push_token. The Blowfish/cipher_key pair is worth cross-referencing with cluster C1’s crypto inventory.
  • k5.c, k5.h = merged runnables; k5.e = interface.
o5 (disk p449o5, 10 files) - TTM window UIh5

o5.a and o5.c = RecyclerView.Adapters; o5.b and o5.e = view-holders; o5.j = Observer; o5.d/o5.f/o5.h = runnables (ALIGN_LEFT, translationY); o5.g = click handler (Expend messages); o5.i = click handler (paywall_noti_ttm, TTM window). All UI only - no network or DB.

w0 (disk p614w0, 1 file, 43 KB) - merged Telecom helperh5

w0.a -> AbstractC21232a implements InterfaceC16772b. C3-relevant methods: m38343q(Call, "REJECT"|"DISCONNECT") -> Call.reject / Call.disconnect; m38322G(Call) -> CLI; m38321F(Call) -> CIS; m38318C(Call) -> is-private-number; m38324I("Call started", null); m38317B(Call, boolean) -> state predicates; m38319D(Call) -> call init. Also carries Crackle mediation-adapter branches (&av=1.2.0.0, %02d:%02d duration formatting).

d (disk p230d, 1 file) - merged utility bagh5

d.a -> AbstractC15790a. C3-relevant: m31044c(int spamType, String cli) and m31045d(int, C20616d0) (spam-type -> UI tag; see 9.2), m31046e(C19862i). Everything else is unrelated library helpers (kotlinx-serialization, barcode formats, JSON tokens, GA4 event names, R.attr.spam_red).

v5, nq, p3, p7, bl, zk, xo, ep, uo, vo, po, pp, wp, ic, kc, lg, m8, eh, nj, vhh5

All [LIB] except where noted in section 0.4. Two with small app-side value:

  • v5.b (C20873b) - the in-call feed ordering adapter. Its slot order is built from [0,3,1,2,5,4,8,6,7] and reshuffled by the remote flags incall_feed_big_small_ads_position (small_top_big_bottom vs the reverse) and incall_feed_middle_ad_enabled; in premium it drops slots 8, 7 and 3 entirely.
  • lg.n (AbstractC18372n) - merged notification-channel helper used by CallStateService.m10209J (AbstractC18372n.m34530r(this, channelId, title, importance, ...)).

16. Tables, endpoints and pref-keys touched by C3h4

SQLite tablesh5
TableWhereColumns touched
spam_listx7.q, w9.a, w9.bcontact_id, cli_in_server, cli, name, spam_type, create_timestamp
sms_tableu7.b, u7.sf144D1 (date), f147E1 (unread), f150F1 (body), f153G1 (sender), f156H1 (text), messageId
ttmn5.d (insert via im/RunnableC17334k case 11)cli, cli_in_server, date, audio_url, message_id, is_read, is_played
recorded_notese5.dcreated at p008a7/C0041b.java:318; columns added by migrations f230j1 and f149F0
table_api_requestsw6.c case 12, w9.b case 6the retry queue
table_share_mediav7.f case 8share tracking
dynamic_area_ticketsv7.f case 23f148F, f268w0
names_photos_cachet5.g (PersonDataCache)cli_in_server, modify_time, spam_type, data-type discriminator
contact block listDBContacts (first-party)blocked C16470g0 rows
HTTP endpoints touched from C3h5
EndpointMethodCalled fromPurpose
https://api.eyecon-app.com/app/pic?cli=&is_callerid=true&size=&type=0&src=&cancelfresh=0&cv=vc_749_vn_4.2026.04.23.1145_aGETw6.h.m38434gcaller-ID photo (and name, via the pic record)
.../app/getnames.jspGETw6.h.m38433f, v7.f case 5caller-ID name lookup / PhotoPicker search
.../app/spamreportPOSTbm.RunnableC3663m1, v7.f case 4{cli,name,is_spam} / {type:"blocked",blocked_numbers:[]}
.../app/resuggest_namePOSTbm.RunnableC3589a.m3809a via x4.b.m38813f{cli,previous_name?,suggested_name?,tag?,langs}
.../app/addcontacts.jspPOSTw6.h.m38428afull address book
.../app/getcontactslang.jspGETw6.h.m38428acontact-language detection
.../app/addfbtoken.jspGETw6.c case 1Facebook token
.../app/updateuserdetails.jspGETw6.c cases 2-4adv_id, mc, app version
.../app/upgrade_pic2eyecon.jspGETw6.c case 6photo upgrade nudge
.../app/uploadpic.jspPOST multipartw6.h.m38441nprofile photo
.../app/messages/receivedPOSTw6.f case 1external SMS ids (not bodies)
.../app/messages/getGETw6.d case 5external SMS sync
.../app/virality/premium_status, .../app/virality/viral_id, .../app/virality/viral_id_checkGETw6.d cases 2/4/9virality
.../app/get_store_productsGETw6.d case 6store
.../app/mysettingsGET/POSTw6.d case 8, p572u4 case 7settings
.../app/cantalk.jsp, .../app/getcantalkstatus.jspGETp474p5 [out of cluster]CanTalk
https://voice2text.eyecon-app.com/init-ttmPOST JSONw6.b case 0TTM session init
https://voice2text.eyecon-app.com/ackPOST JSONw6.b case 1TTM ack
https://voice2text.eyecon-app.com/getTextGETw6.d case 1TTM poll
https://voice2text.eyecon-app.com/ttm-call-forwarding-resultGETw6.d case 3TTM CF check
https://voice2text.eyecon-app.com/ttm-get-tokens-amountGETw6.d case 7TTM balance
https://voice2text.eyecon-app.com/start-ttmPOST JSONp572u4 [out of cluster]TTM start
https://voice2text.eyecon-app.com/no-ttmGETas [out of cluster]TTM teardown
http://numberlookup.instabridge.com/respond-smsPOST JSON (Retrofit)x5.a / x5.n / x5.othe AI assistant
https://eu1|us1.locationiq.com/v1/search.phpGETk5.b [merged, out of C3 flows]geocoding
remote server_report_urlPOST gzipw6.f case 0call report
6 x Google-Drive pending upload URLsPOSTp282f5 [out of cluster]user backup
SharedPreferences / SP keys touched from C3h5

clientId, authenticatedCli, android:hint_username, SP_KEY_USER_EMAIL; SP_KEY_PHONE_NUMBERS_CACHE_V4, SP_KEY_LAST_INIT_TTM_JSON, SP_KEY_TTM_TOKENS_AMOUNT_JSONS, SP_KEY_PENDING_TTM_MESSAGES, SP_KEY_TTM_PHONE_NUMBER, SP_KEY_DID_CLICK_CALL_TTM_BTN; SP_KEY_BLOCK_SPAM_CALLS, SP_KEY_VERSION_SPAM_LIST; SP_AFTERCALL_TYPE, SP_AFTER_CALL_LOGS, SP_KEY_LAST_TIME_BACKUP; SP_KEY_IS_AI_ASSISTANT_* (5 enable flags, 5 message-template keys, SP_KEY_IS_AI_ASSISTANT_NEED_SHOW_TUTORIAL); SharedPreferences file MessageAssistantStorage key STARTED_FLOW_CONTACTS; SharedPreferences file after_call_feedback_prefs keys after_call_<question>.

Remote-config keys (AbstractC15804d, the d5 bag) touched from C3h5

use_ai_assistant, messageAssistantIdleDurationMin, ApiRequestServerRetryCount, apiWorkRepeatIntervalMinutes, IncomingCallPrefetchMode, compact_after_call, ac_under_lock_screen_no_time, incall_feed_big_small_ads_position, incall_feed_middle_ad_enabled, incall_feed_middle_ad_enabled, debug_wait_call_activity_timeout, ttm_countries_configuration, ttmMessageMaxWaitingTime, ttm_free_trial_code, reportSpam, server_report_url, call_forwarding_config.


17. Security-relevant findings, consolidatedh4

Critical / Highh5
  1. S1 - cleartext transmission of SMS content, contact name, phone number and the user’s display name to a third-party assistant host. http://numberlookup.instabridge.com/respond-sms (no TLS), up to 50 messages from both the inbox and the sent folder, for every number that has an active assistant conversation, with no per-message consent. The app permits cleartext globally (usesCleartextTraffic="true", @xml/network_security_config). Anyone on the network path can read and modify the assistant’s instructions and replies. Additionally, whatever the assistant replies is then transmitted by SMS from the user’s own phone (x5.p.m38847d), so a network attacker who can influence the newMessage payload can cause MICO to send attacker-chosen SMS to arbitrary destinations chosen by the assistant.
  2. S2 - exported, unpermissioned, max-priority MyPhoneReceiver. Any app on the device can inject state/incoming_number and drive the call state machine, DB writes, analytics, and (while MICO owns a call) force Call.disconnect(). Full reachable surface enumerated in section 14.
Mediumh5
  1. S3 - unattended outbound SMS. The assistant sends multipart SMS with no user confirmation and no sentIntent/deliveryIntent, so failures are silent and the user cannot easily audit what was sent in their name.
  2. S4 - hard-coded shared API secret PgdtSBeR0MumR7fO shipped in the APK and attached to every *.eyecon-app.com request as e-auth-k; e-auth-c is a trivially reversible function of the CID.
  3. S5 - device/identifier telemetry on essentially every server call: IMEI (Mbridge keyspace), advertising id, device_id, installer name, SIM count, timezone, locale, manufacturer, model, WhatsApp-installed flag and SMS-send-availability flag.
  4. S6 - SMS body and sender written in cleartext to the app-private <filesDir>/eye_log.txt (5 MB cap). Not logcat (the prior C1 claim was wrong), but a root/backup/forensic exposure.
  5. S7 - call-recording audio in shared external storage (Documents/Eyecon/RecordedCalls|RecordedNotes), with the pre-Android-Q path written through the legacy _data column and requestLegacyExternalStorage="true" set. MICO does not create the files, but it indexes them, displays them, and uploads them (plus the plaintext contactdb) to Drive. Recording-consent enforcement therefore rests entirely outside the app.
Lowh5
  1. S8 - debug “Ad stats” dialogs reachable by three long-presses in both after-call activities.
  2. x7.q.m38870b builds SQL by string concatenation (... WHERE cli_in_server = '<cis>'); the input is libphonenumber-normalised so it is not attacker-controlled in practice, but it is still a fragile pattern.
  3. Spam-tag semantics are enforced by a UI allow-list that is also the outbound payload: if x7.v/x4.a0 ever gains a tag value not in the list, the suppression in x4.b.m38813f (which only filters "not_set" and "spam") is the only backstop.
Explicitly NOT found (negative results, stated)h5
  • No call-audio capture of any kind (MediaRecorder, VOICE_CALL, VOICE_DOWNLINK, VOICE_UPLINK, MediaProjection, createAudioCapture: all absent).
  • No SMS body is uploaded by MICO’s own code (messages/received carries ids only). The only SMS-content egress is the assistant’s respond-sms call.
  • No TelecomManager.endCall() anywhere.
  • No cantalksaw.jsp anywhere.
  • No crylpted/EncryptedSharedPreferences usage in the C3 paths - all C3 storage is plaintext.

18. Traceability indexh4

Briefing requirementSection
Package list, file counts, responsibilities0.4
Per-package class tables1-7, 9.8, 15
Flow 1 - incoming-call interception -> lookup -> display -> spam decision8
Flow 2 - spam classification and blocking9
Flow 3 - AI SMS assistant end to end2, 3, 4, 5, 10
Flow 4 - call recording (capture -> storage -> upload)0.2, 11
Flow 5 - after-call feedback / follow-up12
Flow 6 - TTM lifecycle13
MyPhoneReceiver reachable surface14
Tables / endpoints / pref-keys16
Security findings + corrections0.1, 0.3, 17

Cluster C4 - Main screen, Contacts, Contact remindersh3

APK: com.eyecon.global (MICO) - v4.2026.04.23.1145 Cluster scope (TSV docs/briefings/C4-main-contacts.txt): 27 packages, 489 files, 2.6 MB Owning modules: MainScreen, Contacts, ContactReminder Source root: C:\Users\btc_e\Downloads\mico-analysis\src\sources

How to read this doc. Real class names are taken from the JADX header comment /* JADX INFO: renamed from: <pkg>.<Cls> */; the on-disk directory is the obfuscated form (pNNN). Every statement is tagged [OBS] (directly visible in decompiled source) or [INF] (reasoned / not fully provable).


0. Scope reconciliation and corrections to the briefingh4

0.1 Corrections / clarifications to the briefing’s “already-established facts”h5

[OBS] C1 — the addcontacts.jsp uploader is NOT in cluster C4. The briefing attributes the payload builder to w6.h.m38428a. The real package w6 lives on disk at ...\src\sources\p620w6\ (C21273a.java, C21277e.java, C21280h.java, InterfaceC21279g.java, RunnableC21274b.java, RunnableC21275c.java, RunnableC21276d.java, RunnableC21278f.java). Select-String 'renamed from: w6\.h' over the whole tree returns exactly one hit: p620w6\C21280h.java. Therefore C4 cannot prove the @z@ record layout of addcontacts.jsp, the cli_cc / init / possible_langs fields, or the “re-request the contact language when contactLangNeedUpdate” heuristic — those live in another cluster. What C4 does contain is the contact-removal uploader (§2.4) which reuses the same @z@ record convention.

[OBS] C2 — the Blowfish/ECB photo cipher is NOT in cluster C4. m7.d (disk p420m7\C18543s.java, referenced as AbstractC18543s.m34752m(...) from p516r5\C19862i.java:186) and am\RunnableC0215t1 are listed in the briefing, and am\RunnableC0215t1 is in this cluster — but it is a Firebase Messaging thread-pool worker, not a crypto class (see §20). The actual Blowfish keying lives in p420m7, outside C4.

[OBS] C3 — a7.a (disk p008a7\AbstractC0039a.java) is the column-constant table, as claimed, and it is 100 % readable here. Full transcription in §0.2. It is not contact-specific: the same C16458a0 constants are reused for calls, SMS/MMS, favourites, surveys, notes, spam, remote-config rows, etc. There are exactly 132 C16458a0 constants (f133A..f278z1) plus 4 String constants. The “48 columns” figure for the contacts table is therefore a per-table subset and cannot be derived from this file alone.

[OBS] C4 — contactdb schema / migrations / DBContacts are NOT in cluster C4. com.eyecon.global.Contacts.Database.DBContacts is a readable thin wrapper, but the real contactdb SQLiteOpenHelper, the v1->v57 migrations, and DBContacts’ 60+ methods are in p022ad-style packages outside the TSV. C4 only contains callers of DBContacts.

[OBS] C5 — “the decrypt path calls ObjectInputStream.readObject()”. Partially verifiable: am\RunnableC0215t1 (in-cluster) does import ObjectInputStream — see §20.2. It is a Firebase ObjectInputStream over a CipherInputStream, not the contact-photo path. The photo path in-cluster (r5.e, r5.t) uses javax.crypto.CipherInputStream via p315h7/p420m7 helpers. Treated as UNVERIFIED in C4 — needs the p420m7 cluster.

[OBS] C6 — “removecontacts.jsp” record format (new, verified here). p541s5\RunnableC20065s.java (s5.s) default branch:

for (C16470g0 row : list) { // f7.g0 = generic DB row
String cis = row.get(AbstractC0039a.f216f); // f216f == "cli_in_server"
String name = row.get(AbstractC0039a.f219g); // f219g == "name"
if (!isEmpty(cis) && seen.add(cis)) {
sb.append(cis).append("@a@").append(name).append("@z@");
}
}
if (!sb.toString().trim().isEmpty()) {
String url = AbstractC0039a.f168L1 + "removecontacts.jsp"; // https://api.eyecon-app.com/app/
ArrayList p = new ArrayList(2);
p.add(new C21872a("contacts", sb.toString()));
p.add(new C21872a("apsVersion", "vc_749_vn_4.2026.04.23.1145_a"));
C21649b.f57893a.m38854c(new C21653f(p, url, "POST", "removecontacts.jsp"));
}

So: contacts = repeated <cli_in_server>@a@<name>@z@; apsVersion is the same vc_749_vn_4.2026.04.23.1145_a literal used by addcontacts.jsp. @z@ terminates a record and @a@ separates fields — the same convention the briefing reported for the upload path, now independently confirmed on a second endpoint. The request is gated by AbstractC9039h.m10665v() (a network-available check) [OBS].

0.2 Full column-constant table (a7.a, disk p008a7\AbstractC0039a.java) [OBS]h5

C16458a0 constants, in static-initialiser order (field -> literal):

fieldvaluefieldvaluefieldvalue
f207cab_raw_idf210dcontact_idf213ecli
f216fcli_in_serverf219gnamef222his_pic_synced
f225iupdate_datef228jcall_countf231kcli_type
f234lis_primaryf237mis_singlef240nsent_to_server
f243ois_survey_picf246psurvey_pic_urlf249qsurvey_pic_tag
f252rhas_photof255sis_subf258tis_viewer
f261uos_typef264vsms_countf267wwhatsapp_count
f270xcan_talk_countf273yis_can_talk_enablef276zvn
f133Aessagef136Bsourcef139Cis_group
f142Dname_groupf145Ephotof148Fid
f151Gfav_pin_posf154Hlast_action_datef157Iaction_type
f160Jis_favoritef163Kpinned_positionf166Lis_like
f169Mwas_invitedf172Npic_process_donef175Olast_approved_tag
f178Pfacebook_idf181Qfavorite_timestampf183Rcan_talk_status
f185Scan_talk_timestampf187Tpic_status_in_serverf189Ucontact_photo_uri
f191Vscoref193Wlast_action_date_long(f195X alias)duration
f197Ycontact_action_date_milif199Zfresh_pic_init_urlf202a0fresh_pic_init_tag
f205b0has_fresh_pic_initf208c0has_thumbnail_photof211d0custom_phone_number_label
f214e0pre_eyecon_usagef217f0eyecon_favorites_pinned_posf220g0default_cis
f223h0storage_photo_pathf226i0ab_photo_typef229j0extra_values_json
f232k0data_typef235l0data_partf238m0type
f241n0subjectf244o0priorityf247p0max_appearances
f250q0appearancesf253r0schedule_expressionf256s0last_seen
f259t0subject_descriptionf262u0subject_pathf265v0screens
f268w0had_user_interactionf271x0local_ticketf274y0order_value
f277z0max_per_sessionf134A0absolute_positionf137B0first_position
f140C0stepsf143D0one_per_x_daysf146E0question_type
f149F0call_directionf152G0main_cisf155H0main_cli
f158I0secondary_cis(f161J0 = duration alias)f164K0social_typef167L0
f182Q0big_pic_urlf184R0small_pic_urlf186S0pic_seen
f188T0received_datef190U0fresh_pic_tagf192V0ROWID
f194W0is_hash_updatedf196X0default_sim_idf198Y0is_enable
f200Z0create_timestampf203a1rowidf206b1DB_COLUMN_UNICODE_SCORE
f209c1account_typef212d1account_idf215e1participants
f218f1text_notef221g1record_durationf224h1file_name
f227i1is_playedf230j1is_savedf233k1heart_ts
f236l1textf239m1message_idf242n1is_read
f245o1is_spamf248p1is_suspiciousSpamf251q1has_name
f254r1spam_typef257s1urlf260t1time_ms
f263u1providerf266v1cisf269w1is_play
f272x1is_seenf275y1total_time_played_msf278z1type
f135A1server_idf138B1aliasf141C1key
f144D1date_msf147E1no_readf150F1body
f153G1senderf156H1sender_namef159I1json_key
f162J1server_retry_countf165K1db_column_uuid

String constants: f168L1 = "https://api.eyecon-app.com/app/", f171M1 = -1, f174N1 = "START_ACTIVITY_ABOVE_SCREEN_LOCKED", f177O1 = "SP_TELL_SERVER_CONTACT_HAS_NO_PHOTO", f180P1 = "sp_isCalenderAvailable". File path constant f201a = File.separator + ".Eyecon" + File.separator + ".nomedia", f204b = f201a + "/temp_photo.jpg". Also static: AbstractC0077t.f359d (a Toast).


1. Package inventory (27 / 27)h4

#Disk dirReal pkgFilesKBModuleOne-line responsibility§
1p516r5r536696MainScreenCore contact model + AddressBook (ContactsProvider) engine + SocialManager. r5.e is the single 386 KB class that reads ContactsContract.CommonDataKinds.Phone.§2
2p444o0o0111391MainScreenThird-party: Amplitude SessionReplay internals (not MICO logic).§20
3amam76187ContactsThird-party: Firebase Messaging / GMS worker pools.§20
4p493q6q655171MainScreenGeneric RecyclerView widget kit (adapters, view-holders, decorations, span-size lookups) used by the contact lists/grids.§3
5p360j7j720144MainScreenFragment base classes + j7.t (66 KB) ContactListInfo-area mega-class.§4
6p541s5s519139ContactsDBContacts task-runner pool — every DB mutation lambda + removecontacts.jsp uploader (s5.s).§5
7p475p6p616103MainScreenGeneric app glue: fragment lifecycle hooks, Handler.Callback, page-change callbacks, content://sms readers, updateuserdetails.jsp, cantalk.jsp.§6
8p236d5d55101MainScreenRemote-config / A-B flag catalogue (d5.d is a 300-entry Map<String,String>), incl. a cleartext HTTP config URL.§7
9p364jcjc1497MainScreenFragment/dialog base + inset/layout utilities used by the contact screens.§8
10p574u6u61772MainScreenTop-calls / recents adapter set (u6.a model, u6.u adapter, u6.d 20 KB refresh runnable).§9
11p032b5b5867MainScreenPhoto storage / cache-path helper (b5.e, b5.d, b5.f) + handler loop.§10
12p246dhdh1764ContactReminderGoogle Credential Manager / Play-services-auth internals (dh.i = CredentialManagerCallback). Not reminder logic despite the TSV owner.§11
13p046c6c61663MainScreenViewPager2 / animation / touch helpers (c6.a = BroadcastReceiver).§12
14p315h7h71453MainScreenExecutor factory (h7.i scheduler, h7.h ThreadFactory naming threads FLTask-).§13
15p001aa151ContactReminderSingle 51 KB class a.a — a Kotlin/coroutines or R-independent utility; inspected below.§14
16p555t6t61748MainScreenAd/banner + grid decoration helpers (t6.i extends AdListener).§15
17p492q5q5946ContactReminderAdd-contact / caller-identity sheet (q5.k extends c7.j, q5.g static entry point).§16
18p474p5p5533MainScreencantalk.jsp / getcantalkstatus.jsp client.§17
19p304g6g6529MainScreenViewPager1 PagerAdapter + page-change plumbing (legacy main-screen tabs).§18
20p528rkrk326MainScreenrk.b enum + rk.a static holder — small tab/layout descriptor.§19
21p702z6z6526Contactsz6.a = per-number NOTE record (Comparable), z6.e dialog.§19
22p556t7t7222MainScreenGeneric callback adapter (2 classes).§19
23p238d7d7417MainScreenBase Fragment for the contact screens (d7.b = Fragment implements i71.n0) + back-press callback + custom RuntimeException.§19
24p517r6r627MainScreenItemTouchHelper.Callback (swipe-to-delete / drag) + its interface.§19
25p009a8a875MainScreenTiny ViewModels (a8.b/e/g extend ViewModel) + helpers.§19
26p392kqkq25ContactsInterface + abstract base (2 classes, 5 KB).§19
27p542s6s634MainScreenSmall view-holder/handler trio.§19

2. Package r5 (disk p516r5, 36 files, 696 KB) — contact model, AddressBook engine, SocialManagerh4

2.0 Class-by-classh5
disk filerealkindpurpose / key members
C19846a.javar5.amodelAccountWithData — (name, accountType, accountName, isPhoneAccount); resolves AuthenticatorDescription from AccountManager.get(MyApplication).getAuthenticatorTypes(); m36571a(Context):Drawable, m36572b(Context):String (label, with com.google->“Google”, …outlook.USER_ACCOUNT->“Outlook”, “Samsung account”->“Samsung”), m36573c, compareTo
C19847a0.javar5.a0Observersynthetic Rx-style onChanged(Object) bridge
C19848b.javar5.bFragmentEyeSearch account-picker fragment. static final AuthenticatorDescription[] f51112J = AccountManager.get(MyApplication.f12163h).getAuthenticatorTypes(); mo4120k0, onCreateView, onViewCreated; fields ViewGroup f51113H, LinearLayout f51114I
RunnableC19850c.javar5.cRunnable7 branches (a..g): delete-contact / delete-call flows; calls DBContacts.f11707J.m10135X(...), m10134W(), m10123E()
RunnableC19851c0.javar5.c0Runnable4 branches (a..d); case 0 account-selection post-processing
RunnableC19852d.javar5.dRunnable4 branches (a..d); contacts-refresh kick-off
ViewOnFocusChangeListenerC19853d0.javar5.d0OnFocusChangeListenerNewContactActivity — toggles hint R.string.enter_name / R.string.enter_valid_number
C19854e.javar5.estatic utility”AddressBook” — the ContactsProvider engine. 4 399 lines. See §2.1
C19855e0.javar5.e0abstract callbackextends e2.b (async callback) — FB-profile-link probing progress holder; m31518o("NOT_A_FB_PROFILE_ERROR"), m31515h(), m31516i(), m36607q(AbstractActivityC3498e, String)
C19856f.javar5.fmodelCallLogInfo — m36608b():String[] = call-log projection {"_id","date","number","normalized_number","type","name","duration","subscription_id"}; m36609d(int) type->label; m36610a() returns cis (strips leading + from normalized_number, else l7.b.m34444e(number)); m36611c(String,boolean) attach name/starred. Fields number,name,date,type,duration,subscription_id,rowId,isStarred,isContact
C19858g.javar5.gContentObserverContacts-change watcher. static boolean f51163b; onChange(boolean) posts RunnableC18504r to C16942f.m32765b() with C18505s.f46714c (the sync singleton) — i.e. a ContactsProvider mutation triggers an app-side resync
C19859g0.javar5.g0modelSocialInfo — {EnumC19861h0 socialEnum; String socialID; int score; String customSocialType}. Reflective JSONObject (de)serialisation over getDeclaredFields(); enum stored as ordinal int. static socialsIdIsCli = {WHATSAPP, SMS, VIBER, WHATSAPP_VIDEO, WHATSAPP_CALL}. equals compares socialEnum.f51206b (the numeric id) + socialID
C19860h.javar5.hmodelParticipant — {String cis; String cli; String name} + static m36613a(String) splitter. Used for group SMS/MMS (participants column)
EnumC19861h0.javar5.h0enumSocialType — 47 constants (ordinals 0..46). See §2.2
C19862i.javar5.imodelContactInfo (TAG "ContactInfo") — the central contact entity. See §2.3
C19863i0.javar5.i0static utilitySocialManager — resolves/launches every social destination. See §2.5
C19864j.javar5.jmodelPhoneNumberInfo — per-contact number record. See §2.4
C19865j0.javar5.j0LifecycleEventObserverlifecycle hook for NewContactActivity
C19867k0.javar5.k0abstract callbackSocial-media share callback (extends e2.b); launches share sheet for a cis
C19868l.javar5.lRecyclerView.ItemDecorationgrid spacing: h1(67) outer, h1(10) inner — ab list decoration
C19869m.javar5.mpredicate filterimplements am.b1; int[] {2} + mo31520a(ContactInfo) — multi-select predicate
C19870n.javar5.nstate holderselection-mode controller: Activity, HashSet f51232c of selected contacts, AtomicInteger f51229i, String f51234e/f51235f, predicate f51237h
C19871o.javar5.oRecyclerView.ViewHolderab-list row holder: EyeAvatar f51238c, TextView f51239d, C20343m f51240e, ContactInfo f51241f; implements t6.h (mo716Q, mo717U, mo718V, mo721c0, mo722s, mo723w)
C19872p.javar5.pRecyclerView.Adapterab-grid adapter; getItemCount, onBindViewHolder, onCreateViewHolder; field o0.l1 f51244e (Amplitude tracking)
RunnableC19873q.javar5.qRunnablecontact-options menu actions: case 0 delete call, case 4 copy number (R.string.clipboard), case 5 schedule call/message, case 6 note
RunnableC19874r.javar5.rRunnable21-branch global dispatcher. See §2.6
RunnableC19875s.javar5.sRunnableblock / unblock confirmation dialog (R.string.block_number, R.string.block_are_you_sure, R.string.block, R.string.cancel)
RunnableC19877u.javar5.uRunnablereport-spam branch
AbstractC19876t.javar5.tabstract utilitycontact action menu builder. m36681a(ContactInfo, l5.e, q6.j, EyeSearchActivity, String) builds the bottom-sheet (Call details, Add/Edit contact, Copy number, Schedule call or message, Note, Block/Unblock, Report or block, Delete call); m36682b(Activity, contactId) -> ACTION_EDIT on ContactsContract.Contacts.CONTENT_URI/<id>; m36683c(Activity, contactId) -> ACTION_VIEW; m36684d(...) “Link contacts” confirm; m36685e(ContactInfo, Activity) history quick-action sheet
C19879w.javar5.wmodelFreshPic — {String a,b,c,d; int e,f; long g; String h,i; Bitmap j; ContactInfo k; boolean l}
C19880x.javar5.xnode/tree nodecontact tree node: ContactInfo f51276a, String f51277b, C19880x f51278c (parent), three ArrayList children; static C0079u f51272g, static HashSet f51273h, static C19290a f51274i, static HashMap f51275j
InterfaceC19878v.javar5.vinterfacecallback
RunnableC19882z.javar5.zRunnableNewContactActivity helpers: invite-link validation (R.string.enter_valid_number, R.string.invite_link_and_text), copy to clipboard
ViewOnClickListenerC19881y.javar5.yOnClickListenerNewContactActivity account picker click; compares against R.string.account_phone
RunnableC19866k.javar5.kRunnable31 branches — getUpdatedContactInfo_ delta application, givePreEyeconUsage, ad/native-ad fitting, reminders. See §2.7
2.1 r5.e = “AddressBook” — ContactsProvider reader (flow 1)h5

Static state [OBS]:

public static String f51134a = ""; // detected contacts language (iso-639-1, lowercase)
public static final C16945i f51135b = new C16945i(1,1,"AddressBook", true); // single-thread executor
public static final ContentResolver f51136c = MyApplication.getContentResolver();
public static final C19854e f51137d = new C19854e();
public static int f51138e; public static C11859p f51139f; public static C19846a f51140g;
public static boolean f51141h; public static final HashMap f51142i; public static int f51143j;
public static String f51144k; public static int f51145l; public static final HashMap f51146m;

Key methods:

  • static InputStream m36574A(ContentResolver, Uri, AssetFileDescriptor) — uriWithAppendedPath + projection {"data15"} (thumbnail).
  • static String m36575B(String displayName) — query ContactsContract.Data.CONTENT_URI where mimetype = ? AND display_name = ? and mimetype = "vnd.android.cursor.item/vnd.com.whatsapp.profile"; returns the WhatsApp profile data value → WhatsApp-ID extraction from the address book.
  • static String m36576C(Cursor) — generic accessor.
  • static AtomicInteger m36577E(String, HashMap) — language histogram; if count > current max, stores the language in f51144k.
  • static ArrayList m36584e(String) / m36585f(long, String) — call-log queries.
  • static Uri m36586g(ContactInfo) — ContentProviderOperation batch that WRITES a contact into the device address book: newInsert(RawContacts.CONTENT_URI) + newInsert(Data.CONTENT_URI) with mimetype=vnd.android.cursor.item/name and mimetype=vnd.android.cursor.item/phone_v2. (i.e. MICO can create contacts on the device.)
  • static void m36579G(long, byte[]) — writes/updates a raw-contact photo via openAssetFileDescriptor(RawContacts/<id>/display_photo, "rw").
  • static void m36582c(C19854e, byte[], long) / m36583d(long) — deletes the photo / the call-log row.
  • static ArrayList m36592m(String) — RawContacts ids for contact_id = <id>.
  • static Bitmap m36594o(int, String, String) — photo decode.
  • static C16470g0 m36595p(Cursor, String) — builds a social row from an email_v2 data row: social_type = r5.h0.EMAIL.f51206b, social_id = <data1>, contact_id = <id>, social_name = "0", social_source = 0.
  • static C16470g0 m36599t(Cursor, String) — same but social_type = r5.h0.SKYPE.f51206b.
  • static C19846a m36600u() / m36590k() / m36591l() — picks the default sync account; writes {"type":…,"name":…} into f51139f.
  • static long m36597r(String) — RawContacts._id from RawContacts.CONTENT_URI where _id=<rawContactId>.
  • static Bitmap m36604y(Uri) / m36605z(String contactId) — Contacts.CONTENT_URI/<id> photo fetch.
  • static void m36603x(Object[] out, boolean limitToStarred, boolean limitToTop100, Runnable done, int retryFlag) — the reader.

m36603x step-by-step [OBS]:

  1. out[0]/out[1] are the out-params: [0] = C16491s of contact rows, [1] = C16491s of social (email/skype) rows.
  2. Builds the projection (note the duplicate data1 three times and that data5 appears once): data1, data2, data3, contact_id, raw_contact_id, data1, data5, data1, photo_uri, photo_thumb_uri, lookup, starred, pinned, display_name, is_super_primary, account_type, [account_name], mimetype account_name is only appended when SharedPreferences.getString("", "SP_EYECON_DEF_ACCOUNT_V2") is empty (i.e. no user-selected default account).
  3. MyApplication.m10348j() -> SP_EYECON_DEF_ACCOUNT_V2; AbstractC0062l0.m371z1() (default country ISO).
  4. Spawns Executors.newFixedThreadPool(1) and submits RunnableC19318r(hashMap, 5) — a background task in p6 that also drives content://sms reads (§6).
  5. ContentResolver.query(ContactsContract.CommonDataKinds.Phone.CONTENT_URI, proj, null, null, limitToStarred && limitToTop100 ? "starred DESC, pinned ASC, contact_last_updated_timestamp DESC LIMIT 100" : null). [OBS] the 100-row cap is only applied when both flags are set.
  6. Per row:
    • mimetype == "vnd.android.cursor.item/com.skype.android.chat.action" and data5 == 3 -> m36599t(...) (Skype social row).
    • mimetype == "vnd.android.cursor.item/email_v2" -> m36595p(...) (EMAIL social row).
    • otherwise read data1 (the number). If non-empty:
      • C18315e.m34460c(number) (the normaliser, disk p394l5) returns C18311a with f46071b = normalised/server CLI (cli_in_server) and f46072c = digits-only CLI (cli). If f46071b is empty (non-mobile / VOIP / service number) the row is skipped for the normalised path and only cli = <raw data1> + name = display_name is kept in a secondary list.
      • dedup: HashSet keyed on contact_id (map8) holding already-seen f46071b values. Only the first occurrence of each normalised number per contact becomes a canonical row: name = display_name, cli_in_server = f46071b, cli = f46072c.
      • otherwise (f46071b empty) the row goes to arrayList with cli = data1, name = display_name and is normalised later, after the cursor is exhausted, using map3 (the cache filled by RunnableC17219d1).
    • The skipped-number path re-checks the same HashSet and, on success, writes name (falling back to the digits-only CLI), cli_in_server, cli and appends to the same C16491s.
  7. Tail: executorService.shutdown(); awaitTermination(100, SECONDS).
  8. Language detection: map.get(f51144k.toLowerCase()) -> Locale; f51134a = locale.getLanguage().toLowerCase().
  9. Persists SP_KEY_TOP_CONTACTS_LANGUAGE = f51134a and SP_KEY_AMOUNT_OF_CONTACTS = contacts.size().
  10. Sets out[0] = contacts, out[1] = socials, then invokes the Runnable done callback.
  11. Retry: moveToFirst() == -1 && i3 == 0 -> close and re-enter m36603x(..., i3=1); moveToNext() == -1 && i3 == 0 -> same. So a second full pass is attempted once.

Where the rows go afterwards [OBS]: r5.k case 11 assigns dBContacts.f11729g = (C16491s) obj[0] and dBContacts.f11730h = (C16491s) obj[1] then calls m10144t(...). The actual INSERT/UPDATE into contactdb.contacts is inside DBContacts (out of cluster).

2.2 r5.h0 = SocialType enum — the social_type vocabulary [OBS]h5

Constructor (int id /*f51206b*/, int iconRes /*f51207c*/, int coloredIconRes /*f51205a*/, String packageName /*f51208d*/, int kind /*f51210f*/). m36617d() returns kind == 4 (“id is a phone number”); m36616c() returns kind != 32 && installedAppsSet.contains(id), where the installed set is cached in SharedPreferences sp_social_installed_apps_v2 (default "[]").

constantidpackagekindconstantidpackagekind
FACEBOOK1com.facebook1SMSTRANSIT_FRAGMENT_MATCH_ACTIVITY_OPEN (=65536)sms4
LINKEDIN2com.linkedin.android1CALL4101”call”1
VKONTAKTE4com.vkontakte1EMAIL4128”email”1
INSTAGRAM8com.instagram1NAVIGATION4129”navigation”16
BBM..YAHOO_MESSENGER4101..4119…1CALENDER (sic)4132”calender”16
KIK/IMO/OOVOO/AZAR/…4121..4127…1VIDEO_CALL4134”video_call”1
WHATSAPP4118com.whatsapp (or com.whatsapp.w4b)2SHARE_MY_LOCATION4135”share_my_location”16
WHATSAPP_CALL4130com.whatsapp2CUSTOM_SOCIAL-1""1
WHATSAPP_VIDEO4131com.whatsapp4TRUE_CALLER-10com.truecaller1
TELEGRAM4114org.telegram.messenger4TOKI-11""1
VIBER4116com.viber4GOOGLE_PHOTOS4222""1
QQ4109com.tencent.mobileqq4ADD_CONTACT4223""1
SKYPE4111com.skype.raider32REPORT_SPAM4224""1
WE_CHAT4117com.tencent.mm1CAN_TALK4225""1
HANGOUTS4104com.google.android.talk1CALL_REMINDER4226""16
GOOGLE_MESSENGER4125com.google.android.apps.messaging1NOTE4227""16

m36614a() returns a cached display label: for ordinals 13/17/18/27..37 it uses a localised string resource, otherwise AbstractC0042b0.m169R(packageName) (label lookup), falling back to the enum name lower-cased with _->space.

m36647c(ArrayList, SocialInfo) gate [OBS]: a new social is appended only if (a) kind != 32 (SKYPE excluded), and (b) the app is installed OR the type is one of {FACEBOOK, INSTAGRAM, VKONTAKTE, TWITTER, LINKEDIN} and socialID is non-empty, and (c) no existing entry has the same numeric id. Duplicate ids merge by taking the first non-empty socialID and adding the incoming score.

2.3 r5.i = ContactInfo [OBS]h5

Fields (≈70): contact_id, phone_number (cli), phone_number_in_server (cli_in_server), phone_number_type, private_name, score, calculateScore, pre_eyecon_usage_sum, history_call_log_rowId, callDateInMillisecond, eventType, event_duration, lastEventTimeMS, account_type, historyAccountId, type, position, coreIndexing, unicodeScore, ArrayList<r5.j> contactClis, ArrayList<ContactInfo> linked_contacts, ArrayList<r5.h> participants, String participantsJson, r5.i0 mSocialManager, C11859p extraValuesJson, C11859p ttmJson, r5.w freshPic, String freshPicInitUrl/Tag, boolean has_fresh_pic_init, String picTag, String picLastApproveTag, String surveyPicURL, boolean isSurveyPic, boolean hasPhoto/hasName/hasImageInServer/hasNameInServer, boolean shouldFetchName/shouldFetchImage/isStartForcingPhoto/isNeedHashUpdate, String storage_photo_path, String ab_photo_uri, int ab_photo_type, String default_cis, long favorite_timestamp, int fav_pinned_position, int ab_fav_pinned_position, int eyecon_old_fav_pinned_position, boolean isStarred/isHeart, boolean isSpam/isSuspiciousSpam/isPendingContact/isSuggested/isReadOnlyAccount/has_name_history, boolean hasDownloadDate, String contact_id, String[] spliced_name.

Constructors: ContactInfo(String contactId, String phone, String privateName, boolean hasPhoto); ContactInfo(JSONObject) (fully reflective, special-cased for participants, contactClis, mSocialManager, T9Array, linked_contacts, extraValuesJson, freshPic, private_name, spliced_name); toJSONObject() (m36622C); deep-copy static m36618c(ContactInfo).

Methods of note:

  • m36631m():r5.j — primary PhoneNumberInfo (synthesises one from phone_number if the list is empty).
  • m36632n():r5.j — same, with cli_in_server explicitly seeded.
  • m36633o():r5.j — the first “mobile-looking” number (l7.b.m34450o(cli, AbstractC0062l0.m371z1())) — used as the WhatsApp target.
  • m36629k():r5.j — the number with isSub == true.
  • m36635q():r5.j — number matching l7.b.m34444e(phone_number) (the default number).
  • m36634p(String):z6.a — per-number note; m36636r(String):String — per-number defaultSimId.
  • m36637s(r5.h0):r5.g0 — social info for a type; m36638t(char[][]) — T9 predictive digits of private_name (space->0), memoised on T9Array identity.
  • m36641w() — “has a possible mobile number” (cached hasPossibleMobileNumber).
  • m36643y() — true if contact_id is null/empty or starts with "-1", i.e. the record is NOT a real device contact. This is the app’s “is this an unknown number?” test used all over the UI (and m36630l() returns cli_in_server vs contact_id on that basis).
  • m36644z() — participants.size() > 1 (group message).
  • m36619i(int eventType) — icon table (0 block, 1/10 in, 2 out, 3 missed, 5 reject, 6/8 sms, 11/12 cantalk, 13..16 toki, 17 add-contact, 18 call-reminder, 19 toki/ttm).
  • m36620A(String) / m36621B(String) — set extraValuesJson / storage_photo_path (the latter also fires RunnableC3460a from b5).
2.4 r5.j = PhoneNumberInfo [OBS]h5

\{cli, cliType, numericCli, note(z6.a), score, pre_eyecon_usage, canTalkStatus, canTalkTimestamp, isSub, osType, isViewer, version, cli_in_server(private, cached), defaultSimId, label, isCanTalkEnable, isDefault, hasTtm, isReadTtm\}. Constants CANTALK_TYPE_EYECON=0, CANTALK_TYPE_WHATSAPP=1, CANTALK_TYPE_SMS=2; PRIVATE_NUMBER_PREFIX = "PrivateNumber".

  • m36671a() = lazily cli_in_server = l7.b.m34444e(cli) (the server-side normalised number).
  • m36673e() = cli_in_server.startsWith("PrivateNumber").
  • m36668b(String type) maps raw Android phone-type strings: "1"->1 (MOBILE), "2"->2 (HOME), "3"->0, "12"->3, else -1.
  • m36669d(int type, String label) resolves the human label via ContactsContract.CommonDataKinds.Phone.getTypeLabel(...) into a SparseArray cache.
  • compareTo orders by isDefault desc, score desc, type-rank, then cli lexicographic.
2.5 r5.i0 = SocialManager [OBS]h5

Static state: f51213d = R.array.global_priority_AB_testing when the remote flag isFacebookSecond is on, else R.array.global_priority — the social-type priority ordering; f51212c = installed-apps id set; f51215f = “always-allowed even if not installed” list {FACEBOOK, INSTAGRAM, VKONTAKTE, TWITTER, LINKEDIN}; f51216g = “device-action” list {SMS, NAVIGATION, CALENDER, SHARE_MY_LOCATION, NOTE, CALL_REMINDER}; f51217h = scratch string.

Facebook-ID path — m36651g(Activity, String socialId, C3500g) [OBS]:

if (isEmpty(socialId)) return false;
if (!remoteFlag("isTestingFbProfileLinkEnableV2")) return m36654j(act, socialId, -1); // legacy
String url;
try { Long.valueOf(socialId); url = "https://m.facebook.com/profile.php?id=" + socialId; } // numeric -> id
catch (NumberFormatException) { url = "https://m.facebook.com/" + socialId; } // username
long t0 = SystemClock.elapsedRealtime();
act.m3727x0(true); // show blocking progress
C19855e0 probe = new C19855e0(act, socialId, t0, c3500g);
String extracted = m36649e(url, FACEBOOK);
if (extracted.isEmpty() || C21280h.f56471g.contains(extracted)) probe.m31518o("NOT_A_FB_PROFILE_ERROR");
else C16945i.m32774f(C21280h.f56470f.f56472a, 0, new RunnableC20056j(url, probe, extracted, false, 19));

m36649e(String url, SocialType) [OBS] is the FB-ID extractor: for path.contains("profile.php") it returns uri.getQueryParameter("id"); otherwise it splits the path on / and returns the first segment matching ^(?!share$)[a-zA-Z0-9_.]+$. m36654j (the launcher) picks, in order: fb://facewebmodal/f?href=https://www.facebook.com/<id> / fb://profile/<shortNumericId> (<=15 digits and not starting with 1) when the FB app is installed, else https://m.facebook.com/<numericId> or http://m.facebook.com/profile.php?id=<username> (note cleartext http:// on the username path). m36653i opens https://www.facebook.com/search/top/?q=<name> (FB app installed) or https://www.facebook.com/public/<name>. Everything is done with startActivityForResult(intent, 100).

Other launchers [OBS]: m36655k Instagram http://instagram.com/_u/<id> (cleartext http); m36656l LinkedIn https://www.linkedin.com/in/<id>; m36659o vkontakte://profile/<id> then http://new.vk.com/id<id>; m36658n Telegram tg://search?query=<name>; m36646b QQ mqqwpa://im/chat?chat_type=wpa&uin=<id>&version=1 (reads data1 where data5==4); m36664t Skype skype:<id>?chat with explicit component com.skype.raider/.Main (reads data1 where data5==3); m36648d/m36661q ACTION_INSERT on CalendarContract.Events.CONTENT_URI; m36662r ACTION_SEND text/plain pinned to the social package; m36666v opens the app-internal “quick actions” sheet (c6.b); m36667w uses DummyActivity with action EYECON.INTENT_ACTION_UNLOCK_KEYGUARD_AND_OPEN_WHATSAPP_WITH_FALLBACK.

2.6 r5.r — global dispatcher [OBS]h5

Case 4 is a contactdb wipe inside a transaction: delete history; delete contacts; delete social; delete fresh_pics; delete block_list; delete recorded_notes; (uses C0041b.m156t().m160D(500L) to obtain the writable DB). Table names confirmed: history, contacts, social, fresh_pics, block_list, recorded_notes. Case 11 (updateSgn) POSTs {"signatures":[…],"cid":<cid>,"random":<rand>} RSA-encrypted with the public key in R.string.pk_enc_2 to https://api.eyecon-app.com/app/grps/sgn, gated by remote flag sig_report_config.is_enable and a cooldown_days_after_join window; flag SP_KEY_DID_UPDATE_V1453. Case 12 POSTs/GETs updateuserdetails.jsp with username=<trimmed username> + apsVersion=vc_749_vn_4.2026.04.23.1145_a, cached in pref usernameserver. Case 13 schedules ShareMediaJob (id 24) periodically with interval remote("media_share_job_interval") seconds, setRequiredNetworkType(NETWORK_TYPE_ANY), setPersisted(true). Case 10 resets SP_KEY_RECORD_CALLS_MODE = 0 and re-renders the recordings fragment. Case 16/17 toggle SP_KEY_BLOCK_SPAM_CALLS.

2.7 r5.k — the getupdatedcontactsinfo delta application (flow 2, server side) [OBS]h5

case 12 receives two lists: contacts (rows from the contacts table, table id 9) and socials (rows from the social table, table id 12) and merges them:

  1. Contacts pass — for every incoming contact row, DBContacts.m10108g(contactsTable, row, contactsTableComparator(9)) returns all locally stored numbers belonging to the same person; each one inherits is_viewer, os_type, is_sub, is_can_talk_enable, and vn (copied through m32032a/… with a clamp m32031z(0, vn)). All are collected in arrayList9.
  2. Socials pass — for each incoming social row:
    • all local social rows matching it get social_id overwritten and social_source = 1 (meaning “came from the server”), and their contact_id values are recorded into hashSet;
    • then every contact number of that person that does not yet have a social row gets a new social row built with: contact_id, cli_in_server (copied from the contact row), social_score = 0, social_id, social_source = 1, social_type, social_name;
    • if social_type == FACEBOOK(1), a second row is additionally inserted with social_type = FB_MESSENGER(4103) — i.e. a Facebook match implicitly creates a Messenger match.
  3. DBContacts.m10099U(null, null, socialRowsToWrite) persists the social table, then dBContacts6.f11726d = DBContacts.m10092D() reloads it.
  4. DBContacts.m10093O(null, null, arrayList9) persists the contact rows, then dBContacts6.f11724b = DBContacts.m10118v() reloads them.
  5. dBContacts6.m10135X("getUpdatedContactInfo_") marks the table dirty; m10134W()/m10123E() flush.
  6. Logs "DBContacts", "getUpdatedContactInfo_ done time = %s, contacts = %s, socials = %s".

Related branches: case 8 = givePreEyeconUsage — sorts the contacts table by pre_eyecon_usage descending and copies that usage weight onto every phone row of each contact, guarded by SP_KEY_IS_GIVE_PRE_EYECON_USAGE; case 15 clears per-contact caches and sets updatePicsByForceDone; case 8/7/16/17 add tags "main", "social", "main_mix" to the refresh HashSet; case 20 loads SP_KEY_EYECON2_REMINDERS; case 18 sets a 75 dp bottom margin on ForYouFragment.


3. Package q6 (disk p493q6, 55 files, 171 KB) — main-screen RecyclerView widget kith4

[OBS] This is the UI toolkit behind the Communication area (History / Contacts / Favorites). The tab enum is q6.w (EnumC19631w):

HISTORY (ordinal 0, titleRes -1, R.layout.fragment_history_layout)
FOR_YOU (ordinal 1, titleRes -1, R.layout.fragment_for_you_and_favorites_layout)
FAVORITES(ordinal 2, titleRes R.string.favorites, R.layout.fragment_for_you_and_favorites_layout)

The cell-layout enum is q6.d0 (EnumC19593d0, JADX could not restore enum — it is a class with a manual values()): LIST(1), GRID_CELLS_IN_ROW_3(2), GRID_CELLS_IN_ROW_2(3), GRID_MAIN_CARD_VIEW_3(4), GRID_MAIN_CARD_VIEW_PYRAMID(5), GRID_MAIN_CARD_VIEW_2(6), LIST_MAIN_CARD_VIEW(7). m36052b() = “is a plain list” (LIST or LIST_MAIN_CARD_VIEW); m36051c() clamps the card height to h1(88) if the computed cell height would break the 8 dp gutter.

q6.j (AbstractC19605j) = base class of HistoryFragment, ForYouFragment and the favourites fragment. Layout R.layout.fragment_communication_layout. Key members [OBS]:

  • m36060r0(RecyclerView, q6.d0, ArrayList, q6.w, q6.j, boolean, boolean) — installs the right layout manager + decoration + q6.v adapter. LIST -> q6.c (linear, 1 column); GRID_MAIN_CARD_VIEW_3 -> 3-column grid + q6.h; GRID_MAIN_CARD_VIEW_PYRAMID -> 6-column grid + q6.d; otherwise 2-column grid + q6.f. Bottom padding = max(d0.f50353a, h1(350)).
  • mo10082M() — returns ((MainFragment) getParentFragment()).f12025k.getCurrentItem(), i.e. the current ViewPager tab index of the host.
  • mo10087h(ContactInfo) / mo10085X(ContactInfo, View[], q6.w) — opens the quick-action sheet (u6.a, tagged "History" / "ForYou" / "Favorites") or the menifa (SocialManager) dialog.
  • mo10006k0(Bundle) — registers itself in MainFragment.f12024j and captures the search text from MainFragment.f12027m.getText() into f50395i (trimmed); reads arguments.getBoolean("EXTRA_KEY_INIT_WITH_KEYBOARD_OPEN").
  • mo31174n0() — forwards MainActivity.getIntent() to mo10240A(intent).
  • m36059y0(Rect, View, RecyclerView, int span, boolean) — 1/3-width offset decoration for the 6-column pyramid grid (each cell is m183B(88) wide but the visible card occupies 1/3).

q6.r (InterfaceC19621r) is the fragment<->adapter contract [OBS]: D(ContactInfo,boolean,Adapter), F():Set<String>, M():int, S(ContactInfo), W():AbstractActivityC3498e, X(ContactInfo,View[],Tab), d0():boolean, h(ContactInfo), k(q6.e0).

Other classes [OBS]:

realpurpose
q6.a1abstract RecyclerView.OnScrollListener — shared scroll behaviour (infinite scroll for the contact list)
q6.bOnScrollListener impl used by m36061v0
q6.cLinearLayoutManager subclass (1 column)
q6.v0LinearLayoutManager subclass (top-calls area)
q6.d/q6.f/q6.h/q6.x0/q6.y0RecyclerView.ItemDecoration for 6-/2-/3-column grids and list
q6.e/q6.g/q6.i/q6.z0GridLayoutManager.SpanSizeLookup — full-span detection for ad/header rows
q6.k/q6.k0abstract RecyclerView.ViewHolder base + a plain holder
q6.e0abstract clickable/long-clickable holder (implements t6.h, am.b1)
q6.i0contact-list cell holder: binds R.id.PH_checkbox (EyePlaceHolder), EA_contact_photo (EyeAvatar), LAV_loading_photo (Lottie), IV_badge, CL_info_area (ContactListInfoArea), EB_burger, EB_quick_action, simIcon, TV_sim_index, noteIcon
q6.zcontact-grid cell holder: CG_info_area (ContactGridTextArea) + FL_contact_photo_container
q6.vthe main adapter — layouts R.layout.eye_contact_list_cell, R.layout.eye_contact_grid_cell, R.layout.empty_view, R.layout.eye_title_separetor; getItemViewType dispatches list vs grid vs empty vs title; m36075e(ArrayList) filters/sorts the incoming list (reads the active tab index via mo10082M()); m36079j(RecyclerView, ArrayList) performs a diff and only calls notifyItemChanged over the visible window (m36071a..m36072b); m36070g(View) closes an open popup; row 0 on ForYouFragment runs q6.n0.m36067c(...)
q6.nRecyclerView.Adapter for call-log cells (R.layout.call_log_cell)
q6.mcall-log row holder: IV_sim, IV_type, IV_circle, TV_duration, TV_date, TV_time, FL_sim, TV_sim
q6.s03-button row holder (EB_left_btn, EB_middle_btn, EB_right_btn)
q6.o0shimmer placeholder holder (SFL_contacts, IV_icon, TV_text)
q6.c1per-adapter cache object (ArrayList cleared by r5.k case 15)
q6.n0, q6.k0, q6.a0, q6.c0, q6.u0, q6.lsmall helpers: diff delegate, popup helper, scroll-state holder, RecyclerView.ItemAnimator subclass, layout-param helper
q6.w0RecyclerView.OnFlingListener
q6.xFragmentStateAdapter for the ViewPager2
q6.m0enum { SINGLE }
q6.b1, q6.p, q6.d1interfaces: click-dispatch, contact-source contract (a/b — implemented by r5.i), search/host contract

4. Package j7 (disk p360j7, 20 files, 144 KB) — permission dialogs + dialog baseh4

[OBS] Despite the TSV owner “MainScreen”, j7 is mostly permission/consent UX, plus the shared dialog base class.

realpurpose / key members
j7.mPermissionStatusTracker. static C11859p f44091a; single-thread executor named "PermissionStatusTracker". m33635a() builds the JSON: on SDK≥29 also android.app.role.CALL_SCREENING (zq.g.m39742H()) and android.app.role.DIALER (m39745K()), plus DrawAbove, readNotification, batteryOptimizations, fullScreenNotification, and every runtime permission returned by zq.g.m39790s(). Persisted to pref permission_status_json. m33638d(boolean) -> u5.z.m37804b(true); m33639e(String,boolean) posts an async refresh. Consent-relevant: this is the machine-readable record of which permissions the user granted, used to decide which dialogs to show again.
j7.tthe generic bottom-sheet/dialog base (66 KB, extends c7.d). 12 boolean layout flags A..J,O,P,z,y,L; nine EyeSwitch fields o..w; CustomTextView f44144x; String f44117K = "dialog_not_set"; Dialog f44120N; Handler f44124R; volatile boolean f44126T; long timers S,U. Subclassed by j7.a (C17515a extends C17534t) and by every MICO dialog (including the wrappers named ContactListInfoArea / ContactGridTextArea).
j7.aconcrete ContactListInfoArea-family sheet (extends j7.t)
j7.cpermissions explainer dialog — layout R.layout.feature_permissions_dialog, ids EB_ask_for_permissions, EB_close, IV_image, LAV_anim, TV_msg, TV_title
j7.d”force default dialer” dialog — R.layout.force_default_dialer_dialog, ids EB_close, EB_continue, ll_smarter_call, ll_spam, ll_photos; titles R.string.force_callet_id_dialog_title (sic) and R.string.enable_caller_id_role_msg; hides the per-feature rows and calls t0.j0.m33033r(ViewGroup) to conditionally show them
j7.jsingle-permission prompt — R.layout.permission_prompt, ids FL_header, TV_title, TV_one_permission, IV_close, FL_ok, LL_permissions; cells R.layout.permission_prompt_cell (TV_name, TV_explanation)
j7.l”must enable” dialog — R.layout.must_enabled_permission_dialog, ids TV_phone_title, TV_title_callerid, TV_phone_message, TV_caller_id_msg, EB_ok, IV_close
j7.hstatic helper: static C17534t f44084a (cached dialog); m33629a(Activity), m33630b(Activity,String), m33631c(Activity,boolean,boolean,int), m33632d(Activity,boolean,boolean,String,boolean), m33633e(String)
j7.b, j7.rasync callback adapters (extends e2.b)
j7.gRx Observer
j7.pCompoundButton.OnCheckedChangeListener
j7.xsingleton (f44155g) holder
j7.wDefaultLifecycleObserver
j7.e, j7.n, j7.q, j7.s, j7.u, j7.vsmall runnables

5. Package s5 (disk p541s5, 19 files, 139 KB) — DBContacts worker poolh4

[OBS] Every mutation of contactdb is marshalled onto DBContacts.f11708K (a h7.i executor) as one of these Runnables. This is where the contact-removal upload and the fresh-photo distribution live.

realpurpose
s5.aComparator factory holder (7 lazily built comparators on a7.u column sets) + 2 C19290a sort descriptors
s5.b(DBContacts, HashSet tags, Runnable[] done, long elapsed) — tag "main" (case 16) / "social" (case 17); drains and signals completion
s5.c(DBContacts, int) — DBContacts bootstrap/reset
s5.d(long, int[], boolean, boolean, C16491s) — counting/normalisation tail worker for r5.e
s5.e(DBContacts, Runnable, ArrayList) / (DBContacts,Object,Object) — post-merge refresh; case 2 drives w6.h-based server contact refresh on the SP_REQUIRED_SERVER_CONTACTS_REFRESH flag
s5.fwrapper for am.u1
s5.g(DBContacts, r5.w FreshPic, int) — assigns a fresh pic to one contact
s5.h(int, boolean, String, long, long, String)
s5.i(DBContacts, ArrayList, boolean, boolean, e2.b) / (DBContacts, String, String, boolean, boolean)
s5.j54.8 KB dispatcher, 19+ branches: setDefaultSim, setDefaultPhoneNumber, setFavoritePinPosition, giveContactsFreshPicUrls, _FreshPicFinishedCallback, init1-…. Case 10 (giveContactsFreshPicUrls) logs "giveContactsFreshPicUrls skip, has large photo. type = %s, cis = %s" and "giveContactsFreshPicUrls photo is waiting to show. name = %s, cis = %s", writes with DBContacts.m10096R(null, arrayList12, "giveContactsFreshPicUrls#"+tag) + m10093O(null,null,arrayList10), then m10124F(null,false,true) to re-render
s5.k(DBContacts, String, e2.b) — name/photo fetch single contact
s5.l(DBContacts, ArrayList, int)
s5.m(DBContacts, ArrayList[], bm.RunnableC3589a, int)
s5.n(String, String, boolean, int, String) / (DBContacts, String, Runnable, int, boolean)
s5.o(DBContacts, ContactInfo, int, int)
s5.p(DBContacts, String, Runnable, ContactInfo[], Bitmap, ArrayList, String)
s5.q, s5.rinner-dispatch helpers
s5.sremoval uploader — see §5.1
5.1 s5.s case 0 — share-score bump [OBS]h5

Reads table 9 (contacts) with comparator, then for each ContactInfo being removed: DBContacts.m10108g(tableRows, keyRow{c cli_in_server = <removed>.m36631m().m36671a()}, comparator); for every matched row it parses extra_values_json (a7.f229j0), increments socialMediaShareScore, writes the JSON back, and batches via DBContacts.m10093O(null,null,arrayList) + m10135X("updateContactShareScore") + m10134W() + m10123E().

5.2 s5.s case 1 — removecontacts.jsp (flow 2) [OBS]h5

Full source quoted in §0.1 (correction C6). Record format <cli_in_server>@a@<name>@z@; parameters contacts and apsVersion; URL AbstractC0039a.f168L1 + "removecontacts.jsp" = https://api.eyecon-app.com/app/removecontacts.jsp; method POST; dispatched through x6.C21649b.f57893a.m38854c(new x6.C21653f(params, url, "POST", "removecontacts.jsp")).


6. Package p6 (disk p475p6, 16 files, 103 KB) — app glue, SMS/MMS reader, ad/analyticsh4

[OBS]

realpurpose
p6.j29-branch global dispatcher. case 13 registers a JobScheduler job id 23 for SmsJobService with addTriggerContentUri on content://sms/, content://mms/, content://mms-sms (API 24+), gated by SmsFragment.m10565w0() and pref SP_IS_SMS_WINDOW_ENABLE_BY_USER. default branch = IMEI upload: GET https://api.eyecon-app.com/app/updateuserdetails.jsp?imei=<C18502p.m34691f()>&apsVersion=vc_749_vn_4.2026.04.23.1145_a with {NETWORK_ERROR=-10}; cached in pref imeis_updated; skipped if already uploaded or not a validated Eyecon user.
p6.rSMS/MMS reader (33 KB). contentResolver.query(content://sms/inbox, null,null,null,"date DESC") -> builds C20684e(date, body, address, person, "SMS", read); also Telephony.Mms.CONTENT_URI. case 14 does a rawQuery over the local DB reading columns modify_time, plus two C16458a0 column names (the local SMS DB).
p6.f/p6.ge2.b async callback + FragmentManager.FragmentLifecycleCallbacks
p6.nHandler.Callback
p6.oViewPager2.OnPageChangeCallback
p6.sMotionLayout.TransitionListener
p6.pmain-screen toolbar click handler (14 branches): R.string.favorites + R.string.organize_favorites, R.string.for_you, R.string.history, R.string.select
p6.qView.OnFocusChangeListener
p6.a, p6.c, p6.e, p6.ismall runnables; p6.i case 0-3 = dialer-default-role / SMS role flows

7. Package d5 (disk p236d5, 5 files, 101 KB) — remote-config / A-B flag catalogueh4

[OBS] d5.d (AbstractC15804d) is a 680-line class whose static block fills public static final HashMap f38507a with 400 default remote-config entries, plus public static ArrayList f38508b (the list of allowed keys). Typed accessors: m31081b(String):boolean, m31082c(Class,String):Object, m31083d(String):double, m31086g(String):int, m31087h(String)/m31088i(String) (JSON), m31089j(String):long, m31090k(String,long):long, m31091l(String,boolean):String, m31093n(String):String, m31084e()/m31085f(boolean) (a C17307b singleton), m31080a(d5.e) (refresh completion).

7.1 Security-relevant default values [OBS]h5
keydefaultnote
eyecon_remote_config_urlhttp://cloudfront-cdn-all.eyecon-app.com/configs/ab-test.txtCLEARTEXT HTTP — remote config, A-B tests and feature flags (incl. shouldSendNameToServer, ApiRequestSyncInterval, ad unit ids, regexForAddSocial) are fetched over plain HTTP and are trivially MITM-able → remote control of privacy-relevant behaviour. There is also eyecon_remote_config_version.
shouldSendNameToServerfalse (bool2)remote switch to push contact names to the server
ApiRequestSyncInterval20seconds
user_agentMozilla/5.0 (Windows NT 10.0; Win64; x64) …Chrome/123.0.0.0 Safari/537.36desktop UA used for in-app web requests (WebView fingerprint spoofing)
facebooke_Extract_URL_Regexbrowser_native_sd_url":"([^"]+)"regex used to scrape Facebook profile URLs from an HTML page
script_facebook_profile_validate"" (empty)remote-injectable JS
shouldSearchForTwoNamesInFacebookfalsetries first+last name
regexForAddSocialJSON blob of 5 regexes for facebookProfileUrlPattern, facebookDigitProfileIdPattern, instagramProfileUrlPattern, instagramImageProfileUrlPattern, instagramMatchesRegexregexes that parse Facebook/Instagram profile markup — remote-config-overridable
com_start_page1default tab (1 = FOR_YOU/Contacts)
com_title_for_history / com_title_for_you"history" / "contacts"
com_history_default_style7 (= LIST_MAIN_CARD_VIEW)
caller_id_mode_v23
show_caller_id_before_datatrueshow caller-ID UI before any server data arrives
show_get_photo_on_contactstrue
display_facebook_linktrue
whatsappProfileBase64Image[]remote-pushed WhatsApp profile pictures
google_photos_link_loaderJSON containing a JS search_results/links_script and a big_url_script injected into a WebViewremote-controlled WebView JS; placeholders [MAX_LINKS], [WEBVIEW_TAG], [ITEM_ID]
open_whatsapp_intentsJSON with 4 WhatsApp deep-link variants incl. S.jid=[cis]%40s.whatsapp.netdrives WhatsApp deep links from the contact’s cis
mobitech_*_html_script (3)raw <script src="https://the-news-desk.com/js/displaytag*.js" user_id="[advertisingId]" publisher_key="[publisherKey]" sub_id="[placementId]">ad-SDK HTML injected into WebViews; [advertisingId] = device ad id
welcome_back_notification_urlJSON with a Google-Forms url and dismiss_url
call_contest_report_calls_urlhttps://apps.eyecon-app.com/call-contest/a/report-calls
ac_ads_test_internet_connection_urlhttps://www.google.com
ac_ad_unity_id_v2, ac_native_ad_unit_id, history_ad_unit_id, rewarded_ad_unit_id, …AdMob ids ca-app-pub-2959190743815944/…
premium_features{"reverse_lookup":{"is_premium_feature":true,"amount_for_free":2,…},"photo_search":{…2…},"bg_colors":{…0…}}paywall config
singular_events_json, amplitudeNotTrackableEvents, sig_report_config, uid2_config, snoozeAutoSec`JSON blobsanalytics / ad-id resolution
is_amplitude_enabled, is_amplitude_enabledAmplitude (incl. SessionReplay, see §20)
backup_enable, spam_list_sync_interval, share_video_list_sync_interval`“sync/back-up switches”

Other classes: d5.a (loader), d5.b/d5.e (async callbacks), d5.c (remote-config fetch helper).


8. Package jc (disk p364jc, 14 files, 97 KB) — fragment/dialog bases + inset helpersh4

[OBS] jc.d (29.8 KB), jc.g (18.3 KB), jc.e (13.1 KB) are the largest.

realkindnote
jc.afinal classsmall holder
jc.binterfacejc.m implements it
jc.csynthetic impl of InterfaceC21910f, InterfaceC21911glifecycle glue
jc.dlarge classbase for dialog/fragment overlays
jc.esynthetic impl of InterfaceC21910f, InterfaceC17221e, InterfaceC20998d, OnApplyWindowInsetsListener, InterfaceC18495iwindow-insets handling (status/nav bars) for the contact screens
jc.gimpl of InterfaceC17260p1, InterfaceC17569c0, InterfaceC18573banimation/transition contract
jc.h, jc.i, jc.l, jc.m, jc.n, jc.osmall helpers (jc.m implements jc.b)
jc.j, jc.kabstract bases

9. Package u6 (disk p574u6, 17 files, 72 KB) — top-calls / recents rowh4

[OBS] u6.a = the quick-action sheet model (constructed by q6.j.mo10085X with (String screenTag, ContactInfo)); u6.u = its RecyclerView.Adapter; u6.d (20.5 KB) = the refresh runnable; u6.m = GridLayoutManager.SpanSizeLookup; u6.n = SharedElementCallback (shared-element transition for the contact photo); u6.o = Observer; u6.k, u6.p = async callbacks; u6.g, u6.f, u6.j, u6.l = runnables; u6.q/r/s = click listeners; u6.t = interface.


10. Package b5 (disk p032b5, 8 files, 67 KB) — Events bus + photo-acquisition statisticsh4

[OBS]

realpurpose
b5.estatic final C16945i f2334a = new C16945i(1,1,"Events",false) — the app-wide Events broadcast executor. m3650a(String,String), m3651b/m3652c(Bundle), m3653d():FirebaseAnalytics, m3654e() (reads R.string.facebook_app_id and initialises Facebook App Events), m3655f/f3656g(String), m3657h(int,String), m3658i(String), m3659j(String,HashMap,boolean)
b5.dblock/unblock + DB async ops (mo415j, mo308k, mo309m, mo310p); builds the snackbar strings R.string.unblock / R.string.blocked
b5.fphoto-acquisition telemetry collector. static synchronized m3669b(int) returns 10 singletons keyed by an int. m3670a() puts into the map: Full_screen_dialer, Notification_Permission, Shortcut_added, Premium_free_user_type, In_app_purchased ("both"/"Whatsapp_Caller_ID"/"Custom_Theme"/""), Font Size, Theme, Registration_Type, isAccessibilityTalkbkOn. m3673e(int) bucketises "Time spent on page 1..3" into ">1"/"1-2"/"3-5"/"6+"; for bucket 3 it rewrites "Added Name" / "User Photo Added" to "changed" when both Eyecon-provided and user-added values exist; for bucket 8 it maps "# pre-eyecon photos" and "# eyecon found photos (1st time)" through R.array.eyecon_system_count and falls back to AMOUNT_OF_UI_CONTACTS, else a live ContactsContract.Contacts.CONTENT_URI row count
b5.hHandler.Callback — reads pref SP_KEY_PRE_EYECON_AMOUNT_OF_PHOTOS, logs "# pre-eyecon photos" and "# eyecon found photos (1st time)"
b5.a(String,int) / (String,String) runnable — used by r5.i.m36621B to register the contact’s storage_photo_path into m7.AbstractC18515c.f46750b (an ordered ArrayList<C18513b> of photo paths, kept sorted via Collections.binarySearch)
b5.b, b5.c, b5.grunnables (b5.b carries a double, a Bundle and two strings — an analytics event payload)

AMENDMENT to correction C2 (supersedes the text above). On a second pass I proved the briefing right: am\RunnableC0215t1 (disk, in this cluster) IS the contact-photo decryption routine, not merely a Firebase worker. am.RunnableC0215t1.run() is a multi-case Runnable: case 0/default are Firebase-Messaging helpers (ExecutorC0221v1, FirebaseMessaging + PowerManager.WakeLock), but case 1 is the photo decrypt path (lines 101-217): FileInputStream -> grow m7.d.f46768d -> new ObjectInputStream(new CipherInputStream(new ByteArrayInputStream(bytes,0,len), m7.d.f46765a)).readObject() -> BitmapFactory.decodeByteArray(...). The Firebase imports in the file are used by the other cases, which is what makes the class look like Firebase code. Full proof and the key-derivation trace are in §7.2. The briefing’s claim stands.


11. Package dh (disk p246dh, 17 files, 64 KB) — Firebase Remote Config personalisation + Credential Managerh4

[OBS] Despite the TSV owner “ContactReminder”, nothing here is reminder logic.

realpurpose
dh.pabstract base; static final C18984l1 f39041a (Amplitude Logger)
dh.aRemote Config personalisation signal parser — keys rolloutId, parameterKey, parameterValue, variantId, templateVersion, singleton f38998a, implements InterfaceC19134c
dh.bpersonalisation value resolver (extends dh.p)
dh.c, dh.h, dh.j, dh.n, dh.qconfig/JSON/holder helpers (dh.h hard-codes Charset.forName("UTF-8"))
dh.gextends JSONObject
dh.fimplements dh.l; static final C22174e f39015c = new C22174e(9)
dh.iimplements dh.l, OnCompleteListener, CredentialManagerCallback, InterfaceC18305c — Google Credential Manager / Play-services-auth token broker; surfaces R.string.cant_talk_right_now when the credential flow fails. canTalkStatus on r5.j is fed from this.
dh.kextends InputStream (auth-token stream)
dh.mCloseable with Logger
dh.oimplements dh.d; Charset UTF-8
dh.d, dh.linterfaces

12. Package c6 (disk p046c6, 16 files, 63 KB) — ViewPager2/animation/touch, broadcast glueh4

[OBS]

  • c6.d (AbstractC3856d, implements InterfaceC21859n) — base of every MICO bottom sheet. Constructor (View, Object, String) and, critically, ContextCompat.registerReceiver(MyApplication.f12163h, this.f4257e, new IntentFilter("EYECON.INTENT_ACTION_REFRESH_DA_UI"), 4) — every sheet listens for a global “refresh DA (dialer-addressbook) UI” broadcast. public final ArrayList f4264l.
  • c6.l (AbstractC3864l extends c6.d) — carousel variant, h1(10) / h1(30) gaps, m4107h, mo4108b0(long), mo4102g(Object,View,ArrayList), m4109i().
  • c6.p (AbstractC3868p) — h1(9) / h1(5).
  • c6.o (C3867o extends c6.l) — the MenifaFragment sheet, constructed with (View, MenifaFragment, r5.i ContactInfo) — i.e. the social-menu (“menifa”) bottom sheet keyed on a contact.
  • c6.m — after-call sheet (BaseAfterCallActivity); c6.n — SMS sheet.
  • c6.b / c6.c — async callbacks (c6.b carries (Serializable, Object, Object, Object, int) and is the quick-action / SMS-send completion handler).
  • c6.a — BroadcastReceiver for the refresh intent; c6.g — ViewPager2.OnPageChangeCallback; c6.i — page-selector iface; c6.j — ValueAnimator.AnimatorUpdateListener; c6.k — Animator.AnimatorListener; c6.h — OnLayoutChangeListener; c6.f — OnTouchListener with -h1(30) drag threshold; c6.e — runnable.

13. Package h7 (disk p315h7, 14 files, 53 KB) — the app’s executor / dispatcher layerh4

[OBS] This is the concurrency backbone: every contact-DB operation in s5 and every async callback in the app funnels through h7.i.

realpurpose
h7.iC16945i — named executor facade. Fields: static final Handler f42106b = new Handler(Looper.getMainLooper()). Ctor (int core, int max, String threadNamePrefix, boolean postToMain). API: m32769a(self,delay,runnable) (static), m32770b(self,runnable) (static, core queue), m32771c(runnable) (global), m32772d(runnable,delay) (global delayed), m32773e(self,runnable), m32774f(self, int delayMs, Runnable) (the one used everywhere: C16945i.m32774f(DBContacts.f11708K, 0, new s5.j(...))), m32775g(boolean cancelPending)
h7.hThreadFactory — names threads "FLTask-" + <prefix> + "-" + n, daemon threads
h7.fm32764a() / m32765b() — the two global single-thread executors (the one r5.g/r5.k use for contact refresh)
h7.cThreadPoolExecutor subclass with a PriorityBlockingQueue
h7.bFutureTask implements Comparable (priority wrapper)
h7.jextends Binder — local Binder stub
h7.ae2.b async callback base used by the whole app
h7.d, h7.eKotlin coroutine Continuation (invokeSuspend)
h7.k, h7.m, h7.nAdListener adapters (inneractive / Mbridge / AdMob)
h7.g, h7.lrunnables; h7.l runs JobsService.onStartJob

14. Package a (disk p001a, 1 file, 51 KB) — R8 class-merge artifacth4

[OBS] a.a (AbstractC0001a) is a abstract /* synthetic */ “vertical class merging” holder produced by R8, not MICO code. It merges static helpers from Crakle, Moloco, Mintegral (MBConfiguration), Fyber, Ktor (ConnectTimeoutException), Media3, Amplitude, DataStore, Firebase and com.eyecon.global.AfterCall.SplashInterstitialActivity into ~120 static methods (m0A(List,String,Object...), m12M(HttpURLConnection, …), m17a(Context, …), m22f, m32p(), …). It contains no contact, reminder or PII logic. No security finding.

15. Package t6 (disk p555t6, 17 files, 48 KB) — ad banner + grid decoration helpersh4

[OBS] t6.i extends AdListener (ad lifecycle), t6.h (the interface q6.i0 implements), t6.m ViewPager.OnPageChangeListener, t6.o ValueAnimator.AnimatorUpdateListener, t6.p extends p065xx.AbstractC16507i, t6.q, t6.r extends LinearLayout, and three matched pairs of ItemDecoration + SpanSizeLookup (b/c, d/e, f/g) for 2/3/4-column grids. t6.k/l/m/n are runnables; t6.l is an interface. No PII.

16. Package q5 (disk p492q5, 9 files, 46 KB) — Contact-reminder scheduler (flow 6)h4

realpurpose
q5.a (C19572a)ReminderData record: callTime(long), reminderTime(long), phoneNumber(String), name, socialName, isAnswered(boolean), setTime(String), reminderMessageText, reminderType(int), amountOfShown(int). m36034a() serialises to Gson JSON with keys callTime, reminderTime, phoneNumber, name, socialName, isAnswered, amountOfShown, reminderMessageText, reminderType
q5.g (AbstractC19578g)the scheduler — see §16.1
q5.k (C19582k extends c7.j)the schedule dialog, layout R.layout.dialog_contact_reminder. Widget ids: EBSetTimeV, ETContentSms (CustomEditText for the SMS reply text), FLReplyMsgContainer (RoundedCornersFrameLayout), LLTimeOption, call / sms / whatsapp (EyeIconButton channel pickers), noteReminder, chooseAppContainer, chooseTimeContainer, eyeButtonClock1..4 (the four preset delays), EB_main_button, FL_inner_view, TV_title, bigIcon. Title = R.string.schedule + " " + <type label>; validation toasts R.string.cant_set_reminder, R.string.save_reminder; the date/time picker is R.layout.layout_date_time_dialog
q5.c, q5.e, q5.hclick listeners; q5.h builds layout_date_time_dialog, prefixes R.string.today + ", "
q5.bSMS-permission rationale inside ContactReminderActivity (R.string.permissions_needed, R.string.permission_app_setting_message, R.string.go_to_settings, R.string.cancel)
q5.d”write a message” pre-prompt (R.string.write_a_message, R.string.messages, R.string.sms_permission_pre_prompt_msg, R.string.continue_, R.string.message_failed)
q5.fcompletion runnable
16.1 q5.g — scheduling algorithm [OBS]h5

static void m36035a(int eventType, String cli, String name, String src) — auto-scheduling on call end:

if (!AbstractC19327e.m35661t()) return; // user/consent gate
if (eventType==6 || eventType==7) // missed-call variants
&& !remote("shouldShowWhatsappMissedCallReminder")) return;
String delays = remote("missedCallReminderDelayInMinutes", false); // CSV of minutes
if (eventType == 8) { // un-answered outgoing call
if (!remote("shouldShowUnAnswerCallReminder")) return;
delays = remote("outgoingCallReminderDelayInMinutes", false);
label = "unanswer_call";
} else label = "MissedCall";
for (int minutes : parseCsv(delays))
m36036b(new q5.a(elapsedRealtime(), minutes, cli, name, false, "", eventType, 0, src), label, false);

static void m36036b(q5.a data, String src, boolean analytics) — persistence + work enqueue:

if (analytics) b5.f("Scheduler_Set") { type=<type>, setTime=<setTime>, src=<src> }
JSONArray list = parse(pref "sp_call_reminder_list"); // on JsonSyntaxException -> overwrite pref with "[]"
list.add(data.toJson());
pref.putString("sp_call_reminder_list", list.toString()); // *** plaintext PII in prefs ***
Data d = new Data.Builder()
.putLong("callTime", now)
.putString("type", typeName(data.reminderType))
.putLong("reminderDelayTime", data.reminderTime) // minutes
.putString(HintConstants.AUTOFILL_HINT_PHONE_NUMBER, l7.b.m34444e(data.phoneNumber)) // normalised
.putString("src", src).build();
WorkManager.enqueue(new OneTimeWorkRequest.Builder(ContactReminderWorker.class)
.setInputData(d).setInitialDelay(minutes, TimeUnit.MINUTES)
.addTag("CallReminderWorker" + phoneNumber + type).build());

So N reminders can be scheduled per call (the CSV can contain several delays), the pending list is a plain JSON array in SharedPreferences, and the worker tag embeds the normalised phone number.

static void m36037c(Activity, String cli, String name, String src) — the simple “Set reminder” sheet (analytics open_scheduler), invoked from r5.i0.m36652h case ordinal 45 (the CALL_REMINDER social type) with src = "menifa". static void m36038d(Activity, int type, String cli, String name, int shownCount, String msg, String tag, q5.f done) — generic typed scheduler used by the missed-call / WhatsApp-reminder flows.

Remote-config keys that drive reminders [OBS]: shouldShowWhatsappMissedCallReminder, shouldShowUnAnswerCallReminder, missedCallReminderDelayInMinutes, outgoingCallReminderDelayInMinutes, enableReminderForMissCall, isReminderButtonInMenifa, showAdsInContactReminder, showAdsInMissedCallReminder, showAdsInContactReminderLockScreen, showAdsInMissedCallReminderLockScreen, missedCallReminderDelayInMinutes, report_spam_card_hours_limit, share_spam_card_shown_count, plus the ad-unit keys reminderAdmobNativeAdUnitId, reminderGoogleCombineUnitId, reminderAdmobBannerAdUnitId, facebook_ads_reminder_native, facebook_ads_reminder_banner, mobitech_reminder_ad, showAdsInUnAnswerCall, showAdsInUnAnswerCallLockScreen, unAnswerCallGoogleCombineUnitId, unAnswerCallAdmobNativeAdUnitId, unAnswerCallAdmobBannerAdUnitId, shouldShowUnAnswerCallReminder, unAnswerCallReminderDelayInMinutes.

17. Package p5 (disk p474p5, 5 files, 33 KB) — cantalk.jsp clienth4

[OBS]

  • p5.a (AsyncTaskC19296a extends AsyncTask) doInBackground:
    • GET https://api.eyecon-app.com/app/cantalk.jsp?recvcli=<number>&message=<msg>&apsVersion=vc_749_vn_4.2026.04.23.1145_a, logs "AsyncTaskCalls", "msg = %s";
    • GET <cantalk.jsp or getcantalkstatus.jsp>?sendercli=<number>&apsVersion=….
  • p5.e (RunnableC19300e) is the same logic synchronously with w6.C21273a.m38423g(...), using recvcli + message (CampaignEx.JSON_KEY_AD_Q = "adq") for cantalk.jsp, and notifiedcli for getcantalkstatus.jsp.
  • p5.b e2.b callback; p5.c, p5.d runnables (p5.d has 12 cases — the can-talk state machine).

Security note [OBS]: both endpoints are GET and carry plaintext phone numbers and the SMS body in the query string, so the message text and numbers land in any proxy/server logs along the path and in the app’s own HTTP cache.

18. Package g6 (disk p304g6, 5 files, 29 KB) — legacy ViewPager plumbingh4

[OBS] g6.e extends PagerAdapter inflates R.layout.fragment_notification_reader and swaps between the notification-reader and the reverse-lookup screen (R.string.reverse_lookup_title); g6.b ViewPager.OnPageChangeListener; g6.c async callback showing R.string.unlock_your_id_plus / R.string.your_trial_has_expired (R.layout.feature_unlock_options); g6.d extends q7.AbstractC16794a sets R.string.lookup_for; g6.a runnable.

19. Small packages (rk, z6, t7, d7, r6, a8, kq, s6)h4

  • rk (3 files) — R8 merge artifacts, not MICO logic. rk.b is ZXing EncodeHintType (TERMINATOR, NUMERIC{10,12,14}, ALPHANUMERIC{9,11,13}, STRUCTURED_APPEND, BYTE{8,16,16}, ECI, KANJI, FNC1_*, HANZI); rk.a merges three unrelated enums ("ADD_NAME","CONFIRM_NAME","REPORT_SPAM","CONFIRM_CONTACT_NAME","THANK_YOU","AI_ASSISTANCE", "NOT_SUPPORTED/READY/ON_HOLD/…", "INITIALIZE/RESOURCE_CACHE/DATA_CACHE/SOURCE/ENCODE/FINISHED"), a TextInput-type switch, Crakle ad-string assembly, Color.rgb map building, and m36780r(String prefKey, boolean, e2.b) = a SharedPreferences boolean setter (used by r5.r case 17 for SP_KEY_BLOCK_SPAM_CALLS).
  • z6 (5 files, Contacts) — z6.a = Note (implements Comparable; fields a,b,c; the per-phone-number note attached to r5.j.note); z6.e = the note dialog (R.layout.note_dialog_layout, R.layout.surveys_base_layout, R.string.notes, shares via Intent.createChooser(…, R.string.note); empty-note guard R.string.note_is_empty); z6.c = its click listener; z6.d = interface; z6.f = singleton.
  • t7 (2 files) — t7.b extends d7.d is the Reverse Lookup fragment (super(R.layout.fragment_reverse_lookup)); t7.a async callback.
  • d7 (4 files) — base Fragment for the contact screens: d7.b (AbstractC15858b) extends Fragment implements i71.n0 with static final HashSet f38727h and the fragment lifecycle contract (f0,i0,j0,k0,l0,m0,n0,o0,p0,q0); d7.d (AbstractC15860d) adds i,m0,o0,r0,s0; d7.c = OnBackPressedCallback; d7.a = a custom RuntimeException.
  • r6 (2 files) — r6.a extends ItemTouchHelper.Callback (swipe-to-delete / drag-reorder for the contact lists) + r6.b interface.
  • a8 (7 files) — a8.c holds static final ViewModelStore f390a + ViewModelProvider.NewInstanceFactory f391b; a8.b, a8.e, a8.g are plain ViewModels (state holders for the contact screens); a8.a holds three C19290a comparators (ids 8,9,10); a8.d, a8.f small helpers.
  • kq (2 files, Contacts) — kq.a interface + kq.b abstract base with static final LinkedList f45607a.
  • s6 (3 files) — s6.a extends o6.C16146a, s6.b, s6.c interface. Trivial UI glue.

20. Bundled third-party SDK internals (o0, am)h4

20.1 o0 (disk p444o0, 111 files, 391 KB) — Amplitude SessionReplayh5

[OBS] o0.c is a Kotlin Continuation that emits "Opting session(" + sessionId + ") out of recording due to captureEnabled config.", "… due to optOut config.", "… due to sample rate.", and calls SessionReplay.captureSnapshot(). o0.c2 maps CSS properties and fetches remote masking assets over HTTPS from https://static.amplitude.com/onenav/static/img/… (image-not-supported.svg, image-icon.svg). o0.i2 logs a warning if ACCESS_NETWORK_STATE is missing. The remaining ~100 classes are the obfuscated Amplitude session-replay obfuscation/serialisation layer. Finding: Amplitude SessionReplay records and replays app screens. Whether it is enabled is governed by remote flag is_amplitude_enabled (§7.1). The feature replays the contact list, contact-detail screens and the menifa sheets, i.e. contact names, numbers and photos are captured by a third-party session-replay SDK when enabled. [OBS] for the flag’s existence and the code path; [INF] for actual runtime enablement.

20.2 am (disk am, 76 files, 187 KB) — Firebase Messaging/GMS + the contact-photo decryptorh5

[OBS] Mixed bag:

  • am.v1 (ExecutorC0221v1), am.u1, am.u, am.t1 (other cases), am.r, am.r1 — Firebase Messaging helpers; am.t1 default branch builds new ThreadPoolExecutor(0,1,30,SECONDS,new LinkedBlockingQueue(), new NamedThreadFactory("firebase-iid-executor")).
  • am.t1 case 1 — contact-photo decryptor (see §7.2).
  • am.b1, am.f0, am.h0, am.k0, am.l, am.m1, am.o0, am.o1, am.q1, am.y0 — interfaces (RxJava-like io.reactivex.internal contracts that the obfuscator merged here).
  • am.e/e0/e1/f/f0/f1/g/g0/g1/h/h0/h1/i/i0/i1/j/j0/j1/k/k0/k1/l0/l1/m/m0/m1/n/n0/n1/o/o1/p/p1/q/q0/q1/r/r0/r1/s/s0/s1/t/t0/u/u0/u1/v/v0/w/w0/w1/x/x1/y/y0/z/z0/z1 — the RxJava2 io.reactivex.internal.* operator/scheduler implementations (R8 class-merged here). These are third-party, not MICO logic.

21. Flow tracesh4

Legend: [OBS] = visible in the decompiled source of this cluster. [OUT] = the step lives in a package outside C4 (named, with file+line where useful) so it is cited, not proved from C4.

21.1 Flow 1 — ContactsProvider read -> normalisation -> DB insert/updateh5
#StepCode
1Trigger A — external mutation. A ContentObserver on the contacts provider fires.r5.g (C19858g extends ContentObserver). onChange(boolean) -> C16945i.m32774f(h7.f.m32765b(), 0, RunnableC18504r(C18505s.f46714c, C18716e(13, this), 0)). [OBS] r5.g.f51163b is the “already handling” flag.
2Trigger B — full refresh. AddressBook executor r5.e.f51135b (1 thread, name "AddressBook") runs the read.r5.e.m36603x(Object[] out, boolean starOnly, boolean top100, Runnable done, int retry) [OBS]
3Projection built (16-17 columns; data1 appears three times and data5 once).data1,data2,data3,contact_id,raw_contact_id,data1,data5,data1,photo_uri,photo_thumb_uri,lookup,starred,pinned,display_name,is_super_primary,account_type,[account_name],mimetype. account_name only when pref SP_EYECON_DEF_ACCOUNT_V2 is empty. [OBS]
4Query. ContentResolver.query(ContactsContract.CommonDataKinds.Phone.CONTENT_URI, proj, null, null, starOnly && top100 ? "starred DESC, pinned ASC, contact_last_updated_timestamp DESC LIMIT 100" : null). [OBS]r5.e:1477
5Social extraction first (before phone normalisation), because they ride the same cursor.mimetype=="vnd.android.cursor.item/com.skype.android.chat.action" and data5==3 -> m36599t() builds a social row social_type=SKYPE(4111), social_id=data1, contact_id, social_name="0", social_source=0. mimetype=="vnd.android.cursor.item/email_v2" -> m36595p() builds social_type=EMAIL(4128), social_id=data1, contact_id, social_name="0", social_source=0. [OBS]
6Normalisation. C18315e.m34460c(String raw) (disk p394l5, out of cluster) -> C18311a { f46071b = normalised/server CLI, f46072c = digits-only CLI }. Every later read of the same number goes through l7.b.m34444e(String) (p399l7\C18312b), which is the same normaliser used by r5.j.m36671a() and r5.j.m36673e(). [OBS] that l7.b.m34444e produces the cli_in_server value and that PrivateNumber… prefixed values mark un-dialable numbers.
7aMobile path (f46071b non-empty). De-dup by HashSet per contact_id: the first occurrence of a given cli_in_server for a person wins.row: name = display_name, cli_in_server = f46071b, cli = f46072c. [OBS]
7bNon-mobile path (f46071b empty). Row parked in arrayList with raw cli = data1, name = display_name; a parallel single-thread executor (p6/RunnableC17219d1) fills map3 : cli -> C18311a. After the cursor is exhausted the same HashSet dedup runs and name falls back to the digits-only CLI if display_name was null. [OBS]
8Language detection. Per-row the dominant Locale of the contact names is tallied (m36577E); the winner becomes f51134a = locale.getLanguage().toLowerCase().r5.e.m36577E; tail writes SP_KEY_TOP_CONTACTS_LANGUAGE = f51134a and SP_KEY_AMOUNT_OF_CONTACTS = contacts.size(). [OBS]
9Address-book writes back into contactdb. Out-params are set and the caller stores them.out[0] = C16491s contacts, out[1] = C16491s socials, then runnable.run(). [OBS]
10Storage into DBContacts.Executed by r5.k case 11: dBContacts.f11729g = (C16491s) obj[0] (contacts table cache), dBContacts.f11730h = (C16491s) obj[1] (social table cache), then dBContacts.m10144t(((am.u1)…).f811c) which performs the actual INSERT/UPDATE. [OBS] up to the m10144t call; [OUT] for its body (in p020ad-style package).
11Write-back to the device address book (optional, user-driven).r5.e.m36586g(ContactInfo) builds a ContentProviderOperation batch: newInsert(RawContacts.CONTENT_URI) + newInsert(Data.CONTENT_URI) with mimetype=vnd.android.cursor.item/name and mimetype=vnd.android.cursor.item/phone_v2, then applyBatch(ContactsContract.AUTHORITY). r5.t.m36682b(Activity, contactId) / m36683c launch ACTION_EDIT / ACTION_VIEW on ContactsContract.Contacts.CONTENT_URI/<id>. [OBS]
12Retry. If moveToFirst() or moveToNext() returns -1 while retry == 0, the cursor is closed and the whole read is re-entered with retry = 1 (one extra full pass). [OBS]r5.e:1511-1518, 1616-1621
13Table set touched (proved by the wipe in r5.r case 4).history, contacts, social, fresh_pics, block_list, recorded_notes, obtained from a7.C0041b.m156t().m160D(500L). [OBS]
21.2 Flow 2 — batching, addcontacts.jsp, and the server delta algorithmh5

Part A — the uploader (out of cluster, but the exact contract is quoted because C4 consumes it).

w6.h.m38428a(String contactsBlob, boolean init) [OUT, p620w6\C21280h.java:112-179]:

if (contactsBlob.trim().isEmpty()) return; // log "Server/addContactsList empty addedList"
if (!isEyeconUser()) return;
params = [ ("contacts", blob),
("cli_cc", TelephonyManager.getSimCountryIso().toLowerCase()),
("init", init ? "1" : "0"),
("possible_langs", AbstractC0062l0.m356k1().toLowerCase()),
("apsVersion", "vc_749_vn_4.2026.04.23.1145_a") ]
POST https://api.eyecon-app.com/app/addcontacts.jsp
...
int batches = blob.split("@z@").length; // <-- "@z@" is the RECORD separator
int total = DBContacts.J.f11724b.size();
if ( pref.getBoolean("contactLangNeedUpdate", false) // (a)
|| batches > 20 // (b)
|| (total > 0 && (batches * 100) / total > 5) ) { // (c)
pref.putBoolean("contactLangNeedUpdate", true);
GET https://api.eyecon-app.com/app/getcontactslang.jsp?apsVersion=vc_749_vn_4.2026.04.23.1145_a
if (httpCode == 200 && body.matches("[a-zA-Z]{2}")) {
pref.putBoolean("contactLangNeedUpdate", false);
pref.putString("contactLang", body);
f7.j0.f40914f.m32051f(); // re-detect locale
if (!f40914f.f40917c.equalsIgnoreCase(body) // language actually changed
&& pref.getString("","SP_KEY_SELECTED_QUOTES_LANGUAGE_v1").isEmpty()) {
C16945i.m32774f(f40914f.f40915a, 0, RunnableC0032d(…, "server", "", RunnableC21275c(5)));
}
}
}

=> The briefing’s addcontacts.jsp field list, apsVersion literal and the (contactLangNeedUpdate) || batches > 20 || (batches*100)/total > 5 heuristic are all CORRECT. @z@ is confirmed as the record separator.

Part B — the removal uploader (in cluster). s5.s case 1 — full source in §0.1 / §5.2. Record = <cli_in_server>@a@<name>@z@. So the field separator inside a record is @a@ and the record separator is @z@; removecontacts.jsp takes only two parameters (contacts, apsVersion) — no cli_cc, no init, no possible_langs.

Part C — the server-delta application (in cluster). r5.k case 12 — full algorithm in §2.7. Summary of the delta semantics:

  1. contacts delta rows are not allowed to overwrite the user’s local data — only is_viewer, os_type, is_sub, is_can_talk_enable, vn are copied down onto every phone row of the same person (is_viewer = “this device is the viewer”, vn = viewer’s version).
  2. social delta rows are authoritative: the server’s social_id replaces the local one and social_source is set to 1 (from server).
  3. Every number of that person that lacks a social row gets a synthetic social row with social_score = 0, social_source = 1.
  4. A FACEBOOK(1) social match implicitly creates a FB_MESSENGER(4103) match as well.
  5. Commit order: DBContacts.m10099U(null,null,socialRows) -> m10092D() reload -> DBContacts.m10093O(null,null,contactRows) -> m10118v() reload -> m10135X("getUpdatedContactInfo_") -> m10134W() -> m10123E().
  6. Trigger [OBS]: r5.k case 8 — when pref SP_REQUIRED_SERVER_CONTACTS_REFRESH is true and f11746x is false, a forced refresh is scheduled on the w6.h Server executor (RunnableC21082e0(25, s5.e(2, db, boolean[]), boolean[])) and the pref is cleared on completion.

Part D — related in-cluster producers.

  • bm.RunnableC3686q0 logs "Server", "addContactsList size = %s, addedList = %s" (the blob builder). [OUT]
  • s5.g case 0 / s5.k push the per-contact name and photo fetch.
  • r5.i0.m36652h on any social tap writes social_source-worthy data via C16945i.m32774f(DBContacts.f11708K, 0, bm.RunnableC3589a(db, contact, socialInfo, cli, 18)).
  • r5.r case 11 posts a signature blob {"signatures":[…],"cid":…,"random":…}, RSA-encrypted with R.string.pk_enc_2, to https://api.eyecon-app.com/app/grps/sgn (anti-fraud / attestation).
21.3 Flow 3 — contact photo: acquisition, caching, approval, encryption/decryptionh5

A. Local (device) photo [OBS] — r5.e.m36594o(int size, String contactId, String storagePhotoPath):

1. size == -1 -> query Phone.CONTENT_URI { photo_uri, photo_thumb_uri } where contact_id = ?
2. m36602w(photoUri, thumbUri) -> pick full vs thumbnail vs none (return codes 0..4)
3. m36605z(contactId) -> m36601v(contactId) -> m36604y(uri)
m36604y: openAssetFileDescriptor(uri + "/display_photo", "r")
InputStream via m36574A(resolver, uri, afd) // falls back to projection {"data15"}
BitmapFactory.decodeStream(stream)
m36605z: on failure, retry with ContentUris.withAppendedId(Contacts.CONTENT_URI, id)
4. STILL NULL and storagePhotoPath non-empty ->
m7.d c = C18517d.f46764e; // singleton
C16945i.m32773e(c.f46766b, new am.RunnableC0215t1(1, System.currentTimeMillis(), c, storagePhotoPath, Bitmap[1]));
bitmap = bitmapArr[0];

B. Encrypted on-disk cache decrypt [OBS] — am.RunnableC0215t1.run() case 1 (p516r5’s r5.i calls AbstractC18543s.m34752m(int) for icon decode, which is a different class, but the decrypt routine is am.t1):

if (m7.d.f46767c && hasPermission(READ_EXTERNAL_STORAGE)) {
File f = new File(storagePhotoPath);
if (f.exists()) {
int len = (int) f.length();
if (len > m7.d.f46768d.length) m7.d.f46768d = new byte[len]; // grow-only scratch buffer
FileInputStream fis = new FileInputStream(f); fis.read(buffer,0,len);
byte[] clear = (byte[]) new ObjectInputStream(
new CipherInputStream(new ByteArrayInputStream(buffer,0,len),
m7.d.f46765a))
.readObject(); // <-- DESERIALISATION SINK
Bitmap[] out = (Bitmap[]) captured;
if (clear != null && clear.length > 0) out[0] = BitmapFactory.decodeByteArray(clear,0,clear.length,null);
}
}

C. Key derivation — proof of the weak-crypto claim [OBS], p379k5\RunnableC17750b.run() case 10:

C18517d.m34710a(MyApplication); // m7.d static init
String prefix = MyApplication.getString(R.string.cipher_key); // res/values/strings.xml -> "Ewu"
String rand = pref.getString("", "cipher_key");
if (isEmpty(rand)) { rand = String.valueOf(new Random().nextInt(RoomDatabase.MAX_BIND_PARAMETER_CNT)); // nextInt(999)
pref.putString("cipher_key", rand); } // stored in PLAINTEXT prefs
SecretKeySpec k = new SecretKeySpec((prefix + rand + "sTQHGK").getBytes(), "Blowfish"); // "Ewu" + <0..998> + "sTQHGK"
Cipher.getInstance("Blowfish").init(ENCRYPT_MODE, k);
m7.d.f46765a = Cipher.getInstance("Blowfish"); // <-- NO mode, NO padding => Blowfish/ECB/PKCS5Padding
m7.d.f46765a.init(DECRYPT_MODE, k);
m7.d.f46767c = ok; // "cipher enabled" flag

Therefore: keyspace = 999 distinct keys, algorithm Blowfish = Blowfish/ECB/PKCS5Padding (ECB, no IV, deterministic), and the only secret component (0..998) is stored unencrypted in the app’s own SharedPreferences. The briefing said “1000-value keyspace”; the precise figure is 999 (nextInt(999)), otherwise identical.

D. Server photo acquisition / caching / approval (out of cluster). Cited [OUT]: https://api.eyecon-app.com/app/pic?cli=<cis>&is_callerid=true&size=<small|big|null>&type=0&src=<n>&cancelfresh=<0|1>&cv=vc_749_vn_4.2026.04.23.1145_a (p620w6\C21280h.m38434g, lines 317-339 — the briefing’s URL is exactly right), plus uploadpic.jsp (w6.h.f56468d), getfreshpicsjson.jsp, getoptionalpics.jsp (p607vk\RunnableC21082e0:242), picapproval.jsp (p572u4\RunnableC20597o:147-151, GET with params). In-cluster touch points: r5.w = FreshPic record ({cisA, cisB, tagA, tagB, type, size, timeMs, urlBig, urlSmall, Bitmap, ContactInfo, waiting}); s5.j case 10 = giveContactsFreshPicUrls (the fan-out that pushes downloaded URLs onto contacts, with the “skip, has large photo” / “type not in range” guards); s5.g = per-contact assignment; b5.a case 2 = registration of storage_photo_path in m7.AbstractC18515c.f46750b; r5.i.m36621B = setter that fires b5.a; r5.e.m36606D = the async variant. AbstractC0039a.f177O1 = "SP_TELL_SERVER_CONTACT_HAS_NO_PHOTO" — a flag that lets the app tell the server “this contact still has no photo”.

21.4 Flow 4 — number -> display name, social lookup, Facebook-ID pathh5

[OBS]

  1. Local name first. r5.i.private_name (device display_name) is the default; if it is empty or equals the raw number, l7.b.m34437h().m34440a(private_name) is used, else R.string.private_number. r5.i.m36643y() decides “is this a real device contact?” (contact_id empty or starts with "-1" => unknown number).
  2. T9 / unicode search keys. r5.i.m36638t(char[][] keypad) memoises the T9 digit string of private_name (space -> 0); DB_COLUMN_UNICODE_SCORE / unicodeScore support non-Latin names.
  3. Server name (caller ID). Out of cluster: getnames.jsp (p599v7\RunnableC20908f:372-382). In-cluster the result lands on r5.i.private_name with hasNameInServer = true (AbstractC0039a.f189U / f221g area) and on r5.i.shouldFetchName.
  4. Social lookup table. Every social id is one row of the social table (table id 12) with the columns contact_id (a7.f210d), cli_in_server (f216f), social_type (f164K0 = r5.h0.f51206b numeric id), social_id (f167L0), social_score (f170M0), social_last_change (f173N0), social_name (f176O0), social_source (f179P0, 0 = from device, 1 = from server), server_id (f135A1), rowid (f203a1). r5.i.m36637s(SocialType) looks one up by enum.
  5. Ingest rules. r5.i0.m36647c(ArrayList, SocialInfo) — reject kind==32 (Skype), reject when the app isn’t installed unless the type is in {FACEBOOK, INSTAGRAM, VKONTAKTE, TWITTER, LINKEDIN}, reject empty socialID; merge duplicates by summing score and keeping the first non-empty socialID. Device-sourced seeds: r5.e.m36595p (EMAIL, from email_v2 data rows) and r5.e.m36599t (SKYPE, from com.skype.android.chat.action with data5==3).
  6. Server-sourced seeds: r5.k case 12 (§21.2 Part C) — authoritative social_id, and the implicit Facebook -> Messenger duplication.
  7. Facebook-ID path (r5.i0.m36651g + m36649e + m36654j, source quoted in §2.5). Remote gate isTestingFbProfileLinkEnableV2 selects the new probe path; the old path calls m36654j directly. Probe support: w6.h.f56471g (a HashSet of already-tried profile names — a hit produces NOT_A_FB_PROFILE_ERROR) and the Server executor w6.h.f56472a. w6.h.m38436i(name, type, attributes) builds {"name":…,"type":…,"attributes":{…},"timestamp":<epoch-seconds>} and posts it (that is the notifyaddcontact.jsp payload). Also shouldSearchForTwoNamesInFacebook (remote, §7.1) makes the search try first+last name, and facebooke_Extract_URL_Regex = browser_native_sd_url":"([^"]+)" scrapes the resulting page.
  8. Launch (all social types) — r5.i0.m36652h(ContactInfo, SocialInfo, PhoneNumberInfo, Activity), a 38-case switch on socialEnum.ordinal(), mapping to: QQ, Skype, Telegram, Twitter (twitter://user?screen_name= -> https://mobile.twitter.com/<id>), Viber (vnd.android.cursor.item/vnd.com.viber.voip.viber_number_message pinned to com.viber.voip), WhatsApp, SMS, Facebook, LinkedIn, VK, Instagram, Email, Calendar, Navigation, Note, Share-location, Video call, Can-talk, Call reminder, Report spam, Add contact.
  9. Naming helpers. r5.h.m36613a(String) splits participant strings; r5.i.m36639u() builds the group display text from r5.h participants, falling back to their cli, then to private_name.

[OBS]

  • MainActivity (wrapper) hosts MainFragment, which owns f12024j (Map<Class, Fragment> of registered q6.j children), f12025k (the ViewPager whose getCurrentItem() is the tab index read by q6.j.mo10082M()), f12027m (the search EditText).
  • Tabs = q6.w = HISTORY(0) / FOR_YOU(1) / FAVORITES(2). FOR_YOU and FAVORITES share R.layout.fragment_for_you_and_favorites_layout; HISTORY uses R.layout.fragment_history_layout. Default start page = remote com_start_page = 1. Default list style = remote com_history_default_style = 7 (LIST_MAIN_CARD_VIEW).
  • Fragment contract: q6.j extends d7.b (Fragment) implements q6.d1, q6.r. d7.b supplies the lifecycle hooks (f0,i0,j0,k0,l0,m0,n0,o0,p0,q0) plus static final HashSet f38727h. Tab-specific subclasses: HistoryFragment, ForYouFragment, and the favourites fragment (all thin wrappers in com.eyecon.global.MainScreen.Communication.*).
  • Adapters: q6.v is the universal contact adapter (list cell R.layout.eye_contact_list_cell, grid cell R.layout.eye_contact_grid_cell, R.layout.empty_view, R.layout.eye_title_separetor). q6.i0 = list row holder, q6.z = grid row holder, q6.n + q6.m = call-log rows, q6.o0 = shimmer placeholders, q6.s0 = 3-button rows. r5.p + r5.o = the separate A/B grid (ab_fav_pinned_position, ab_photo_type, ab_photo_uri), decorated by r5.l (67 dp / 10 dp).
  • Layout managers / decorations chosen by q6.j.m36060r0 from the q6.d0 cell enum (1-column list / 2- / 3- / 6-column grids, with q6.e/g/i/z0 span-size lookups giving ad and section-header rows full width, and q6.d/f/h/x0/y0 decorations).
  • Diffing: q6.v.m36079j(RecyclerView, ArrayList) compares old vs new, and when the sizes match issues per-position notifyItemChanged only for the visible window (m36071a..m36072b), then re-anchors scroll.
  • Search: q6.j.mo10006k0(Bundle) snapshots MainFragment.f12027m.getText() into f50395i (trimmed, m31995E) and m36062w0() = f50395i.length() > 0. q6.j.mo10247x0(String, ArrayList) / mo10282Z(String) are the search entry points (query + result list). EyeSearchActivity (wrapper) hosts r5.b (the sync-account picker) and r5.a (the account model); r5.i0.m36653i/m36654j do a name->Facebook search from there.
  • Long-press / selection: r5.t.m36684d (link/merge contacts confirm), r5.n (selection state: Activity + HashSet of selected ContactInfo + AtomicInteger counter + predicate r5.m), r6.a (ItemTouchHelper.Callback — swipe / drag).
  • Cell actions: r5.t.m36681a builds the bottom sheet; u6.a/u6.u render it; c6.o is the menifa (SocialManager) sheet; c6.d registers EYECON.INTENT_ACTION_REFRESH_DA_UI so any sheet refreshes on demand.
  • Ads in the list: q6.a1/q6.b (scroll listener) + q6.e/g/i/z0 (full-span lookups) + t6.* decorations; remote flags native_ad_dynamic_area, dynamic_area_url, reminderAdmob*, facebook_ads_reminder_*, cards_update_threshold.
  • Legacy pager: g6.e extends PagerAdapter for the notification-reader / reverse-lookup screens; q6.x extends FragmentStateAdapter for the ViewPager2 version; c6.g page-change callback.
21.6 Flow 6 — contact-reminder scheduling and notificationsh5

[OBS]

  1. Entry points.
    • Automatic: q5.g.m36035a(eventType, cli, name, src) is called from the post-call pipeline (the same family that r5.r case 9/10 hooks). eventType 6/7 = missed call, 8 = un-answered outgoing.
    • Manual: q5.g.m36037c(Activity, cli, name, "menifa") from the CALL_REMINDER entry in the menifa sheet (r5.i0.m36652h, ordinal 45); r5.t.m36681a also offers “Schedule call or message” (r5.q case 5, R.string.schedule_call_or_message).
    • Typed: q5.g.m36038d(Activity, type, cli, name, shownCount, msg, tag, done) from the missed-call / WhatsApp / spam-card flows.
  2. Gating — AbstractC19327e.m35661t() (master enable), then remote flags shouldShowWhatsappMissedCallReminder, shouldShowUnAnswerCallReminder.
  3. Delay selection — remote missedCallReminderDelayInMinutes or outgoingCallReminderDelayInMinutes, parsed as a comma-separated list; one WorkManager job per entry, so a single missed call can arm several reminders.
  4. Persistence — the ReminderData is appended to the SharedPreferences array sp_call_reminder_list (plaintext JSON; keys callTime, reminderTime, phoneNumber, name, socialName, isAnswered, amountOfShown, reminderMessageText, reminderType). On a JsonParseException the code logs "Corrupted JSON in SP_CALL_REMINDER_LIST, resetting" and overwrites the whole list with [] — i.e. any malformed entry silently discards every pending reminder.
  5. Scheduling — OneTimeWorkRequest(ContactReminderWorker) with setInitialDelay(minutes, MINUTES), Data\{callTime, type, reminderDelayTime, phoneNumber = l7.b.m34444e(cli), src\}, addTag("CallReminderWorker" + phoneNumber + type).
  6. UI — q5.k (layout dialog_contact_reminder): channel picker (call / sms / whatsapp / noteReminder EyeIconButtons), ETContentSms for the pre-filled reply SMS (FLReplyMsgContainer, LLTimeOption), and four preset delay buttons eyeButtonClock1..4 plus EBSetTimeV -> R.layout.layout_date_time_dialog (date prefixed with R.string.today + ", "). Title = "Schedule " + <type label>; toasts R.string.cant_set_reminder / R.string.save_reminder.
  7. Worker — com.eyecon.global.ContactReminder.ContactReminderWorker (thin wrapper; the real doWork is outside C4). It reads the Data keys above and is expected to show the reminder notification / fire the channel action. [INF] on the notification content, because ContactReminderWorker’s body is not in this cluster.
  8. Notification controls (remote) — showAdsInContactReminder, showAdsInMissedCallReminder, showAdsInContactReminderLockScreen, showAdsInMissedCallReminderLockScreen, showAdsInUnAnswerCall, showAdsInUnAnswerCallLockScreen, report_spam_card_hours_limit, share_spam_card_shown_count, missedCallReminderDelayInMinutes, outgoingCallReminderDelayInMinutes, lockScreenTimerDelay, ShouldTurnedOnLockScreenTimer, snoozeAutoSec, permissionNotificationHourOfDay, must_permission_notification_interval_hours, numOfShownDialogSchedulerNightMessage. Ad units for the reminder surface: reminderAdmobNativeAdUnitId, reminderGoogleCombineUnitId, reminderAdmobBannerAdUnitId, facebook_ads_reminder_native, facebook_ads_reminder_banner, mobitech_reminder_ad, unAnswerCall*AdUnitId, lock_screen_no_video_ad_unit_id.
  9. Permission UX — q5.b (notification permission rationale inside ContactReminderActivity: R.string.permissions_needed / permission_app_setting_message / go_to_settings) and q5.d (SMS permission pre-prompt).

22. SharedPreferences keys observed in cluster C4 [OBS]h4

keytypewritten by / meaning
SP_KEY_TOP_CONTACTS_LANGUAGEStringr5.e.m36603x — detected ISO-639-1 language of the address book
SP_KEY_AMOUNT_OF_CONTACTSintr5.e.m36603x — count of imported contacts
SP_EYECON_DEF_ACCOUNT_V2Stringr5.e.m36603x — user-selected default sync account
sp_social_installed_apps_v2String (JSON array)r5.i0.m36645a — cached set of installed social-app numeric ids
permission_status_jsonString (JSON)j7.m.m33637c — full map of granted/denied permissions incl. DIALER and CALL_SCREENING roles
cipher_keyStringthe Blowfish key component, nextInt(999), stored in PLAINTEXT (p379k5 case 10)
sp_call_reminder_listString (JSON array)q5.g.m36036b — pending reminders incl. phoneNumber + name
SP_KEY_EYECON2_REMINDERSString (JSON array)r5.k case 20
SP_REQUIRED_SERVER_CONTACTS_REFRESHbooleanr5.k case 8 — forces a server contact refresh
SP_KEY_IS_GIVE_PRE_EYECON_USAGEbooleanr5.k case 8 — one-shot flag for pre_eyecon_usage distribution
SP_KEY_DID_UPDATE_V1453booleanr5.r case 11 — anti-fraud signature already sent
SP_KEY_BLOCK_SPAM_CALLS / SPAM_CALLbooleanr5.r cases 16/17, rk.a.m36780r
SP_KEY_RECORD_CALLS_MODEintr5.r case 10
SP_KEY_PRE_EYECON_AMOUNT_OF_PHOTOSintb5.h — photo-acquisition telemetry baseline
AMOUNT_OF_UI_CONTACTSintb5.f — fallback contact count for telemetry
SP_KEY_FAKE_AUTHENTICATED_CLIStringused by the “fake authenticated CLI” test path
SP_KEY_FID__LIST__FOR_NOT_SHOW_DIALOGStringsuppression list for permission dialogs
SP_KEY_REG_PERMISSIONS_SUMMERYStringregistration permission summary
SP_KEY_DONT_ASK_ME_AGAIN_TRACKERStringdo-not-ask-again tracker
SP_MAIN_VIEW_SORTING_MODEintmain list sort mode
SP_NEW_FACEBOOK_SOCIAL_CONTACT_IDStringnew-FB-social path flag
SP_KEY_LAST_SPAM_SYNC, SP_KEY_LAST_TIME_BACKUP, SP_KEY_LAST_TIME_REQUESTED_CARDSlong
SP_KEY_JOIN_VERSION, SP_KEY_JOIN_VERSION_CODE, SP_KEY_IS_REJOINregistration join telemetry
SP_KEY_VALIDATION_WITHStringb5.f — e.g. "Phone number"
SP_KEY_NOT_REGISTERED_WITH_REAL_CLI_V2booleanregistration state
SP_KEY_MY_VIRAL_ID, SP_KEY_FID__LIST__…referral / suppress-dialog data
SP_KEY_CONTACT_QUICK_GUIDE_JSON_V19Stringquick-guide state (contact_quick_guide remote flag)
SP_KEY_DA_NEED_PEEK_ANIMATION_V4, SP_KEY_IS_DA_ENABLE_FOR_MAIN_SCREEN_V3, SP_KEY_CLICKED_DRAW_ABOVE_GO_BTN, SP_KEY_CLICKED_ON_AUTO_START_GO_BTN, SP_KEY_DID_BATTERY_OPT_INTENT_FAILEDpermission/role-request UX bookkeeping
SP_ALLOWED_TO_START_GIVING_PICS_BY_FORCE, SP_INIT_FRESP_PICS_COUNTERphoto pipeline gates
SP_KEY_SYSTEM_EVENT_SENT, SP_KEY_STARTING_RESTORE_PROCESS, SP_KEY_START_TRAIL_POPUP_SHOWN, SP_KEY_SHOW_PREMIUM_PRESENT, SP_KEY_NEED_SHOW_ON_BOARDING, SP_KEY_ON_BOARDING_SHORTCUT_SHOW, SP_KEY_SHOULD_SHOW_MISSED_CALL_ON_HISTORY_TITLE, SP_KEY_PENDING_TTM_MESSAGES, SP_KEY_EYECON_REMOTE_CONFIG_JO_v2misc UI/feature flags
imeis_updatedStringp6.j — IMEI already pushed
usernameserverStringr5.r case 12 — last username pushed to updateuserdetails.jsp
SP_IS_SMS_WINDOW_ENABLE_BY_USERbooleanp6.j case 13 — SMS-window content-observer opt-in
SP_IS_OPEN_MENU_E2booleanmenu A/B
analytics_worker_reload_json, singular_events_json, default_recording_mode_jsonString (JSON)analytics / SDK configs

Reading: sp_call_reminder_list, permission_status_json, sp_social_installed_apps_v2 and cipher_key together constitute a plaintext profile of who the user is, what they permitted, which messengers they use, which phone numbers they have scheduled reminders for — stored in the app’s unencrypted-by-default SharedPreferences, i.e. readable by anything with the app’s data directory on a non-secure device or via a root/backup extraction.


23. Security-relevant findingsh4

Severity is my assessment [INF] unless the observation itself is marked [OBS].

F1 — HIGH · Weak “encryption” of cached contact photos, with a 999-value keyh5

[OBS] Algorithm Cipher.getInstance("Blowfish") = Blowfish/ECB/PKCS5Padding (no mode, no IV). Key = "Ewu" (from R.string.cipher_key) + String.valueOf(new Random().nextInt(999)) + "sTQHGK". The random component is persisted in plaintext under SharedPreferences key cipher_key. Consequences: (a) brute-forceable in <=999 tries; (b) ECB so identical photos produce identical ciphertext blocks (leaks image structure); (c) the key is recoverable from the device anyway. Correction to the briefing: the keyspace is 999, not 1000 (nextInt(999), RoomDatabase.MAX_BIND_PARAMETER_CNT = 999). Also the app can write a photo back into the user’s own address book (r5.e.m36579G opens RawContacts/<id>/display_photo for rw), so the cipher also protects a file written into the shared contacts provider.

F2 — HIGH · Java deserialisation sink on attacker-influenceable bytesh5

[OBS] am.RunnableC0215t1.run() case 1: new ObjectInputStream(new CipherInputStream(new ByteArrayInputStream(fileBytes), cipher)).readObject(). The plaintext bytes come from a file at ContactInfo.storage_photo_path. Because the cipher is trivially breakable (F1), anyone able to replace that file on shared/external storage (the code checks READ_EXTERNAL_STORAGE) can substitute an arbitrary Java serialised object stream and get readObject() to instantiate attacker-chosen classes in the app process. readObject() is reached twice in the same case (lines 132 and 179/204), so there are two distinct sinks on the same buffer. [INF] on exploitability: requires write access to the photo directory, which on Android 10+ scoped storage largely prevents, and a gadget class must exist on the classpath — but the pattern is unconditionally reachable and the app ships a very large merged classpath (Amplitude, Firebase, Mintegral/Mbridge, Fyber, Crakle, Ktor, Media3, Room…), which historically provides usable gadgets.

F3 — HIGH · Remote configuration and A-B flags fetched over cleartext HTTPh5

[OBS] d5.d default eyecon_remote_config_url = "http://cloudfront-cdn-all.eyecon-app.com/configs/ab-test.txt". Everything in the 400-key table (§7.1) is attacker-controllable on a hostile network, including:

  • shouldSendNameToServer — remote switch to upload contact names to the server;
  • ApiRequestSyncInterval — request cadence;
  • regexForAddSocial, facebooke_Extract_URL_Regex, shouldSearchForTwoNamesInFacebook, script_facebook_profile_validate, shouldSearchForTwoNamesInFacebook — regex/JS that drives Facebook/Instagram scraping;
  • google_photos_link_loader — JS source injected into a WebView;
  • mobitech_*_html_script — raw <script> HTML injected into WebViews with user_id="[advertisingId]";
  • all AdMob / Mintegral / Fyber / Crakle unit ids, use_ai_assistant, paywall_ui, premium_products*, shouldForceCallerIdRole, shouldForceDefaultDialerRole;
  • permissionDialogTiming, all_permission_dialogs_auto, should_show_fs_permission_auto_state, must_permission_worker_enable — remote control of consent-prompt presentation. Combined with F1 this is a full MITM-to-app-control chain.
F4 — HIGH · Bulk PII exfiltration surface reachable from the contact screensh5

[OBS] The contact screens drive, via r5.* and s5.*:

  • POST /addcontacts.jsp — contacts (whole address book), cli_cc (SIM country), possible_langs, init, apsVersion [OUT, w6.h];
  • POST /removecontacts.jsp — <cli_in_server>@a@<name>@z@ [OBS, s5.s];
  • GET /getcontactslang.jsp [OUT];
  • GET /getupdatedcontactsinfo.jsp + GET /resetupdatedstatus.jsp [OUT], applied in-cluster by r5.k case 12;
  • GET /pic?cli=<number>&is_callerid=true&…&cv=… per lookup [OUT] — one HTTP request per looked-up phone number, number in the URL;
  • uploadpic.jsp, getfreshpicsjson.jsp, getoptionalpics.jsp, picapproval.jsp [OUT];
  • GET /notifyaddcontact.jsp [OUT];
  • GET /cantalk.jsp?recvcli=<number>&message=<SMS body> and GET /getcantalkstatus.jsp?notifiedcli=<number> — plaintext numbers and the SMS text in the query string [OBS, p5.a / p5.e];
  • GET /updateuserdetails.jsp?username=… and ?imei=<IMEI> [OBS, r5.r case 12 / p6.j];
  • POST /grps/sgn with an RSA-encrypted {signatures, cid, random} attestation [OBS, r5.r case 11]. [INF] On the consent question: the app does gate the upload behind AbstractC9039h.m10665v() (“is a validated Eyecon user”) and the reminder flows behind AbstractC19327e.m35661t(), and j7.m records permission state, but there is no in-cluster evidence of an explicit, separately-recorded user opt-in to address-book upload; the address book is imported and uploaded as part of the normal first-run/refresh path (r5.g observer -> r5.e read -> s5 -> w6.h).
F5 — MEDIUM-HIGH · Contact data in plaintext SharedPreferences and logsh5

[OBS] sp_call_reminder_list (phone numbers + names), permission_status_json, sp_social_installed_apps_v2, cipher_key, usernameserver, imeis_updated, SP_KEY_PRE_EYECON_AMOUNT_OF_PHOTOS. Log tags that print PII: "Server", "addContactsList size = %s, addedList = %s", "AsyncTaskCalls", "msg = %s" (the message body), "ContactReminder", "Corrupted JSON in SP_CALL_REMINDER_LIST, resetting", "DBContacts", "giveContactsFreshPicUrls photo is waiting to show. name = %s, cis = %s", "PermissionStatusTracker", "loadJson: … / saveJson …", "SocialManager", "launchFacebook_ open in facebook app with username = %s", "TAG", "addNew result = data = %s" (the whole reminder JSON). Note the release-flushable flag print_logs_in_release in d5.d — so all of this is potentially emitted from a release build.

F6 — MEDIUM · Device write access to the user’s own contacts providerh5

[OBS] r5.e.m36586g(ContactInfo) performs applyBatch inserts into ContactsContract.RawContacts / Data (name, phone_v2), and r5.e.m36579G(long, byte[]) opens RawContacts/<id>/display_photo with mode "rw" to write the downloaded photo back. [INF] This is a legitimate feature (the user can approve an Eyecon name/photo for a contact) but it is a write into a system provider that other apps can observe; combined with F1 (the cipher protects bytes written here) it is worth calling out.

[OBS] r5.i0: http://m.facebook.com/profile.php?id=<username> (username path), http://instagram.com/_u/<id>, http://instagram.com/<id>, http://new.vk.com/id<id>. All are passed to startActivity/Intent.ACTION_VIEW; on a device without the target app these launch a browser over cleartext. [INF] the apps themselves now redirect to HTTPS, but the first hop is unencrypted and DNS/MITM can rewrite it.

F8 — MEDIUM · Third-party session replay over contact screensh5

[OBS] o0 is Amplitude SessionReplay (SessionReplay.captureSnapshot(), “Opting session(…) out of recording due to optOut config”, remote masking assets pulled from https://static.amplitude.com/onenav/static/img/…), gated by remote flag is_amplitude_enabled and driven from the communication/main-screen fragment chain. [INF] When enabled this captures contact names, numbers, avatars and the menifa sheet contents into a third-party replay service — no separate consent UI for “session replay” was found in this cluster.

F9 · MEDIUM · Facebook/Instagram profile scraping and identity bridgingh5

[OBS] r5.i0.m36649e extracts an FB id/username from a URL; m36651g loads https://m.facebook.com/profile.php?id=<id> in-app, scrapes the result with remote regexes, blocks repeats via w6.h.f56471g, and on success launches fb-messenger://user/<id>. r5.e.m36595p/m36599t also harvest EMAIL and SKYPE ids straight out of the device address book and store them as social rows with social_source = 0. w6.h.m38436i posts {name, type, attributes, timestamp} for each new match. This is mapping the user’s real address book onto third-party social identities — the strongest single privacy signal in C4.

[OBS] j7.m stores a machine-readable permission map; j7.d is the “force default dialer / enable caller-ID role” dialog with per-feature rows (ll_smarter_call, ll_spam, ll_photos) that can be individually hidden; remote flags permissionDialogTiming, all_permission_dialogs_auto, should_show_fs_permission_auto_state, reg_ask_runtime_permission_first, ask_read_notification_in_registration, shouldForceCallerIdRole, shouldForceDefaultDialerRole, shouldForceCallerIdRole, aftercall_caller_id_role_request_cooldown_in_hours, interval_ms_role_changes determine when and how often the app asks for the default-dialer / caller-id roles. must_permission_worker_enable / must_permission_worker_repeat_time run a background “must ask” worker. d7.c supplies the back-press trap. This is a pattern of nudging-for-permission rather than a one-shot consent, controllable by the (cleartext) config.

F11 · LOW · Heavy R8 class merging increases blast radiush5

[OBS] p001a (a.a), p528rk (rk.a/rk.b), and the 76-class am package are R8 “class-vertical-merging” artifacts that collapse ZXing, Amplitude SessionReplay, Firebase Messaging, RxJava2, Crakle, Moloco, Mintegral, Fyber and Ktor code into shared utility classes. Two consequences: (a) any gadget-chain analysis must consider this merged classpath (F2), and (b) rk.a.m36780r is a generic SharedPreferences boolean setter reachable from ordinary feature code, i.e. arbitrary boolean preference writes via one small helper.

F12 · INFO · Not vulnerabilities, but worth recordingh5
  • r5.e reads the address book on every ContactsProvider change (r5.g observer) — no visible debounce in C4; the dedup HashSet per contact_id limits write amplification.
  • r5.e.m36603x applies LIMIT 100 only when both flags are set — in the general case the full address book is walked on every refresh.
  • q6.v.m36079j diffs but still calls notifyItemChanged per visible row.
  • q5.g.m36036b silently drops every pending reminder if the JSON array is corrupt.
  • The r5.j CANTALK_TYPE_* / canTalkStatus flow depends on the Google Credential Manager (dh.i), i.e. can-talk state is coupled to a Google sign-in token.

24. Appendix — what this cluster does NOT containh4

Briefing claimReal locationStatus
w6.h.m38428a builds the addcontacts.jsp payloadp620w6\C21280h.java:112-179Not in C4. Claim verified correct out-of-cluster (see §21.2 Part A).
@z@ record format for addcontacts.jspp620w6\C21280h.java:140 (blob.split("@z@").length)Not in C4. Format confirmed; field separator @a@ confirmed via removecontacts.jsp inside C4.
delta heuristic contactLangNeedUpdate / >20 / >5 %p620w6\C21280h.java:143Not in C4. Verified correct out-of-cluster.
getcontactslang.jsp returns a 2-letter ISO codesame file, .matches("[a-zA-Z]{2}")Not in C4. Verified.
Blowfish/ECB key "Ewu" + nextInt + "sTQHGK"p379k5\RunnableC17750b.java:401-414Not in C4 (key derivation) — but the decrypt sink is in C4 (am\RunnableC0215t1). Verified; keyspace is 999 not 1000.
a7.AbstractC0039a column constantsfp008a7\AbstractC0039a.javaReferenced from C4, fully transcribed in §0.2.
contactdb 48 columns / v57 migrationsthe SQLiteOpenHelper package (outside C4)Not in C4. Table names history, contacts, social, fresh_pics, block_list, recorded_notes recovered in C4 from r5.r case 4.
Caller-ID GET .../pic?cli=…&is_callerid=true&…p620w6\C21280h.java:317-339Not in C4. Verified verbatim correct.
getfreshpicsjson.jsp / getoptionalpics.jsp / picapproval.jspandroidx\fragment\app\RunnableC2598c.java, p607vk\RunnableC21082e0.java:242, p572u4\RunnableC20597o.java:147Not in C4.
getnames.jspp599v7\RunnableC20908f.java:372Not in C4.
notifyaddcontact.jspas\RunnableC3316o.java:111,116Not in C4.
getupdatedcontactsinfo.jsp / resetupdatedstatus.jspp607vk\RunnableC21093i.java:140, p607vk\RunnableC21082e0.java:368Not in C4 — but the response-application algorithm IS in C4 (r5.k case 12).
com.eyecon.global.* wrappers (MainActivity, MainFragment, ContactListInfoArea, ContactGridTextArea, EyeSearchActivity, FavoritesFragment, ForYouFragment, HistoryFragment, ContactsChooserActivity, NewContactActivity, DBContacts, ContactReminderActivity, ContactReminderWorker)com\eyecon\global\…Not in C4 (thin wrappers only). Their callees are the packages documented here.
manifest <queries> for ~30 messaging appsmanifestNot in C4. However, the in-code install check r5.i0.m36645a() caches the detected set into sp_social_installed_apps_v2, and r5.h0 enumerates 46 social apps with their package names — consistent with the manifest claim.
”The app queries ~30 messaging apps for feature detection”r5.h0 (46 enum constants, ~30 distinct package names) + sp_social_installed_apps_v2Partially confirmed in C4 (the enum + the pref); the manifest block itself is out of scope.

Erratum to §20.2 (am package attribution). On a second pass I checked the imports of the am classes rather than assuming a library. am is not a coherent third-party namespace: it is a mixed R8 class-merge bucket. Confirmed contents: Firebase-Messaging helpers (am.t1 Firebase branches — FirebaseMessaging, PowerManager.WakeLock, NamedThreadFactory("firebase-iid-executor"); am.v1 ExecutorC0221v1; am.u1, am.r, am.r1, am.s, am.s1, am.x, am.x1), plus a large set of small merged utility/interface types whose imports are plain JDK (java.nio.charset.Charset, java.util.BitSet, java.net.SocketAddress, java.util.logging.Level, java.util.concurrent.*) and MICO’s own e2.b async-callback contracts. The RxJava-style names (am.b1, am.k0, am.l, am.m1, am.o0, am.q1, am.h0, am.f0, …) are single-method interfaces that MICO’s own classes implement (e.g. r5.m implements am.b1), i.e. obfuscated MICO callback interfaces that happen to share a bucket with merged library code — not a bundled RxJava. Conclusion stands that am contains no contact/reminder logic other than am.t1 case 1, but the “RxJava2 operators” wording in §20.2 should be read as “obfuscated/merged third-party and MICO helper interfaces”. [OBS] the Firebase + photo-decrypt mix in am.t1 itself remains verified.

Erratum to §19 (rk.c). rk.c’s 40-element static table new C19951c(n, C9049r(...), ...) is Facebook Audience Network / Meta placements (C9049r, C17558l, package com.facebook.ads.internal.*), merged into the R8 bucket by accident — not a MICO notification/permission index as one might assume from the shape. No MICO semantics.


25. Coverage ledgerh4

itemstate
27 / 27 packages listed in the TSV100 % read (p516r5 36, p444o0 111, am 76, p493q6 55, p360j7 20, p541s5 19, p475p6 16, p236d5 5, p364jc 14, p574u6 17, p032b5 8, p246dh 17, p046c6 16, p315h7 14, p001a 1, p555t6 17, p492q5 9, p474p5 5, p304g6 5, p528rk 3, p702z6 5, p556t7 2, p238d7 4, p517r6 2, p009a8 7, p392kq 2, p542s6 3 = 489 files — matches the TSV)
Every class given a real name from renamed fromYes — §0.2, §2.0, §3-§19, §20
Flow 1 (ContactsProvider -> DB)Traced, 13 steps, §21.1
Flow 2 (batching/upload/delta)Traced, Parts A-D, §21.2
Flow 3 (photo acquire/cache/approve/encrypt)Traced, A-D, §21.3
Flow 4 (name resolution / social / FB-ID)Traced, 9 steps, §21.4
Flow 5 (main screen / tabs / adapters / search)Traced, §21.5
Flow 6 (reminder scheduling / notifications)Traced, 9 steps, §21.6
Pref keys / endpoints / table names inventoried§0.2, §7.1, §22, §24
Security findings§23, F1-F12

Cluster C5 — Media, Social, and Peripheral Featuresh3

APK: com.eyecon.global (MICO — caller ID / spam blocker), v4.2026.04.23.1145 Source root: C:\Users\btc_e\Downloads\mico-analysis\src\sources Scope: 14 obfuscated packages, 229 files, ~1.2 MB Document status: COMPLETE - 14/14 packages documented, plus the 34 first-party wrapper files of the 13 named modules.

Convention: “OBSERVED” = directly readable in decompiled source. “INFERRED” = reasoned from call sites / names / cross-cluster references and not fully proven. Disk dir pNNNxxx maps to real package NNN via the /* JADX INFO: renamed from: ... */ header on each file.


0. Package inventory and first-party / third-party classificationh4

#Disk dirReal pkgFilesKBBriefing moduleVERDICTResponsibility (one line)
1p703z7z751286TokiFIRST-PARTY MICOToki (walkie-talkie) UI Fragment + contact-chooser + adapters + ring/sound assets
2p282f5f527194BackupFIRST-PARTY MICOGoogle-Drive backup/restore of contactdb
3p598v6v622137MoreMenuAndSettingsFIRST-PARTY MICO”More menu” / Settings entry hub, deep-link router
4p432n7n727125MoreMenuAndSettingsFIRST-PARTY MICO - but it is the Photos / PhotoPicker module, not a Google-Account sync adapterPhotoPicker source-chooser, crop, persist, Facebook + Google-Images photo harvesting
5p572u4u431120AdsIBTHIRD-PARTY (Fyber FairBid)FairBid interstitial/rewarded mediation layer
6p526riri22119MoreMenuAndSettingsTHIRD-PARTY (Google Firebase Cloud Messaging internals)FCM EnhancedIntentService, topic subscribe, ProcessIntentHelper, GCM task prefs. NOT first-party. See 8.1
7p720zqzq1591PermissionsMIXED — zq.g FIRST-PARTY PermissionsHelper; zq.a…zq.o are THIRD-PARTY OkHttp 4 platform/TLS-socket-alias classesPermissions helper + OkHttp TLS platform
8p477p8p8665MoreMenuAndSettingsTHIRD-PARTY / JADX merge (Facebook BitmapUploader + OkHttp HttpUrl + RxJava plugins)See 8.2. Only first-party content is the SP key SP_KEY_IS_MISSED_CALL_NOTIFICATION_ENABLED
9p566tsts740MoreMenuAndSettingsTHIRD-PARTY (Moloco tech.crackle)Crackle rewarded-video ad + OMID viewability
10p249dndn1334AdsTHIRD-PARTY (Moloco tech.crackle VAST/advert-session layer)Ad event + pricing waterfall glue for Moloco
11p462olol215PhotosTHIRD-PARTY (Glide)Glide Bitmap downsample+EXIF-rotate AsyncTask. NOT photos code. See 4
12p010a9a9115MoreMenuAndSettingsMIXED / JADX artefact — one giant Runnable merging ~9 unrelated original classesSettings-screen WebView + Fyber FairBid banner request
13p367jhjh48MenifaFIRST-PARTY MICO — but not Menifa; it is the “Can You Talk” (SMS/WhatsApp) delivery-status featureCanTalk status widget + VOIP-ish payload holder
14p007a6a611DynamicAreaFIRST-PARTY MICO (1-line stub)ViewPager2 fragment lifecycle observer for the Dynamic-Area card pager

Totals: 14 packages / 229 files / ~1.15 MB (measured: 1,146,538 bytes).

First-party vs third-party split (file counts)h5
  • FIRST-PARTY MICO: 133 files - z7 51, f5 27, v6 22, n7 27, jh 4, a6 1, zq.g 1
  • THIRD-PARTY SDK: ~95 files - ri 22 (Firebase Messaging), u4 31 (Fyber FairBid), ts 7 + dn 13 (Moloco/Crackle), ol 2 (Glide), p8 6 (Facebook/OkHttp/RxJava), zq 14 (OkHttp platform), a9 ~0.9 (FairBid/InMobi/Vungle merged)
  • Mixed merge artefacts carrying a thin first-party payload: n7.e (9-way merge, 1 first-party class), f5.n/f5.x (~0 first-party), a9 (1 first-party payload: the eyecon://idPlus settings-consent deep link)
Notable briefing correctionsh5
  • p462ol is NOT the Photos module. It is Glide’s image downsampler (BitmapFactory inSampleSize doubling + EXIF orientation parse via pl.C19372b + OkHttp fetch via p407ll.C18399k). Mislabelled in the TSV.
  • p367jh is NOT “Menifa”. It is the Can You Talk (CYT) SMS/WhatsApp read-receipt tracker. Menifa is not represented in this cluster’s file list at all (searched: no Menifa string in any of the 229 files — see §13).
  • p572u4 is NOT first-party ad mediation; it is verbatim Fyber FairBid SDK code (com.fyber.fairbid.g00, FairBidAdRequest, waterfall types).
  • p566ts and p249dn are Moloco, not first-party.
  • Toki does NOT stream the microphone continuously. Proof in §2.
  • f5 backup: “the ENTIRE contactdb file is uploaded” is CORRECT but needs qualification — see §3.
  • The briefing omits com.eyecon.global.Photos.PhotosTrackerWorker entirely. It is a WorkManager job that runs every 3 days on Wi-Fi and silently downloads the Facebook profile picture of every Facebook-linked contact and installs it as the Eyecon contact avatar, using a Facebook application secret hardcoded in strings.xml (R.string.fct). See 17.4 / 17.5.
  • The briefing omits the hidden Facebook mobile-web people search (m7.h -> https://m.facebook.com/search/people/?q=<contact name>) run in an invisible JS_ENABLED WebView with a spoofed Pixel 2 / Chrome 79 user-agent. See 17.2.
  • Remote config is fetched over CLEARTEXT HTTP and is unauthenticated (http://cloudfront-cdn-all.eyecon-app.com/configs/ab-test.txt, 400 keys). CONFIRMED - see 20.1.
  • cards.eyecon-app.com is the Dynamic-Area CARDS host, not the ad host. Ad configuration is GAM unit ids from remote config. See 15.5 vs 16.
  • NEW: e-auth-c is NOT a cryptographic challenge. C21273a.m38421E() merely sums the numeric values of the first characters of the --separated e-auth segments. It adds no authentication. See 21.1.

1. Package a6 — DynamicArea card pager fragment (1 file)h4

Disk: p007a6/C0038a.java → real a6.a, classes3.dex Classification: FIRST-PARTY MICO. Kind: Fragment lifecycle observer subclass.

a6.a
public final class C0038a extends AbstractC22072a {
public final ViewPager2 f132h; // renamed from: h
public C0038a(ViewPager2, InterfaceC21859n, ArrayList);
}

Constructor body (OBSERVED):

super(interfaceC21859n, arrayList);
this.f132h = viewPager2;
this.f60181e.getLifecycle().addObserver(this);
  • Superclass p701z5.AbstractC22072a (y5.a) holds f60181e (a LifecycleOwner, base of the ViewPager2 FragmentStateAdapter chain) and wires InterfaceC21859n (y5.n) page callbacks.
  • No endpoints, no pref keys, no strings. Purely the Dynamic-Area “cards” carousel plumbing. The config fetch for these cards is cards.eyecon-app.com and is implemented elsewhere (p647.../C21856k, p674..., etc., outside C5 scope).
  • No security-relevant content.

2. Package z7 — Toki (Walkie-Talkie) — FIRST-PARTY, 51 filesh4

2.0 Class maph5
Disk fileReal classKindPurpose
C22122v0.java (128 KB)z7.v0Fragment + wl.InterfaceC21451n implThe Toki call Fragment. Owns UI, state machine, session, PTT button
C22107o.javaz7.oRecyclerView adapter-ishParticipant row binder (m39366q(C22113r))
C22101l.javaz7.lAdapterGrid adapter (D/E renamed members)
ViewOnTouchListenerC22089f.javaz7.fView.OnTouchListenerSwipe-up/down detector (FlingAnimation, SpringAnimation, SpringForce)
C22111q.javaz7.qDialogSession-error / press_and_hold bubble (m4117o0, m4130r0, m4132t0, m4133u0, m4115m0, m4130r0)
C22125x.javaz7.xListener—
RunnableC22092g0.javaz7.g0RunnableRECORD_AUDIO never_ask_again_mode gate
C22109p.javaz7.pModelRemote Toki invitation state (f60360l, f60353e)
ViewOnClickListenerC22095i.javaz7.iClick listener—
C22081b.javaz7.bFloatPropertyCompatAnimated property helper
C22103m.java, C22104m0.javaz7.m, z7.m0Handler callbacks—
C22086d0.java, C22128y0.javaz7.d0, z7.y0Internal views—
C22090f0.javaz7.f0View holderParticipant cell
C22093h.java, RunnableC22094h0.javaz7.h, z7.h0Listener / RunnableRetry buttons
ViewOnClickListenerC22098j0.javaz7.j0Click listener—
C22096i0.javaz7.i0View holder—
C22091g.java, RunnableC22079a.javaz7.g, z7.aHolder / Runnable—
ViewOnClickListenerC22126x0.javaz7.x0Click listenerOpens https://docs.google.com/forms/d/17QjnHJanVGPt0V_AkVj_baVmE2xZZGOyjAKjB22oeOU/viewform?edit_requested=true (user-feedback Google Form, hardcoded)
C22099k.java, ViewOnClickListenerC22100k0.javaz7.k, z7.k0——
C22110p0.javaz7.p0BroadcastReceivercom.eyecon.global.isonear (near-peer/live-cluster broadcast)
ViewOnClickListenerC22117t.javaz7.tClick listener—
C22113r.javaz7.rModelPer-participant row model: f60377c name, f60378d cli, f60379e number, f60382h bitmap, f60383i adapter, f60384j status enum
RunnableC22114r0.javaz7.r0Runnable—
C22115s.javaz7.sRegistryf60387b.f60388a = Map<…, z7.v0> of live Toki fragments
EnumC22118t0.javaz7.t0EnumTalk-status per participant
C22119u.java, EnumC22120u0.javaz7.u, z7.u0Enum + factorySound-effect / talk modes (.REJECT, .CLICK_TALK, m39377a(), m39378b(float))
C22121v.javaz7.v——
C22112q0.javaz7.q0CallbackCHANNEL_JOIN callback
C22127y.java, C22116s0.javaz7.y, z7.s0AudioFocus listenerz7.s0 = AudioManager.OnAudioFocusChangeListener
C22083c/85d/87e.javaz7.c/d/eDynamicAnimation.OnAnimationEndListenerAnimation completion
ViewOnClickListenerC22082b0 / C22084c0z7.b0, z7.c0Click listenersBoth also open the same hardcoded Google Form feedback URL
ViewOnTouchListenerC22102l0.javaz7.l0Touch listener—
AbstractC22088e0.javaz7.e0BuilderAudioFocusRequest.Builder (API 26+), m39345a()
C22080a0.javaz7.a0ModelContacts-chooser row (.private_name, .phone_number_in_server)
2.1 RESOLVED QUESTION — is Toki continuous or on-demand microphone?h5

CORRECTION TO PRIOR REPORTING. Toki is a PUSH-TO-TALK walkie-talkie. The microphone is captured only while the user holds/clicks the talk button, and the capture is stopped on release. It is NOT a continuous background microphone stream.

Proof (OBSERVED, line-exact):

  1. The single AudioRecord in the whole APK is p599v7/RunnableC20908f.java:508 (v7.f switch case 11), verbatim:
case 11:
RunnableC21441d runnableC21441d = (RunnableC21441d) this.f55270b; // wl.d ("client.Recorder")
C21450m c21450m3 = (C21450m) this.f55271c; // wl.m (session)
runnableC21441d.f57146h = c21450m3; // wl.d.h
if (runnableC21441d.f57145g) { return; } // already recording
Process.setThreadPriority(-19); // THREAD_URGENT_AUDIO
runnableC21441d.f57148j = false; // buffered (not yet live)
AmrEncoder.init(0);
AudioRecord audioRecord = new AudioRecord(1, 8000, 16, 2, 16000); // MIC, 8000Hz, MONO, PCM16, 16000 buf
runnableC21441d.f57147i = audioRecord;
audioRecord.startRecording();
runnableC21441d.f57145g = true; // RECORDING flag ON
return;
  1. case 11 is posted from exactly two call sites, both inside z7.v0.m39404R0() (the press-talk handler):
    • p703z7/C22122v0.java:1170 → runnableC21441d.m38602b(new RunnableC20908f(11, runnableC21441d, c21450m)); followed by TALK_START
    • p703z7/C22122v0.java:1186 → same, followed by CHANNEL_JOIN_START_TALK
  2. m39404R0() is the z7.f OnTouchListener ACTION_DOWN / CLICK_TALK path. Its guard: int i3 = this.f60427G0; if (i3 == 7) { EnumC22120u0.REJECT.m39377a(); return; } if (i3 != 5) … — i.e. recording can only start in state 5 (“answered”).
  3. Stop path (OBSERVED, p633wl/RunnableC21440c.java = wl.c):
    • case 0 → m38601a() (stop + release) then f57144f = false (kills the thread)
    • case 1 → f57148j = true; then flushes the buffered DatagramPacket list — this is “answer started, send what I said while the invite was ringing”.
  4. wl.d.m38601a() calls audioRecord.stop(), flushes queued packets only if f57148j was never set to true (i.e. never released → discard), then audioRecord.release(), f57145g = false, Process.setThreadPriority(0), System.out.println("Recorder Stopped").
  5. App-wide search for new AudioRecord returns exactly one hit; MediaRecorder returns zero hits in first-party code; AudioRecord is also guarded by z7.v0.m39395G0():
if (ContextCompat.checkSelfPermission(MyApplication.f12163h, "android.permission.RECORD_AUDIO") != 0) …

and RECORD_AUDIO refusal is persisted per-permission as "never_ask_again_mode" (z7.g0).

Conclusion (OBSERVED): on-demand, press-and-hold only. Frames captured while the button is held are AMR-encoded and pushed to ptt.eyecon-app.com:<port> over plain UDP. Any third party on the network path can read the speech.

2.2 Capture → encode → UDP framing → server (step by step)h5

Step 1 — CAPTURE — wl.d (p633wl/RunnableC21441d.java), thread name "client.Recorder" (set in wl.m’s constructor, p633wl/C21450m.java:76).

byte[] bArr = new byte[TypedValues.PositionType.TYPE_CURVE_FIT]; // 2002-byte TX packet buffer
short[] sArr = new short[2400]; // 2400 samples @ 8 kHz = 300 ms
int iEncode3 = AmrEncoder.encode3(this.f57149k /* mode = 7 */, sArr, 0,
this.f57147i.read(sArr, 0, 2400), bArr, 16); // payload starts at offset 16
bArr[16] = (byte) (bArr[16] | 2); // flag "AMR" on the frame
DatagramPacket datagramPacket = new DatagramPacket(bArr, iEncode3 + 16);
if (this.f57148j) { this.f57146h.m38611f(datagramPacket); } // live → send now
else { this.f57143e.add(datagramPacket); } // pre-answer → buffer
  • io.kvh.media.amr.AmrEncoder (kvhk AMR port) — mode index 7 ⇒ AMR-NB frame; AmrEncoder.init(0) = single-frame encode API.
  • f57149k is assigned 7 at the top of run() and never changed (OBSERVED) — a single fixed AMR mode.
  • Buffer ring: Runnable[1000] command ring + ArrayList DatagramPacket FIFO.

Step 2 — FRAMING / SEND — wl.m.m38611f(DatagramPacket) (p633wl/C21450m.java:166):

byte[] data = datagramPacket.getData();
data[7] = (byte)((i3 >>> 24) & 255); // i3 = wl.m.d == the SESSION id
data[8] = (byte)((i3 >>> 16) & 255);
data[9] = (byte)((i3 >>> 8) & 255);
data[10] = (byte)( i3 & 255);
C21952k k = (C21952k)((C22277c) this.f57168a.f1990b).f61108c; // yl.k transport
DatagramSocket sock = k.f59289b;
DatagramPacket parity = k.f59306s;
byte[] data2 = datagramPacket.getData();
data2[1] = 1; // PKT TYPE = AUDIO
k.f59285A++; AbstractC19124b.m35480z(data2, 2, k.f59285A); // 4-byte seq @ offset 2
k.f59312y++; AbstractC19124b.m35480z(data2, 7, k.f59312y); // 4-byte control-seq @ offset 7
data2[6] = 1; // flags byte
datagramPacket.setAddress(k.f59291d); datagramPacket.setPort(k.f59290c);
sock.send(datagramPacket);
if (datagramPacket.getLength() != parity.getLength()) { // OAR FEC parity (XOR)
sock.send(datagramPacket); k.f59312y++;
} else {
byte[] data3 = parity.getData();
for (int i = 16; i < data2.length; i++) data3[i] = (byte)(data3[i] ^ data2[i]);
k.f59285A++; AbstractC19124b.m35480z(data3, 2, k.f59285A);
k.f59312y++; AbstractC19124b.m35480z(data3, 7, k.f59312y);
data3[6] = 2; // PKT TYPE = XOR PARITY
sock.send(parity);
}
parity.setData(datagramPacket.getData(), 0, datagramPacket.getLength());

Step 3 — TRANSPORT — yl.k (p687yl/C21952k.java) + yl.b (p687yl/RunnableC21943b.java), thread "network.UDPClient" / RunnableC21943b.class.getName().

Wire format (OBSERVED, 15-byte control header; audio uses the same 15 bytes + 1 marker byte so audio payload begins at offset 16):

OffsetSizeMeaning
01(unused/0 in all observed constructions)
11packet type: 0=CONTROL, 1=AUDIO, 2=XOR-FEC-parity
24packet_seq (int, AbstractC19124b.m35480z/m35471q)
61flags: 0x80=SYN, 0x08=MSG/keepalive-class, 0x40=carries NACK list
74control_seq / media timestamp (for audio: the Toki session id)
114NACK / lost-packet seq list (present iff flags & 0x40)
1510 for audio (differentiates from a 15-byte CONTROL packet)
16..nAMR frame; first byte has bit1 set (| 2) to mark it AMR

Handshake state machine yl.k.f59308u: 0 = initial, 1 = SYN_RCVD, 2 = SYN sent, 3 = ESTABLISHED (set in yl.b.run() case i15 == 2 && data[1]==0 && flags&0x80 && flags&0x40, then m39304h() starts the 5 s keepalive and onConnected() fires). Every control packet is sent twice (m39303g: datagramSocket.send(datagramPacket); datagramSocket.send(datagramPacket);).

No DTLS, no SRTP, no AES, no HMAC anywhere on this path. DatagramSocket (plain UDP), setSendBufferSize(2002).

Step 4 — SERVER — as.u2 (as/C3349u2.java:874, log tag "TalkiServer" — the class was renamed from the Talki/walkie-talkie SDK):

public C3349u2(InetAddress inetAddress, int i3, C21450m c21450m) throws SocketException {
this.f1989a = 24; this.f1994f = c21450m; // wl.m
DatagramSocket datagramSocket = new DatagramSocket();
AbstractC17448b.m33485E("TalkiServer", "socket port:" + datagramSocket.getLocalPort()
+ " connecting to: " + i3);
datagramSocket.setSendBufferSize(TypedValues.PositionType.TYPE_CURVE_FIT);
C21952k c21952k = new C21952k(datagramSocket, inetAddress, i3);
...
thread.setName("network.UDPClient");
…
// send initial SYN
byte[] bArr = new byte[15]; bArr[1] = 0; bArr[6] = (byte)(bArr[6] | -128);
c21952k.f59308u = 2;
c21952k.m39303g(c21942a);
}

Host is supplied by the caller. In z7.v0 it is:

this.f60476s0 = "ptt.eyecon-app.com"; // C22122v0.java:419 (default field init)
…
this.f60476s0 = arguments.getString("INTENT_KEY_SERVER_ADDRESS", "ptt.eyecon-app.com"); // :1994

Port: zq.g.m39737C() (p720zq/AbstractC22312g.java:221):

C11856m arr = C18416a.m34586w(AbstractC15804d.m31091l("toki_ports", false)).m14605h();
// remote-config key "toki_ports" = JSON array of ints and/or "lo-hi" ranges
return ((Integer) arrayList.get(new Random().nextInt(arrayList.size()))).intValue();
} catch (Exception e) { … return 5620; } // hard default

Security-relevant (OBSERVED): the UDP destination port is server-controlled via remote config (toki_ports). Anyone able to tamper with remote config (which C1/C2 established is fetched over cleartext HTTP) can redirect the plaintext microphone stream to an arbitrary host.

Step 5 — PLAYBACK (receive side) — wl.b (p633wl/RunnableC21439b.java), thread "client.Player":

this.f57127q = AudioTrack.getMinBufferSize(8000, 4, 2); // 8000 Hz, MONO, PCM16
…
int length2 = datagramPacket.getLength() - 16;
if ((data[16] & 2) != 0) { // AMR marker
short[] sArr = new short[length2 * 5];
AmrDecoder.decode2(this.f57122l, data, 16, length2, sArr);
iWrite = this.f57120j.write(sArr, 0, length2 * 5);
} else { // raw PCM16 fallback
iWrite = this.f57120j.write(data, 16, length2);
}

Plus per-packet jitter accounting printed to logcat: "dataPos: ", "position: ", "duration:", "jitter:", "packet_jitter_avg_100:" (System.out.println, i.e. logcat I/D).

2.3 Session / auth (e-auth re-implementation)h5

Step by step:

  1. z7.v0.m39382O0(...) (push-notification entry) reads SharedPreferencesC16497y.m32082b(null, "authenticatedCli") — pref key authenticatedCli (C22122v0.java<508>). If present it builds a session:
int iM39737C = AbstractC22312g.m39737C(); // UDP port from remote cfg
C21450m c21450m = new C21450m(new C22112q0(c21450mArr, z10, zM34703c, i3, i10),
MyApplication.f12163h,
C18312b.m34437h().m34444e(str3)); // authenticatedCli -> cli
c21450m.m38608b(str /* server host */, iM39737C); // -> InetAddress.getByName -> TalkiServer
  1. wl.m.m38608b(String host, int port) → new Thread(new RunnableC17180l0(this, host, port, iArr, 6)) → case 6 (p340i7/RunnableC17180l0.java:305):
InetAddress byName = InetAddress.getByName((String) this.f42704c);
C21450m c21450m = (C21450m) this.f42706e;
c21450m.f57168a = new C3349u2(byName, this.f42703b, c21450m); // as.u2 TalkiServer
((C21450m) this.f42706e).f57168a.m3504w();
  1. The e-auth credential is re-implemented inside the signalling payload — wl.m.m38606c(String cli, String e_auth, String e_auth_c) (p633wl/C21450m.java:101):
C11859p o = new C11859p(); // Gson JsonObject
o.m14611q("cli", cli);
o.m14611q("vn", "4.2026.04.23.1145"); // hardcoded app version
o.m14611q("vc", String.valueOf(749)); // hardcoded VOIP/protocol version 749
o.m14611q("e-auth-v", "e1"); // auth scheme version
o.m14611q("e-auth", e_auth); // <-- THE CREDENTIAL
o.m14611q("e-auth-c", e_auth_c); // challenge/nonce answer

CORRECTED (see 21.1 and 22.2). p620w6/C21273a.m38421E() was later read in full: split e-auth on ”-”, sum Character.getNumericValue(first char) of each segment, return the decimal string. So e-auth-c is NOT a signature or challenge response - it is a trivially derivable checksum of e-auth that any passive observer of a single frame can recompute, and it provides zero additional authentication. e-auth itself is the Eyecon registration/session id (cid) per clusters C1/C2. What is proven here is that the Toki UDP client recomputes and re-sends both values in every CHANNEL_* JSON frame over the same signalling channel.

Signalling verbs sent by wl.m (all as Gson objects, C3349u2.m3503v(type, json, callback, runnable)):

VerbPayload keysMethod
CHANNEL_JOINcli, vn, vc, e-auth-v, e-auth, e-auth-c, channel_id, invitation_idm38609d(int channelId,int invitationId,String auth,String authC)
CHANNEL_JOIN_START_TALKsame + channel_id, invitation_idz7.v0.m39404R0() line 1190
CHANNEL_INVITEsame + invited_clis (array)m38607a(C11856m invited,String auth,String authC)
TALK_STARTchannel_id, session_idz7.v0 line 1174
TALK_DONEchannel_id, session_idm38612g()
CHANNEL_BROADCAST_COMMANDsound_fx_id, cli, channel_id, session_id, type="playSoundFX"m38613h(int)
SESSION_STATUSchannel_id, cli, session_id, statusm38614i(int)
INVITE_REJECTchannel_id, invitation_id, cli, typem38610e(int,int,int,Runnable)

Error mapping (z7.v0.mo37215L, OBSERVED) — pushed into a HashSet f60482v0 used as an analytics tag set:

  • HTTP-ish 412 → "receiver version not supported", UI code 8
  • 409 → "receiver not support toki", UI code 7
  • other → "invite failed", UI code 5
2.4 Toki state machine (OBSERVED)h5

z7.v0.f60427G0 (G0) states seen in code: 1, 2, 3, 4, 5 (answered / can talk), 7 (rejected), 8 (client too old — mic disabled), 9 (finished). m39397J0() returns true for {2,3,4,8,9} = “not currently able to talk”. m39380C0() returns the singleton fragment unless f60436P == null || G0 == 9. m39381H0() scans z7.s’s registry for any fragment with G0 != 9. f60477t = new C3818e("**") — Lottie animation placeholder, ** is a mask/loop sentinel.

2.5 Toki security findingsh5
SevFindingEvidence
HIGHMicrophone audio (AMR) transmitted over plaintext UDP; any on-path observer (Wi-Fi AP, ISP, malicious router) can record/replay voice.p633wl/C21450m.java:166-210 m38611f → DatagramSocket.send; p687yl/C21952k.java:320-321
MEDIUMToki is push-to-talk, not continuous — lower severity than a “live mic” claim. Recording stops on button release; frames buffered before answer are discarded if the session never went live.p633wl/RunnableC21440c.java:26-46, RunnableC21441d.java:64-79
MEDIUMe-auth credential (session token) replayed into the Toki signalling payload; anyone who captures one CHANNEL_JOIN obtains the token verbatim.p633wl/C21450m.java:101-110
MEDIUMDestination host/port remotely configurable ⇒ redirection of the plaintext audio stream if remote config is tampered (cleartext config per C1).p720zq/AbstractC22312g.java:221-252, p703z7/C22122v0.java:419
LOWHardcoded Google-Form feedback URL in 3 click handlers.ViewOnClickListenerC22082b0:39, ViewOnClickListenerC22084c0:45, ViewOnClickListenerC22126x0:51
LOWSystem.out.println of jitter / audio byte counters (information in logcat).p633wl/RunnableC21439b.java:151-186

3. Package f5 — Google-Drive Backup / Restore — FIRST-PARTY, 27 filesh4

Entry point: com.eyecon.global.Backup.BackupActivity (p261e6 bundle). Fragments: f5.j (upload UI) and f5.u (restore UI), both p047c7.AbstractC3872d (BaseFragment) subclasses, both holding a com.eyecon.global.Backup.ProgressTracker.

3.0 Class maph5
Disk fileReal classKindPurpose
AbstractC16422f.javaf5.fabstract baseBackup/restore session base. Owns the six Drive object names, Drive handler, thread slots, progress
C16434r.javaf5.rf5.f subclassRESTORE (backup reader). Lists appDataFolder, downloads, merges into a temp DB, then swaps
C16442z.javaf5.zf5.f subclassUPLOAD (backup writer). Creates folders, zips/copies/uploads the five payloads
C16418b0.javaf5.b0final class (DriveHandler)Google Drive REST wrapper (Drive, Drive.Files, SSLContext, ZIP unzip)
C16426j.javaf5.jFragment”Upload to Drive” screen
C16437u.javaf5.uFragment”Restore from Drive” screen; static f40753C = active f5.z upload handle
RunnableC16439w.javaf5.wRunnableUpload worker, 6 cases (0=orchestrator, 1=TTM zip, 2=recordings, 3=address book, 4=database, 5=call log)
RunnableC16429m.javaf5.mRunnableRestore worker, 6 cases
RunnableC16431o.javaf5.oRunnableDownload one call-record file (m31908c) + progress PROGRESS_RECORDS
RunnableC16432p.javaf5.pRunnableDownload address_book / call_logs
RunnableC16433q.javaf5.qRunnableDiff-and-upload single file (skips if a file with the same name already exists remotely)
RunnableC16438v.javaf5.vRunnablepending_temp_<name> → rename, and fail_safe_<name> fallback / cleanup
RunnableC16441y.javaf5.yRunnableUpload one AMR recording into eyecon_backup_for_calls_records
RunnableC16423g.javaf5.gRunnableACTION_RESTART_AFTER_RECOVERY broadcast after restore
RunnableC16428l.javaf5.lRunnableLooper.quitSafely()
C16416a0.javaf5.a0ThreadCancellable worker thread (f40660a = cancel flag)
C16417b.javaf5.bHandler.CallbackFans cancel to all 5 worker threads; what==2 → start work after 5 what==2 messages
C16419c.javaf5.cExceptioncode 1 = NO_BACKUP, 2 = MISSING_FILES
C16420d.javaf5.dExceptionNetwork-to-Eyecon error (opaque)
C16427k.javaf5.kInterfaceC20992bDrive ProgressRequestInitializer adapter → f5.e.mo267g(int)
InterfaceC16421e.javaf5.einterfacevoid mo267g(int) progress callback
ViewOnClickListenerC16424h/i.javaf5.h, f5.iclick listenerssignOut / clear SP_KEY_STARTING_RESTORE_PROCESS / m31923u0()
ViewOnClickListenerC16435s/t.javaf5.s, f5.tclick listenerssignOut / m31944t0()
C16430n.java (15 KB)f5.nJADX artefactMerged ~15 unrelated library classes (Vungle, BottomSheet, Billing, f5 progress callback is only the tail)
C16440x.java (34 KB)f5.xJADX artefactMerged: Vungle InterstitialAdListener, ExoPlayer DataSource, Amplitude Api (https://api2.amplitude.com/2/httpapi), WebView, ShareTarget
3.1 WHAT IS COLLECTED (OBSERVED)h5

f5.f.f40666B (renamed from B) is the canonical list of Drive folders created:

public static final String[] f40666B = {
"eyecon_backup_for_ttm_audios", // TalkiToMe / IVR recordings (.zip)
"eyecon_backup_for_calls_records", // RecordedCalls -> .amr files
"eyecon_backup_for_notes_records", // RecordedNotes -> .amr files
"eyecon_backup_for_database", // contactdb -> application/x-sqlite3
"eyecon_backup_for_call_logs", // CallLog JSON -> application/json
"eyecon_backup_for_address_book" // Contacts JSON -> application/json
};

Per-thread source → sink (from f5.w = RunnableC16439w):

Thread nameCaseSourceSinkMIME
BU_ttm_thread1gf.AbstractC16733c.m32601u() (TTM dir) → AbstractC0060k0.m331u(...) ZIP<filesDir>/ttm.zipapplication/zip
BU_records_thread2e5.AbstractC16098a.m31460c(0) = Documents/Eyecon/RecordedNotes, m31460c(1) = Documents/Eyecon/RecordedCalls (MediaStore)one Drive object per AMR fileaudio/amr
BU_address_book_thread3af.C0125a.m559k(...) (getAddressBookContacts)<filesDir>/address_bookapplication/json
BU_database_thread4MyApplication.f12163h.getDatabasePath("contactdb") → AbstractC0085x.m456g0(dbPath, <filesDir>/database, progress)<filesDir>/databaseapplication/x-sqlite3
BU_call_log_thread5gf.AbstractC16733c.m32593k(...) → CallLog.Calls query<filesDir>/call_logsapplication/json

Call-log JSON columns read verbatim (gf.AbstractC16733c.m32593k, line 411):

contentResolver.query(CallLog.Calls.CONTENT_URI,
new String[]{"date","duration","number","type","countryiso",
"geocoded_location","presentation",
"subscription_component_name","subscription_id",
"data_usage","new","features"}, null, null, null)

⇒ real phone numbers + timestamps + durations + geocoded location + which SIM/subscription are serialised to JSON and uploaded.

Address-book JSON: af.C0125a.m539A(...) → full contact list (C18416a log tag AddressBookAgent).

3.2 ENCRYPTION — CORRECTION TO THE BRIEFINGh5

BRIEFING CLAIM (from C1): “f7.f0.m32019n wraps the backup key with RSA/ECB PKCS1v1.5 and encrypts content with Blowfish/ECB, no IV, no AEAD; the public key is pinned in strings.xml.” VERDICT: that crypto exists in the APK, but it is NOT used for backup files. It is used to encode error text before it is sent to the Eyecon error endpoint. The backup payloads are not encrypted at all.

Proof (OBSERVED):

  1. The database payload is produced by p008a7/AbstractC0085x.java:183 — real name a7.g0, m456g0(File,File,InterfaceC16421e):
public static void m456g0(File file, File file2, InterfaceC16421e interfaceC16421e) throws IOException {
float fMax = Math.max(1.0f, file.length());
byte[] bArr = new byte[1024];
FileInputStream fileInputStream = new FileInputStream(file);
FileOutputStream fileOutputStream = new FileOutputStream(file2);
… fileOutputStream.write(bArr, 0, i3); // byte-for-byte copy
interfaceC16421e.mo267g((int)((f3 / fMax) * 100.0f)); // progress only
}

No Cipher, no stream cipher, no key. It is a plain copy. 2. The upload MIME type is application/x-sqlite3, i.e. the Drive object is a raw SQLite file. 3. On restore, f5.r.mo31914b opens the downloaded temp file directly as SQLite:

C0041b c0041b3 = new C0041b(MyApplication.f12163h, "eyecon_restore_temp_database", null, 57);
C16491s c16491sM10122z = DBContacts.m10122z(c0041b3);

An encrypted blob cannot be opened by SQLiteDatabase. ⇒ Not encrypted. 4. The RSA+Blowfish routine (p284f7/AbstractC16468f0.java:354, real f7.f0.m32019n) is called from exactly one place in f5 — f5.f.m31912a(Throwable, Context, String):

KeyGenerator kg = KeyGenerator.getInstance("AES"); kg.init(256);
byte[] encoded = kg.generateKey().getEncoded();
SecretKeySpec ks = new SecretKeySpec(encoded, "Blowfish"); // <-- 32-byte AES key used as Blowfish key
Cipher c1 = Cipher.getInstance("Blowfish"); c1.init(1 /*ENCRYPT*/, ks);
String blowfishB64 = Base64.encodeToString(c1.doFinal(str.getBytes(UTF_8)), 0);
PublicKey pk = KeyFactory.getInstance("RSA")
.generatePublic(new X509EncodedKeySpec(Base64.decode(
MyApplication.m10343e().getString(R.string.pk_enc), 0))); // pinned pubkey
Cipher c2 = Cipher.getInstance("RSA/ECB/PKCS1Padding"); c2.init(1, pk);
return base64(c2.doFinal(encoded)) + "#" + blowfishB64;

m31912a is the error-message scrubber: it converts a Throwable into {sanitisedStackTrace, userFacingMessage, reasonCode, bucket} for the error report. So:

  • Blowfish/ECB with a 32-byte key (Blowfish key is effectively truncated), no IV, no AEAD, identical keystream blocks ⇒ deterministic, ECB-patterns leak — but it applies to log/error strings, not user data files.
  • R.string.pk_enc public key is pinned in strings.xml — confirmed.
3.3 UPLOAD (OBSERVED)h5

f5.z.m31962h(f5.z) (static h):

  1. Create/resolve the six folders: C16418b0.m31906a("pending_temp_eyecon_backup_for_<X>") for each of the six names. m31906a creates a Drive folder with parents=["appDataFolder"], mimeType="application/vnd.google-apps.folder", and returns its id.
  2. Progress 1, then 6 parallel RunnableC16438v(..., case 1) tasks: delete stale pending_temp_<name> and fail_safe_<name> objects.
  3. Progress 2, then 6 case 0 tasks: if the canonical object is missing but fail_safe_<name> exists, rename the fail-safe back (m31910e).
  4. Progress 3, then start the 5 worker threads (BU_*).
  5. f5.z.mo31914b() at 95 %: rename pending_temp_<name> → canonical eyecon_backup_for_<name> for all six (parallel RunnableC3589a(…, 9)).
  6. Delete local staging files f40677j/f40679l/f40678k/f40680m (database / call_logs / address_book / ttm.zip).
  7. Report to the backend (this is the one place the weak crypto helper is used):
C11859p o = new C11859p();
o.m14609o(Long.valueOf(System.currentTimeMillis()), CampaignEx.JSON_KEY_ST_TS); // "ts"
o.m14611q("account", this.f40783C); // Google account e-mail
C21280h.m38440m(TypedValues.Custom.S_STRING, "backup_info", o.toString(), true, …);

then persist locally:

MyApplication.m10347i().m32076c(c11859p.toString(), "SP_KEY_LAST_TIME_BACKUP");

Analytics: AbstractC3464e.m3658i("Backup upload success") / new C3465f("Backup upload failed").m3672d("Has internet", …). 8. Drive transport details (f5.b0): new Drive.Builder(httpTransport, jsonFactory, credential).setApplicationName("Eyecon"); SSLContext.getInstance("TLS") + TrustManagerFactory.getInstance("PKIX") + GoogleUtils.m14483a() (the standard Google CA keystore) — no custom trust manager, no cert pinning, no cleartext. Credential = Android AccountManager token for oauth2: <granted scopes>. Upload chunk size mediaHttpUploader.f55741n = 262144 (256 KB).

  • Optional mTLS path gated on System.getenv("GOOGLE_API_USE_CLIENT_CERTIFICATE") — off by default on Android.
3.4 RESTORE (OBSERVED)h5

f5.r.m31936j(f5.r) (static j):

  1. f40747F = getDatabasePath("contactdb"); f40677j = new File(<db parent>, "eyecon_restore_temp_database").
  2. Local dirs prepared: <filesDir>/notes (wiped), <filesDir>/calls (wiped), <filesDir>/call_logs, <filesDir>/address_book.
  3. List appDataFolder (setSpaces("appDataFolder").setFields2("nextPageToken, files(id, name, size, parents)"), paginated).
  4. If empty → throw new C16419c("Selected account has no files", 1) ⇒ UI string backup_restore_no_backup_v2, reason NO_BACKUP, bucket 4.
  5. 5 × RunnableC3589a(…, 8) → m31934h(r, name) resolves fail_safe_<name>; if absent ⇒ C16419c(…, 2) ⇒ backup_restore_missing_files, reason MISSING_FILES, bucket 4.
  6. Resolve three subfolders: m31937k("database","eyecon_backup_for_database"), ("address_book", …), ("call_logs", …).
  7. Start 5 threads: BR_ttm_thread, BR_records_thread, BR_address_book_thread, BR_call_log_thread, BR_database_thread.
  8. f5.r.mo31914b() at 95 %: opens eyecon_restore_temp_database as a DB and merges row-by-row into the live DB via DBContacts.m10108g(...), matching on AbstractC0039a.f213e/f212d1/f216f (number / name / lookup key) and bump-revisioning via DBContacts.m10109h(...) + 1.0; recency/timestamp fields f154H, f215e1.

The filesystem-level renameTo swap — confirmed, but it lives in p261e6/RunnableC16126b.java case 4, not in f5:

File databasePath = MyApplication.f12163h.getDatabasePath("contactdb");
File file = new File(databasePath.getParentFile(), "eyecon_restore_temp_database"); // restored copy
File file2 = new File(databasePath.getParentFile(), "eyecon_temp_db"); // current DB
boolean zRenameTo = databasePath.renameTo(file2);
if (zRenameTo ? !file.renameTo(databasePath) : !zRenameTo) {
file2.renameTo(databasePath); // roll back on failure
}
File file5 = new File(databasePath.getParentFile(), "eyecon_restore_temp_database");
File file6 = new File(getFilesDir(), "call_logs");
File file7 = new File(getFilesDir(), "address_book");
file5.delete(); file7.delete(); file6.delete();
AbstractC0062l0.m352g1(new File(getFilesDir(), "calls"));
AbstractC0062l0.m352g1(new File(getFilesDir(), "notes"));
((RunnableC3503j) this.f39736b).run(); // -> ACTION_RESTART_AFTER_RECOVERY (f5.g case 0)

So the swap is contactdb → eyecon_temp_db, eyecon_restore_temp_database → contactdb, with rollback. INFERRED: the “swap” is a directory-level renameTo with no verification that the restored DB passed an integrity/schema check before the swap (the merge already happened, so a partially-merged DB would still be moved into place).

3.5 WHAT LEAKS IF INTERCEPTED (OBSERVED consequences)h5

If a Google account is compromised, or if the appDataFolder objects are obtained, the attacker gets all of the following in cleartext:

  1. The complete contactdb SQLite file — every contact: names, numbers, e-mail, physical addresses, birthdays, notes, caller-ID/spam tags, revision history.
  2. The full call log — every number called/received, direction (type), epoch date, duration, countryiso, geocoded_location, subscription_component_name/subscription_id (SIM), data_usage, new.
  3. Every recorded call (RecordedCalls) and voice note (RecordedNotes) as raw .amr audio uploaded individually.
  4. All TalkiToMe (IVR) recordings as a ttm.zip.
  5. The full address book as JSON.
  6. The Google account e-mail address ("account" key in backup_info).

Transport is TLS to Google with the standard CA bundle, so this is not an interception risk in the passive sense; it is a cloud-exposure / account-compromise risk. The passive-MITM risk is low. The high risk is that the payloads have zero application-layer encryption, so Google’s own storage, any Drive sharing/backup-of-backup feature, or a law-enforcement/insider request yields the raw data with no key to hold back.

3.6 Backup security findingsh5
SevFindingEvidence
HIGHBackup payloads uploaded unencrypted (raw SQLite + raw JSON + raw AMR audio) into the user’s Google Drive appDataFolder. No application-layer key, no envelope encryption.p282f5/RunnableC16439w.java:209-213 → AbstractC0085x.m456g0 (plain copy); MIME application/x-sqlite3; p282f5/C16434r.java:267 opens the restore copy directly as SQLite
HIGHDocuments/Eyecon/RecordedCalls + RecordedNotes audio, and full call log incl. geocoded location and SIM id, leave the device.p282f5/RunnableC16439w.java:128-167; gf/AbstractC16733c.java:411; p260e5/AbstractC16098a.java:35
MEDIUMThe briefing/C1 attribution of RSA-PKCS1+Blowfish-ECB to backup encryption is wrong: it is f5.f.m31912a’s error-text scrubber. Blowfish/ECB with a 32-byte AES key, no IV, deterministic, and RSA-ECB-PKCS1v1.5 key wrap are still weak; the pinned pubkey is R.string.pk_enc.p284f7/AbstractC16468f0.java:354-366; p282f5/AbstractC16422f.java m31912a
MEDIUMTwo-step folder dance (pending_temp_* → rename → fail_safe_* fallback) leaves at most two generations of sensitive data in the cloud, and a failed run can leave the fail_safe_ copy behind indefinitely.p282f5/RunnableC16438v.java cases 0/1; p282f5/C16434r.java:68-75
MEDIUMRestore is a blind renameTo swap with rollback only on rename failure — no schema/integrity gate on the merged DB before it becomes contactdb.p261e6/RunnableC16126b.java:124-150
LOWGoogle OAuth drive.appdata scope requested and checked on every entry (m31913c); token never leaves the Android keystore/AccountManager.p282f5/AbstractC16422f.java m31913c

4. Package ol — Glide image down-sampler — THIRD-PARTY, 2 filesh4

Disk fileReal classKindPurpose
C19143a.javaol.aimmutable result(Bitmap bitmap, nl.C18870a orientation) or (Exception). Mirrors Glide Downsampler.DecodeResult
AsyncTaskC19144b.javaol.bAsyncTaskGlide BitmapLoader/Downsampler equivalent: fetch (http/https via OkHttp, else copy content:///file://), BitmapFactory inSampleSize halving, 100 MB cap, EXIF orientation rotate/flip

Key facts (OBSERVED):

  • ml.InterfaceC18606a = ml.a = callback { void mo34915a(Bitmap, nl.C18870a, String, String); void onFailure(Exception); } → Glide BitmapLoadCallback.
  • nl.C18870a = nl.a = {int orientation; int rotationDegrees; boolean flipHorizontally;} → Glide Orientation.
  • EXIF parsed by pl.C19372b(InputStream).m35701a() → returns Orientation constants 2/3/4/5/6/7/8; maps to 180/90/270 rotations — Glide ExifInterface.
  • Http client: p407ll.C18399k.f46325b lazy Object initialised to p466oq.C19227c0 (OkHttp OkHttpClient) — Glide’s HttpUrlFetcher/Internal holder. Requests wrapped in com.google.firebase.perf.network.FirebasePerfOkHttpClient and androidx.webkit.ProxyConfig.MATCH_HTTP used as the “http” scheme constant.
  • 100 MB pixel budget: if (bitmap.getByteCount() > 104857600) { options.inSampleSize *= 2; } — Glide Downsampler.DOWNLOAD_BUFFER_SIZE / MAX_SIZE behaviour.
  • No first-party MICO code in this package. The briefing’s “Photos” label is wrong.

5. Package jh — “Can You Talk” (CYT) — FIRST-PARTY, 4 filesh4

Correction: this package is not the Menifa feature. It is the Can You Talk SMS/WhatsApp read-receipt feature (com.eyecon.global CYT screens; CanTalkActivity referenced from RunnableC17750b case 27).

Disk fileReal classKindPurpose
C17626a.javajh.aimmutable3 booleans. m33860a() returns (c || b) && a → AND-of-OR gating for a CYT eligibility decision
C17627b.javajh.bimmutable payload(long timestamp, cm.C4014v contact, jh.a flags, double lat, double lon, int) — a CYT request/record with two double fields (location-like)
InterfaceC17628c.javajh.cinterfaceC17627b mo33861n(C21851f request, JSONObject response)
C17629d.javajh.dwidget controllerThe “Can you talk” bubble UI: status text + live timer

jh.d details (OBSERVED):

  • Constructor (View, p516r5.C19862i contact, p034b7.AbstractActivityC3498e activity), binds R.id.TV_status, R.id.TV_time; view id resolved to colour -111 (R.color.light_main_color).
  • m33862a() — send the CYT question:
    • picks EnumC19861h0.WHATSAPP if m36616c() else EnumC19861h0.SMS;
    • target = C19862i.m36633o() (preferred) else m36631m();
    • validates the number through p399l7.C18312b.m34437h().m34449n(c19864j.cli); if invalid → dialog "can_you_talk" + "support_only_valid_number", analytics "can_talk_failed_dialog";
    • SMS path: p008a7.AbstractC0062l0.m344M1(activity, cli, R.string.can_talk_question, true);
    • WhatsApp path: p516r5.C19863i0.m36666v(activity, cli, R.string.can_talk_question, null);
    • analytics AbstractC16257t1.m31645n("CanTalk", enumC19861h0).
  • m33863b(int status) — delivery status → UI text, with local wall-clock timer rendered as HH:mm from Calendar.get(11)/get(12): 1 → R.string.sent, 2 → arrived, 3 → seen, 4 → yep, 5 → failed.
  • Pref/key surface: none beyond resource strings. No endpoints, no secrets.
  • Security note (MEDIUM): jh.b carries two double fields (f44553d, f44554d) that are populated alongside a contact and a timestamp. From jh.d alone it is INFERRED (not proven) that these are latitude/longitude — i.e. the location of the person who was asked. Treat as unconfirmed.

6. Package n7 — PhotoPicker / contact-photo UI — FIRST-PARTY, 27 filesh4

Correction: the briefing assigns n7 to MoreMenuAndSettings. It actually belongs to Photos (com.eyecon.global.Photos.PhotoPicker.PhotoPickerActivity) and is one of the largest genuinely first-party packages in this cluster. It is the code behind the “Adding photo / Camera / Gallery / Facebook” flow.

6.0 Class maph5
Disk fileReal classKindPurpose
C18716e.java (36 KB)n7.ep262e7.AbstractC16130b (Presenter)JADX merged artefact — see §6.1. Real n7 part = a 5 000 ms timeout presenter n7.e(C19062o) + the photo-picker callbacks mo308k/mo309m
C18734w.javan7.wC3878j (DialogFragment)Source-chooser sheet: new C3877i(R.string.Camera, R.drawable.ic_camera_colored, …) + R.string.gallery + R.string.facebook
C18730s.javan7.sRecyclerView.AdapterPhoto grid; m35060b() returns a WeakReference<PhotoPickerActivity>
RunnableC18728q.javan7.qRunnableCrop/transform result → EyeAvatar.setPhotoAndRescaleWhenNeeded(bitmap)
ViewOnClickListenerC18729r.javan7.rRecyclerView.ViewHolder + OnClickListenerSingle photo cell (R.id.EA_photo), pager setCurrentItem, m10424I0(1)
RunnableC18718g.javan7.gRunnableApplies a selected list (f12467v0) into PhotoPickerActivity.f12438Q0
RunnableC18720i.javan7.iRunnableWrites the postal address of every chosen contact into the system Contacts provider
C18714c.javan7.cml.a (Glide callback)mo34915a(bitmap, orientation, src, dst) → persist + notify
C18730s/RunnableC18713b.javan7.bRunnableintent.setData(Uri.fromParts("package", pkg, null)); startActivityForResult(intent, 123) → takePictureFromCamera(null)
RunnableC18715d.javan7.dRunnablePermission gate: AbstractC22312g.m39774g0(activity, strArr) then CAMERA check → takePictureFromCamera(null)
C18711a.javan7.aAbstractC3872d (DialogFragment)5-second auto-dismiss confirmation dialog, R.layout.confirmation_dialog, Lottie LAV_anim max frame 45
C18721j.javan7.jRecyclerView.ItemDecorationGrid spacing
ViewOnTouchListenerC18724m.javan7.mOnTouchListenerZoom rect getGlobalVisibleRect
C18722k.javan7.kandroidx.lifecycle.ObserverLiveData of fetched photo bitmaps (p009a8.C0089a.f385a) → RunnableC3380e(…, 3)
RunnableC18726o.javan7.oRunnableFinish + m10416C0(activity, strArr) + adapter refresh
C18725n.javan7.nViewPager.OnPageChangeListenerPage tracking
C18719h.javan7.hFileFilterfile.getName().startsWith("survey_activity_temp_pic_") / file.isDirectory()
C18723l.javan7.lGridLayoutManager.SpanSizeLookupSpan sizing
C18727p.javan7.pRecyclerView.OnScrollListenerScroll paging
C18712a0.javan7.a0ComparablePhoto item: {boolean a, String b..f, EnumC18737z g}
C18730s/EnumC18737z.javan7.zenum holderPhoto-source / result discriminator (f47503a = default)
DialogInterfaceOnDismissListenerC18717f.javan7.fOnDismissListenerDialog cleanup
C18735x.javan7.xAbstractC16130bSecond presenter stub (empty bodies m35055q/r/s)
C18736y.javan7.yclass1-field holder
RunnableC18731t/32u/33v.java, RunnableC18715d.javan7.t/u/vRunnablesSheet / page / camera callbacks
6.1 n7.e is a JADX merge artefact (important caveat)h5

n7.e.mo308k() / mo309m() are giant switch blocks whose cases belong to at least nine unrelated original classes: PhotoPickerActivity, MainFragment, PhoneNumberValidationActivity, RegistrationActivity, TtmWindowActivity, HistoryFragment, C20361r, C20684e (ad loader, remote key loadAdTimeOutSmsMS), ViewOnClickListenerC20876b, SmsWindowActivity. Its three private methods m35055q/r/s are empty. The genuine n7 logic is:

public C18716e(C19062o c19062o) { super(true, 5000L); this.f47426f = 2; this.f47427g = c19062o; }
// case 0 in mo309m: ((PhotoPickerActivity) obj).m10423H0((String) m32061b(), 200, EnumC18737z.f47503a);
// case 0 in mo308k: ((PhotoPickerActivity) obj).m10423H0("", ((Integer) m32061b()).intValue(), EnumC18737z.f47503a);
// case 1 in mo309m: C21280h.m38431d(c19862i.m36624e(), photoBytes, "bad", new String[1]);

⇒ THIRD callout: n7 also leaks a WEAKFERENCE-style photo payload to w6.h.m38431d(ArrayList<C19862i>, byte[] photo, "bad", String[]) — the app’s “send contact photo to Eyecon” path.

6.2 Photo capture → crop → storage → encryption → upload (full trace)h5

Step 1 — SOURCE SELECTION (n7.w, R.string.select_photo): three tiles —

  • Camera → n7.b → intent.setData(Uri.fromParts("package", pkg, null)); startActivityForResult(intent, 123) → PhotoPickerActivity.takePictureFromCamera(null)
  • gallery → Storage/ACTION_PICK (handler in the merged n7.e)
  • facebook → R.string.facebook, R.string.com_facebook_loading, R.string.already_photos (“You’ve already added Facebook photos”), R.string.alert_no_facebook_connection_msg, endpoint prefix https://graph.facebook.com/

Step 2 — PERMISSION (n7.d): AbstractC22312g.m39774g0(activity, perms) (the PermissionsHelper state machine of §7) then ContextCompat.checkSelfPermission(..., "android.permission.CAMERA").

Step 3 — DECODE + DOWNSCALE — Glide (ol.b AsyncTaskC19144b), §4. Target bounds come from m10423H0(str, 200, EnumC18737z.f47503a) ⇒ the picker asks for a 200 px thumbnail. EXIF orientation applied (nl.C18870a).

Step 4 — CROP / SCALE — n7.q → EyeAvatar.setPhotoAndRescaleWhenNeeded(bitmap); n7.m uses getGlobalVisibleRect to compute the zoom crop rectangle.

Step 5 — PERSIST — n7.c.mo34915a(bitmap, orientation, srcPath, dstPath):

  • if photoPickerActivity.f12448c0 (new-contact mode): NewContactActivity.f11749G0 = bitmap; intent.putExtra("cis", f12443X); m10425J0(-1, extras); finish(); — no disk write.
  • else: show dialog R.string.adding_photo / R.string.please_wait, then C16945i.m32774f(DBContacts.f11708K, 0, new RunnableC20062p(dBContacts, contactKey, RunnableC18375q(7,…), arr, bitmap, list, null)) — asynchronous DB write on the contacts executor. Failure → toast R.string.saving_photo_went_wrong.

Step 6 — STORAGE + ENCRYPTION (the Blowfish part) — p420m7.C18517d → real m7.d, executor log tag "ContactsPhotosStorage":

public static File m34710a(MyApplication myApplication) { // m7.d.a
String strM3069p = AbstractC2489a.m3069p(sb2, File.separator, "Eyecon", File.separator, "ContactsPhotos");
if (AbstractC0042b0.m178a0()) return new File(MyApplication.f12172q, "ContactsPhotos"); // external mounted
return new File(myApplication.getFilesDir().getAbsolutePath() + strM3069p); // internal
}

MyApplication.f12172q (OBSERVED, com/eyecon/global/Others/MyApplication.java:467) =

f12172q = new File(Environment.getExternalStoragePublicDirectory(Environment.DIRECTORY_DOCUMENTS), "Eyecon");

⇒ on any device where external storage reports mounted (pre-scoped-storage, or legacy opt-in) contact photos are written to the WORLD-READABLE shared path Documents/Eyecon/ContactsPhotos.

Key derivation (OBSERVED, p379k5/RunnableC17750b.java case 10):

String string = MyApplication.f12163h.getString(R.string.cipher_key); // "Ewu"
String strValueOf = (String) SharedPreferencesC16497y.m32082b("", "cipher_key"); // SP "cipher_key"
if (isEmpty(strValueOf)) {
strValueOf = String.valueOf(new Random().nextInt(RoomDatabase.MAX_BIND_PARAMETER_CNT)); // 0..999
MyApplication.m10347i().m32076c(strValueOf, "cipher_key");
}
SecretKeySpec secretKeySpec = new SecretKeySpec((string + strValueOf + "sTQHGK").getBytes(), "Blowfish");
Cipher.getInstance("Blowfish").init(1, secretKeySpec); // ENCRYPT
Cipher cipher = Cipher.getInstance("Blowfish");
cipher.init(2, secretKeySpec); // DECRYPT
c18517d.f46765a = cipher; c18517d.f46767c = true;
  • R.string.cipher_key = Ewu (res/values/strings.xml).
  • Effective key material = "Ewu" + <0..999> + "sTQHGK" ⇒ at most 1000 distinct keys, all trivially brute-forceable, and the whole search space is derivable from the APK + one SharedPreferences read.
  • Cipher.getInstance("Blowfish") ⇒ Blowfish/ECB/PKCS5Padding, no IV, no AEAD — deterministic, pattern-leaking, no integrity.
  • m7.d.m34711b() (b) = delete every file in the storage dir — used by v6.c "delete_ab_photos" / "delete_ab_photos_alert_msg".

Step 7 — SYNC / UPLOAD — uploadpic.jsp IS THE PROFILE PICTURE, NOT CONTACT PHOTOS. Correction (explicit). The briefing’s “Photos / contact-photo handling: … upload via uploadpic.jsp” is wrong. OBSERVED, p620w6/C21280h.java (= w6.h, log tag "Server"):

f56466b = AbstractC0039a.f168L1; // "https://api.eyecon-app.com/app/"
f56468d = AbstractC0037a.m129q(new StringBuilder(), f56466b, "uploadpic.jsp");
f56467c = AbstractC1574a.m1345l(str, "join.jsp");

and w6.h.m38441n() (n):

if (!MyApplication.m10348j().getBoolean("SP_KEY_NEED_UPDATE_PROFILE_PIC", false)) return -1;
String str = (String) SharedPreferencesC16497y.m32082b(null, "clientId"); // <-- key of the form field
if (!AbstractC9039h.m10665v()) return -1; // must be a logged-in Eyecon user
File file = new File(MyApplication.f12163h.getFilesDir(), AbstractC0039a.f201a); // "/.Eyecon/"
File file2 = new File(file, "temp_photo.jpg");
if (!file2.exists() || (bitmap = AbstractC18543s.m34744e(file2.getPath())) == null) return -1;
HttpURLConnection con = (HttpURLConnection) FirebasePerfUrlConnection.instrument(new URL(f56468d).openConnection());
con.setRequestMethod("POST");
con.setRequestProperty("Content-Type", "multipart/form-data;boundary=*****");
C21273a.m38427k(con); // adds e-auth headers
DataOutputStream dos = new DataOutputStream(con.getOutputStream());
dos.writeBytes("--*****\r\nContent-Disposition: form-data; name=\"msisdn\"\r\n\r\n" + str + "\r\n");
bitmap.compress(Bitmap.CompressFormat.JPEG, 90, dos);
dos.writeBytes("\r\n--*****--\r\n");
…
if (con.getResponseCode() == 200) { …putBoolean("SP_KEY_NEED_UPDATE_PROFILE_PIC", false)… }

⇒ POST https://api.eyecon-app.com/app/uploadpic.jsp, multipart/form-data, form field msisdn = the Eyecon clientId, body = a raw JPEG of the user’s own Eyecon profile picture at <filesDir>/.Eyecon/temp_photo.jpg. Pref keys: SP_KEY_NEED_UPDATE_PROFILE_PIC, clientId. (AbstractC0039a.f201a = File.separator + ".Eyecon" + separator and f204b = that + "temp_photo.jpg" — the briefing’s claim about those two constants is CONFIRMED, at p008a7/AbstractC0039a.java:459-460.)

Contact photos do not use uploadpic.jsp. They go through the ordinary contacts pipeline: w6.h.m38428a(String contactsJson, boolean init) → POST https://api.eyecon-app.com/app/addcontacts.jsp with form fields contacts, cli_cc (SIM country ISO), init, possible_langs, aps_version = "vc_749_vn_4.2026.04.23.1145_a", plus getcontactslang.jsp and join.jsp. INFERRED: the contact-photo bytes are inside the contacts JSON body (via C19862i.m36624e()), not a separate multipart upload. Transport is HTTPS (f168L1 = "https://api.eyecon-app.com/app/"), so there is no cleartext on the photo-upload path — a correction to any “cleartext photo upload” claim.

6.3 Contact side-effect (security-relevant)h5

n7.i (RunnableC18720i, default branch) writes the chosen postal address into the system Contacts provider for every selected contact:

Uri uri = ContactsContract.Data.CONTENT_URI;
// UPDATE
ContentProviderOperation.newUpdate(uri)
.withSelection("mimetype = ? AND contact_id = ? ",
new String[]{"vnd.android.cursor.item/postal-address_v2", c19862i.contact_id})
.withValue("data1", str).build()
// or INSERT when no postal-address row exists
ContentProviderOperation.newInsert(uri)
.withValue("raw_contact_id", c19862i.primary_raw_id)
.withValue("mimetype", "vnd.android.cursor.item/postal-address_v2")
.withValue("data1", str).build()
MyApplication.f12163h.getContentResolver().applyBatch("com.android.contacts", …)

and n7.i case 0 calls C21280h.m38431d(ArrayList<C19862i>, String, "yes", new String[1]) — a server confirmation that the photo/address was accepted. ⇒ Writing to the user’s real address book is a side effect of a “photo” flow; it requires WRITE_CONTACTS (declared) but is easy to miss in a consent flow.

6.4 Photos security findingsh5
SevFindingEvidence
HIGHContact photos written Blowfish/ECB, no IV with a key of ≤1000 possible values ("Ewu" + SP cipher_key 0..999 + "sTQHGK") — fully brute-forceable offline, and deterministic.p420m7/C18517d.java (m7.d), p379k5/RunnableC17750b.java:400-415, strings.xml:cipher_key = "Ewu"
HIGHOn devices with Environment.getExternalStorageState() == "mounted" and legacy storage, contact photos land in the shared, world-readable Documents/Eyecon/ContactsPhotos.p420m7/C18517d.java m34710a; MyApplication.java:467
MEDIUMPhoto flow writes the postal address into the user’s system Contacts (applyBatch("com.android.contacts")) — a WRITE_CONTACTS side effect inside a “choose a photo” UI.p432n7/RunnableC18720i.java
MEDIUMuploadpic.jsp posts a raw JPEG of the user’s profile picture with msisdn = clientId; endpoint is server-selectable via AbstractC0039a.f168L1. HTTPS today, but the base URL is a mutable static field.p620w6/C21280h.java:437-497; p008a7/AbstractC0039a.java:602
LOWFacebook photo import hits https://graph.facebook.com/; facebook_app_id = 1716660955257637.p432n7/C18730s, strings.xml

7. Package v6 — “More menu” / About / Settings entry hub — FIRST-PARTY, 22 filesh4

v6 is the controller layer behind MoreSettingsFragment, SettingActivity, AboutActivity. It is mostly UI wiring + navigation + analytics, not business logic.

Disk fileReal classKindPurpose
C20875a0.java (31 KB)v6.a0p262e7.AbstractC16130b (Presenter)The main “More/Settings” presenter: ad request builder (m39100n(HashMap, Object, boolean, String, ArrayList, callback)), theme purchase (SP_KEY_WAITING_FOR_CUSTOM_THEME_PENDING_PURCHASE, R.string.product_preview_option, R.string.add_your_own_theme), singular/Singular SDK events, Photos.PhotosTrackerWorker, AfterCall.BaseAfterCallActivity, Themes.CustomThemePreviewActivity, Toki.TokiContactsChooserActivity, Contacts.Database.DBContacts. Also loads an ArrayList<Map> into a HashMap and publishes AtomicBooleans.
C20898x.java (16 KB)v6.xmulti-interface hubImplements InterfaceC9123k, OnAdManagerAdViewLoadedListener, InterfaceC10638a, InterfaceC20639p, InterfaceC22228k, InterfaceC21855j, ActivityResultCallback, InterfaceC22216a — the ad-manager/activity-result glue for the settings screen
C20881g.javav6.gC3878j (DialogFragment)Consent dialogs, incl. R.string.ask_me_for_every_call (CustomCheckbox) and R.string.sms_permission_pre_prompt_msg
RunnableC20888n.javav6.nRunnable”Do you like Eyecon?” social sheet (R.id.EIB_facebook/EIB_instagram/EIB_tiktok, R.string.do_you_like, R.string.got_it), full-screen caller-ID explainer (R.string.caller_id, R.string.full_screen_caller_id_is_on), Backup entry (AbstractC9039h.m10634N(activity, R.string.backup, "Backup", null))
RunnableC20880f.javav6.fRunnable”No face” avatar-animation tutorial (R.string.no_face_anim_text5, R.id.goSocial, R.id.copy_facebook, R.id.copy_instagram)
RunnableC20899y.javav6.yRunnableR.string.choose_default_sim dialog (R.id.EET_name), R.string.edit_busy_message / save / cancel, FAQ dialog → https://faq.eyecon-app.com/, R.string.removeMessage, R.string.shortcut_already_exist
ViewOnClickListenerC20876b.javav6.bclick listenerDelete-account confirmation (R.string.delete_account, R.string.delete_account_msg), privacy policy → https://www.eyecon-app.com/docs/private_policy.html, accessibility statement (R.string.accessibility_statement(_msg)), About links → https://www.eyecon-app.com, https://www.instagram.com/eyeconapp/, https://www.youtube.com/channel/UCYI84aNWZ6Q0TxaCVTjiaFQ, EULA → https://www.eyecon-app.com/docs/eula.html
ViewOnClickListenerC20877c.javav6.cclick listenerR.string.delete_ab_photos / delete_ab_photos_alert_msg → m7.d.m34711b() (wipes Eyecon/ContactsPhotos); EULA; https://twitter.com/EyeconApp; https://www.facebook.com/eyeconapp
ViewOnClickListenerC20889o.javav6.oclick listenerOpens https://www.facebook.com/eyeconapp, https://www.tiktok.com/@eyecon.app (×2 paths: ACTION_VIEW + AbstractActivityC3498e.m3705y0)
ViewOnTouchListenerC20891q.javav6.qtouch listenerScroll/gesture
RunnableC20892r.javav6.rRunnableCaller-ID explainer (same strings as v6.n)
C20883i.javav6.iListAdapterOpen-source-licenses screen: R.id.TV_artifact / TV_license / TV_title
C20884j.javav6.jDiffUtil.ItemCallbackLicense-list diffing
RunnableC20874a.javav6.aRunnableNavigates the license screen
C20885k / C20886l / C20887m.javav6.k / l / mAbstractC21478j coroutine scopesAsync ad/banner lifetimes (InterfaceC16320o)
C20897w.javav6.wTextWatcherName/label field
C20878d.javav6.dInterfaceC18794bLifecycle hook
C20882h.javav6.hempty classPlaceholder (133 bytes)
RunnableC20896v / RunnableC20893s.javav6.v / sRunnablesMisc callbacks

Endpoints / keys touched by v6 (all hardcoded, all HTTPS): https://www.eyecon-app.com, /docs/private_policy.html, /docs/eula.html, https://faq.eyecon-app.com/, https://www.facebook.com/eyeconapp, https://www.instagram.com/eyeconapp/, https://www.tiktok.com/@eyecon.app, https://twitter.com/EyeconApp, https://www.youtube.com/channel/UCYI84aNWZ6Q0TxaCVTjiaFQ, https://graph.facebook.com/. Keys: SP_KEY_WAITING_FOR_CUSTOM_THEME_PENDING_PURCHASE, "spam_page", "spam_pager". Theme strings confirm the Themes module is driven from here: R.string.product_preview_option, R.string.add_your_own_theme, com.eyecon.global.Themes.CustomThemePreviewActivity. No secrets, no crypto, no PII egress in v6. Ad-SDK classes (v6.x, v6.k/l/m, v6.a0’s m39100n) are thin wrappers only.


8. Packages ri, p8, a9 — all THREE are mis-filed; none is the advertised first-party moduleh4

8.1 ri = Firebase Cloud Messaging internals — THIRD-PARTY (Google), 22 filesh5

Correction: the briefing labels ri “MoreMenuAndSettings”. It is in fact com.google.firebase.messaging’s internal service layer.

Disk fileReal classKindIdentification evidence (OBSERVED)
AbstractServiceC19926g.javari.gabstract ServiceLog.isLoggable("EnhancedIntentService", 3) → EnhancedIntentService; unconfigurableExecutorService(threadPoolExecutor); field f51430b = ri.v Binder
C19925f.javari.ffinal classextends-style wrapper over com.google.firebase.messaging.FirebaseMessagingService; remote flag "gcm.n.noui"; KeyguardManager.inKeyguardRestrictedInputMode()
C19937r.javari.rfinal class"SERVICE_NOT_AVAILABLE", "INTERNAL_SERVER_ERROR", "TOO_MANY_SUBSCRIBERS", bundle.putString("gcm.topic", "/topics/" + str) → InstanceId/TopicManager subscribe
RunnableC19939t.javari.tRunnablenewWakeLock(1, "wake:com.google.firebase.messaging") + ConnectivityManager → FCM heartbeat
ServiceConnectionC19943x.javari.xServiceConnectionConnectionTracker.getInstance().bindService(..., 65); if (iBinder instanceof BinderC19941v)
BinderC19941v.javari.vBinderif (Binder.getCallingUid() != Process.myUid()) → same-process guard
AbstractC19940u.javari.uabstractcontext.startService(intent) → ProcessIntentHelper
C19930k.javari.kCloseableBlocking queue close
C19935p.javari.pfinal classSharedPreferences + ScheduledThreadExecutorService → FCM task scheduling prefs
C19938s.javari.sBroadcastReceiverBoot/package-replace receiver
C19920a/b/c.javari.a/b/cInterfaceC19134c implsTaskCompletionSource completions
C19929j.javari.jOnSuccessListener—
C19932m.java (45 KB)ri.mInterfaceC20435w1 + ViewBinding + MBSplashShowListenerJADX merge artefact: also contains KeyGenParameterSpec/GCMParameterSpec/Keystore and Mintegral MBSplashShowListener code
C19927h/C19933n/C19934o/C19942w/C19931l/C19923d/C19924eri.h/n/o/w/l/d/ehelpersTask/service plumbing

Nothing first-party. No MICO endpoints, no PII handling. FCM is nonetheless the transport for the CYT (cantalk_q/y/n/c), heart, authentication, share, ptt and deferred deep-link push events (see §12), so the data it carries is first-party even though the code is Google’s.

8.2 p8 = Facebook SDK + OkHttp + a connectivity helper — THIRD-PARTY / mixed artefact, 6 filesh5

Correction: the briefing labels p8 “MoreMenuAndSettings”. It is not.

Disk fileReal classWhat it actually is
AbstractC19327e.java (40 KB)p8.eJADX merge of ≥4 originals. m35632A(AccessToken, Uri, …) builds GraphRequest to "me/staging_resources" (Facebook BitmapUploader). m35633B(Throwable) is RxJava2 RxJavaPlugins.onError (UndeliverableException, OnErrorNotImplementedException, MissingBackpressureException, CompositeException). m35634C(String) parses an OkHttp query string into C21165z (= HttpUrl). m35645b(Context) is a network-availability check (NetworkCapabilities.hasCapability(12) = NET_CAPABILITY_VALIDATED) used by as.u2 (TalkiServer) and as.AbstractC3329q2.
C19326d.javap8.dFacebook AppEventsConstants-style registry: f49674c = ["fb_mobile_complete_registration","fb_mobile_add_to_cart","fb_mobile_purchase","fb_mobile_initiated_checkout"]; f49675d = ["address","health"] (Facebook restricted-data categories); f49673b ConcurrentHashMap of pending tasks
C19324b.javap8.bFacebook task/FetchResult helper
C19328f.javap8.fInterfaceC18606a-like/observer
C19325c.javap8.cSmall builder
C19323a.javap8.aGraphics matrix holder (int[3] + float[]), used by p8.e.m35646c — pulled in from a rendering/VAST library

Take-aways (OBSERVED):

  • Facebook Audience Network / App Events are integrated with app id 1716660955257637 (R.string.facebook_app_id) and restricted-data categories address, health declared.
  • p8.e.m35634C/p8.e is also the base class of zq.b, OkHttp’s platform/SSL trust-manager helper — confirming p8 and zq share one third-party namespace.
  • Only first-party-ish content in the package: pref key SP_KEY_IS_MISSED_CALL_NOTIFICATION_ENABLED (referenced from p8 scope).
8.3 a9 = one giant merged Runnable — MIXED, 1 fileh5

p010a9/RunnableC0096a.java (15 KB) has no renamed from header — the whole file is a JADX synthetic. It merges at least:

  1. Post-registration flow (com.eyecon.global.Registration.RegistrationActivity): m10528P0(…, R.string.oops_, …), m10544e1(false), m10506B0(), m10550k0(), f12997x0.m34274K(activity).
  2. Settings consent toggles (com.eyecon.global.MoreMenuAndSettings.SettingActivity): SettingActivity.m10331H0(key, !checkboxValue) + MyApplication.m10347i().m32077d(key, !checkboxValue) + m31543W(activity, key) → deep link eyecon://idPlus?source=<key>.
  3. FairBid banner (MediationRequest, BannerWrapper, InternalBannerOptions, AdDisplay, FetchOptions, SettableFuture).
  4. InMobi (C13000l2, C13085q2, C13102r2) and Vungle (VungleWebClient, WebViewAPI) imports.

Genuine first-party content: the onboarding consent checkbox → SP key (dynamic) + eyecon://idPlus deep link. Nothing else. Classification: MIXED — mostly third-party ad plumbing with a thin MICO settings-consent payload.


9. Package dn — Moloco / Crackle ad-event + VAST layer — THIRD-PARTY, 13 filesh4

Disk fileReal classKindPurpose
AbstractC15989e.javadn.eabstract baseAd-request builder: ArrayList f39223a, int f39224b, boolean f39225c — an ORTB/Vast request accumulator
AbstractC15990f.javadn.fabstractimplements InterfaceC19424c0 — ad-provider contract
C15997m.javadn.mdn.fProvider impl A
C15986b.javadn.bdn.fProvider impl B
C15996l.javadn.lInterfaceC20758d, InterfaceC21472dListener pair (impression / completion)
C15995k.javadn.kInterfaceC21472d, InterfaceC20758dSecond listener pair
C15988d.javadn.dfinal classSmall request/response value
C15987c.javadn.cextends ThrowableAd error
C15985a.javadn.aAbstractC21471c (coroutine Job)Job wrapper
C15994j.java / C15992h.java / C15993i.javadn.j/h/iextends AbstractC16733cSmall helpers / constants
AbstractC15991g.javadn.gabstractMarker

Third-party sibling packages (not in C5 scope but referenced): p275ep (22 files), p587uo (11), p636wo (10, AbstractC21471c = coroutine CoroutineScope used app-wide), p609vo, p484po, p421m8, p506qo, p263e8, p261e6 — all tech.crackle.* / com.moloco.sdk.* internals. No first-party MICO code. No endpoints or keys of its own. The only ad-relevant string that lands in the Ads-labelled file is R.string.conference_call_management (in ts, see §10).


10. Package ts — Moloco Crackle rewarded video + OMID viewability — THIRD-PARTY, 7 filesh4

Disk fileReal classKindPurpose
C20542a.javats.aAbstractC21478j + InterfaceC16320oOwns a tech.crackle.cracklertbsdk.vast.ShowFullScreenVideo; builds an IAB OMID AdSession + AdEvents (InteractionType, MediaEvents) to certify viewability of the rewarded video
C20547f.javats.ffinal classcom.iab.omid.library.crackletech.Omid, AdSessionConfiguration, AdSessionContext, CreativeType, ImpressionType, Owner, Partner, VerificationScriptResource — OMID partner/verification-resource plumbing
ViewOnClickListenerC20544c.javats.cOnClickListenerRewarded-video close button; pushes a C19399k(now, "closeLinear") event to the VAST tracker (C20444m0)
C20545d.java / C20546e.javats.d / ts.eAbstractC21471cCoroutine jobs (play/close)
C20548g.javats.gAbstractC17896r + Function1Suspend builder
AbstractC20543b.javats.babstractMarker

Resource string referenced: R.string.conference_call_management (confirmation dialog for managing conference calls — pulled into this third-party file by JADX). No first-party logic. Note the OMID partner verificationResources are downloaded from a third-party URL at runtime — INFERRED standard OMID behaviour.


11. Package u4 = AdsIB — MICO’s own in-banner ad loader, built ON FairBid — MIXED, 31 filesh4

u4 is the only package in this cluster that is a genuinely first-party ad module that wraps a third-party SDK. It is the “IB” (in-banner) loader: MICO’s own AbstractC20588f0 (FairBid-like placement descriptor), C20585e (ad unit), C20586e0 (entry), C20608z (the loader, u4.z) plus the FairBid NetworkAdapter implementations MICO ships for its own mediation.

Disk fileReal classKindPurpose
C20608z.java (27 KB)u4.zfinal classThe IB ad loader. FairBidListenerHandler, NetworkAdapter, MediationStartedListener, EnumC10001w0 (FairBid FetchOptions), C21417a(6)/C21466a(6) thread pools, CopyOnWriteArraySet of live units, Map<ViewGroup, u4.v>; container slot label "ib_native"; View lifecycle (attach/detach) awareness
C20601s.javau4.sfinal classIn-flight load record (Context, u4.z, u4.w, p484po.C19404p) + CancellationException handling
C20604v.javau4.vfinal classPer-container binding: (ViewGroup, u4.e unit, EnumC20594l format, String adKey, WeakReference host, attach listener); m37684a() = tear-down
C20605w.javau4.wAbstractC21478j + InterfaceC16320oLoad coroutine
C20606x.java / C20607y.javau4.x / u4.ysameRender coroutines
C20600r.javau4.rsame + Function1Dispatch coroutine
C20591i.java / C20592j.javau4.i / u4.jsameInit / ensure-is-initialised coroutines
AbstractC20588f0.javau4.f0abstract ComparableAd placement/format descriptor
AbstractC20585e.javau4.eabstractBase ad unit (u4.b, u4.c, u4.d derive from it)
C20584d0.javau4.d0extends AbstractQueueFairBid request queue
C20587f.javau4.fextends HashMapWaterfall map
C20580b0.javau4.b0ComparableSortable unit
C20586e0.javau4.e0final classLoader entry
C20578a0.javau4.a0final classPlacement registry
C20579b/C20581c/C20583du4.b/c/du4.eAd-type variants
EnumC20577a.javau4.aenum holderFormat enum
EnumC20589g/g/h/k/l.javau4.g/h/k/lenumsu4.l is the only real enum — placement formats (INTERSTITIAL, REWARDED, BANNER, …)
InterfaceC20582c0.java / InterfaceC20595m.javau4.c0 / u4.minterfacesIB host callbacks
AbstractC20585e/C20598p/C20599qu4.p, u4.qAbstractC21471cCoroutine scopes
RunnableC20596n.java / RunnableC20602t.javau4.n / u4.tRunnablesCallbacks
RunnableC20597o.java (14 KB)u4.osynthetic class RunnableFirst-party Server case-runner. case 6 is the TTM/voice2text call: POST https://voice2text.eyecon-app.com/start-ttm with JSON {mycli, acli, calltag, purchases} (AbstractC9039h.m10649f() = own CLI), guarded by AbstractC9039h.m10665v() (“is Eyecon user”). Imports also InMobi (InMobiSdk, SdkInitializationListener, AdConfig), Amazon APS (ApsMetricsDataMap), FairBid, Facebook, Photos.PhotosTrackerWorker.
11.1 What triggers an ad refresh (OBSERVED)h5
  • Master switch: remote flag use_IB_ad_loading (boolean, AbstractC15804d.m31081b).
  • Refresh cadence: remote ac_ib_refresh_time_ms (long ms, AbstractC15804d.m31089j); when <= 0 the loader refreshes once via C20608z.m37685a(unit), otherwise it schedules a C20604v re-bind into the Map<ViewGroup, u4.v>.
  • Init: u4.v.m37687c(Context) → if use_IB_ad_loading and not yet initialised, C16941e.m32763a(new C20591i(value, this, null)) → m37692h("ensureIsInitialized") → new com.fyber.fairbid.g00(13, this, str).
  • Container slot is labelled "ib_native" (AbstractC19051d.m35339a(view, "ib_native")), i.e. the ad is inserted at index 0 of the host container.
  • Dyn-area (cards) ad slot: remote native_ad_dynamic_area (p033b6.C3468a) selects between MICO’s own native card and an ad.
  • Screens carrying ads in this cluster: Settings (v6.x, v6.k/l/m, a9), More-menu (v6.n), AfterCall (v6.a0 references BaseAfterCallActivity), DynamicArea (p033b6), SMS window (remote key **loadAdTimeOutSmsMS**), and Toki (banner in z7`).
11.2 Ad networks present (classification — depth not required)h5
NetworkEvidence
Google AdMob / GAMR.string.admob_app_id = ca-app-pub-2959190743815944~6092014645; MobileAds.getRequestConfiguration(); MediationAdLoadCallback; adapter classes com.google.ads.mediation.* incl. FacebookMediationAdapter, VungleMediationAdapter, MintegralMediationAdapter; fb_ts_mediation_network_status_only_admob_missing_configuration (FairBid↔AdMob interop)
Facebook Audience Networkcom.facebook.appevents.AbstractC9039h, com.google.ads.mediation.facebook.FacebookMediationAdapter, facebook_app_id = 1716660955257637, p8.d AppEvents
Fyber FairBid / Inneractivecom.fyber.fairbid.* everywhere, com.fyber.inneractive.sdk.external.InneractiveMediationDefs/MediationNameConsts, fb_ts_sdk_version = "FairBid SDK %s", MintegralAdapter
Pangle / ByteDancecom.bytedance.sdk.openadsdk.TTAdConstant, com.bytedance.sdk.component.pglcrypt.PglCryptUtils, com.google.ads.mediation.pangle
InMobicom.inmobi.sdk.InMobiSdk, com.inmobi.media.*, com.inmobi.commons.core.configs.AdConfig
Amazon APSR.string.amazon_app_key = 905c0238-13dd-4cf9-af3d-90f64672a4a9, com.fyber.fairbid.adapters.APSAdapter, com.amazon.aps.shared.metrics.model.ApsMetricsDataMap
Vungle / Liftoffcom.vungle.ads.*, com.vungle.mediation.VungleInterstitialAdapter, com.vungle.ads.internal.ui.VungleWebClient
Mintegral (Mbridge)com.mbridge.msdk.*, MintegralAdapter, MintegralMediationAdapter, ~30 mbridge_* strings
AppLovinR.string.applovin_key = UZvHsQBqsDCf0qv_yVDKn0UeTmRknnQSgTT3HP7HFikqs8IxdD-7uWdloUQVGTn09CQ0LThxLm6tfMnKDFS9xa
Moloco / Crackletech.crackle.*, com.moloco.sdk.*, com.iab.omid.library.crackletech.*, R.string.crackle_key = fai=1:917722272265:android:af67d7515574ed8c689ed8~~~fak=AIzaSyBxXQfYa0khnAZ2-NiK3WQV-4JbMWCw6ls~~~fpi=eyecon---eyecon-caller-id~~~s5=417687~~~s6=234363
Singularcom.singular.sdk.Singular, com.singular.sdk.internal.Constants (used in v6.a0)
Amplitudehttps://api2.amplitude.com/2/httpapi / https://api.eu.amplitude.com/2/httpapi
Firebase Performance / CrashlyticsFirebasePerfOkHttpClient, FirebasePerfUrlConnection.instrument(...), com.google.firebase.crashlytics prefs
Play Billingcom.android.billingclient.api.Purchase, BillingClient bundle handling
AdjustR.string.adjust_app_token = m23t9uwrtvy8
AppVestorR.string.app_vestor_api_token = 980d4e3f-d057-4e5b-9034-1587edadf5a4, R.string.app_vestor_app_key = 8c830f45-9f86-48d3-bcca-4e14fe2c1129
11.3 What user data reaches ad networks (OBSERVED, partial)h5
  • No user PII (contacts, call log, numbers, photos, location) is assembled by any class in u4, ts, dn, a9 or p8. These are all thin SDK glue.
  • COPPA / child-directed handling exists but is advisory only: p274eo.C16303b:976 → MintegralMediationAdapter.f21500b.setCoppaStatus(context, MobileAds.getRequestConfiguration().getTagForChildDirectedTreatment() == 1), and ir.AbstractC17448b:154 → new AdError(103, "MobileAds.getRequestConfiguration() indicates the user is a child. Pangle SDK V71 or higher does not support child users.", "com.google.ads.mediation.pangle"). INFERRED: Pangle is therefore not COPPA-safe for tagged-children traffic and AdMob mediation will refuse those requests.
  • Ephemeral identifiers that DO reach SDKs (standard, not MICO-specific): Android Advertising ID / OAID via each SDK, AppMeasurement, Singular events, Amplitude events, and MICO’s own analytics tags built from C3465f (AbstractC3464e.m3659j/3658i) — these carry feature/event names such as "Backup upload success", "Invisible Ad"/house_ad, "functional_permission_dialog_show", "IncomingShareMedia", "dialer_role_requested", "callerid_role_requested", "draw_above_granted", "thirdparty_read_granted", "no_caller_id_role_dialog_show". These are feature flags, not PII.
  • Firebase Performance instruments outbound HTTP app-wide: FirebasePerfUrlConnection.instrument(new URL(...).openConnection()) is used on the photo-upload and other MICO calls.

NOTE ON SECTION NUMBERS. The Table of Contents in section 0 was written before sections 12+ existed. Two forward-references in section 0 are stale and should be read as: item 7 (zq) -> section 12; item 8 (p8) -> section 8.2; section 6.2 -> the PermissionsHelper in section 12.1.

12. Package zq - PermissionsHelper (1 file) + OkHttp TLS platform aliases (13 files) - MIXED, 15 filesh4

Classification (final, verified file-by-file): 14 of the 15 files are THIRD-PARTY OkHttp 4.x (okhttp3.internal.platform.*) that JADX renamed into the zq namespace. Exactly one file - AbstractC22312g.java - is first-party MICO code, and it is the PermissionsHelper.

12.0 Third-party members (OkHttp) - identity proofh5
Disk fileReal classOkHttp class it isProof (OBSERVED)
C22306a.javazq.aokhttp3.internal.platform.Android10Platform / Android10SocketAdapterSSLSockets.isSupportedSocket, SSLSockets.setUseSessionTickets, isSupported() -> Build.VERSION.SDK_INT >= 29
C22307b.javazq.bokhttp3.internal.platform.Android10CertificateChainCleanerextends p477p8.AbstractC19327e (which is okhttp3.internal.tls.CertificateChainCleaner); X509TrustManagerExtensions.checkServerTrusted(chain, "RSA", hostname)
AbstractC22308c.javazq.cokhttp3.internal.platform.PlatformCopyOnWriteArraySet f61169a of loggers, Log.isLoggable, 4000-char log chunking; linkedHashMap.put(C19227c0.class.getName(), "okhttp.OkHttpClient")
C22309d.javazq.dokhttp3.internal.platform.OkHttpLogHandlerextends java.util.logging.Handler, maps INFO -> 4, above -> 5
C22310e.javazq.eokhttp3.internal.platform.Jdk9Platform / Jdk9SocketAdapterreflective getDeclaredMethod("setUseSessionTickets", Boolean.TYPE), getAlpnSelectedProtocol, setAlpnProtocols
C22311f.javazq.fokhttp3.internal.platform.BouncyCastlePlatformmo38653a returns false; delegates to zq.h
C22313h.javazq.hBouncyCastleJssePlatform / BouncyCastleJsseSocketAdapterorg.bouncycastle.jsse.provider.BouncyCastleJsseProvider, BCSSLSocket, BCSSLParameters
C22314i.javazq.iConscryptPlatformConscrypt.isConscrypt(sSLSocket)
C22316k.javazq.kConscryptSocketAdapterConscrypt.setUseSessionTickets, Conscrypt.setApplicationProtocols, version gate >= 2.1.0
AbstractC22315j.javazq.jConscrypt.version() shimmajor()/minor()/patch() comparison to 2.1.0
C22318m.javazq.mAndroidPlatform basesynchronized InterfaceC22319n m39800d(SSLSocket) lazy adapter selection
InterfaceC22317l.javazq.lSocketAdapterboolean mo38653a(SSLSocket), SocketAdapter mo38654b(SSLSocket)
InterfaceC22319n.javazq.nPlatform.SocketAdaptermo39731a/b/c + isSupported()
C22320o.javazq.oAndroidSocketAdapterempty extends C22310e (1-line stub)

No first-party logic. No MICO endpoints. No secrets. zq.b extends p8.e is the hard proof that p8 and zq are one third-party (OkHttp) namespace, as claimed in section 8.2.

Security-relevant (positive): there is no custom TrustManager, no HostnameVerifier, no certificate pinning, and no ALLOW_ALL_HOSTNAME_VERIFIER anywhere in zq. zq.b delegates to the platform X509TrustManagerExtensions. Standard TLS validation. Good.

12.1 zq.g = PermissionsHelper (first-party, 1369 lines, /* JADX INFO: renamed from: zq.g */)h5

This is a JADX merge artefact - it contains the whole first-party PermissionsHelper plus fragments of ~10 unrelated originals (kotlinx-serialization SerialDescriptor builders, OkHttp Charset/Duration helpers, Facebook AccessToken parsing, as.u2 connectivity, CrackleMediationAdView.destroy() tree-walk, InMobi, Mbridge, Amazon APS, Google Play Billing, DomExceptionUtils). The first-party methods are identifiable because they log under the tag "PermissionsHelper" and/or the tag "RoleDontAskMeAgainHand".

Key methods (all public static, OBSERVED):

MethodReal sigPurpose
m39735A(List)ArrayList A(List)Filter a permission list down to the not-yet-granted ones via ContextCompat.checkSelfPermission; on Throwable treat as denied.
m39736B()Intent B()Build the OEM auto-start / battery-whitelist Intent for 13 vendors. Sets SP SP_REG_AUTO_START_SHOWN=true when none matched.
m39737C()int C()Toki UDP port picker. Reads remote-config JSON key toki_ports (AbstractC15804d.m31091l("toki_ports", false)), expands "lo-hi" ranges, returns arrayList.get(new Random().nextInt(size)). Fallback on any exception: hard-coded 5620.
m39738D()boolean D()”Does this OEM need an auto-start prompt?” Cached in SP key Eyecon.isNeedAutoStart.
m39739E()Intent E()ACTION_REQUEST_IGNORE_BATTERY_OPTIMIZATIONS with package: URI.
m39740F()boolean F()PowerManager.isIgnoringBatteryOptimizations().
m39741G()boolean G()RoleManager.isRoleAvailable("android.app.role.CALL_SCREENING") (API 29+).
m39742H()boolean H()RoleManager.isRoleHeld("android.app.role.CALL_SCREENING").
m39743I()boolean I()Facebook AccessToken != null && !isExpired() (i.e. “FB connected”).
m39744J()boolean J()isRoleAvailable("android.app.role.DIALER") else canResolveActivity(CHANGE_DEFAULT_DIALER).
m39745K()boolean K()isRoleHeld(DIALER) else package-name comparison.
m39746L()boolean L()Settings.canDrawOverlays() with fallbacks (vendor AppOpsManager, m39781k).
m39747M(String) / m39748N(String...)single / all permissions granted.
m39749O()boolean O()J() && !K() = “dialer role is available but not held”.
m39750P()boolean P()API 34+ NotificationManager.canUseFullScreenIntent().
m39751Q()boolean Q()API 33+ POST_NOTIFICATIONS.
m39752R()boolean R()reads Settings.Secure "enabled_notification_listeners" and substring-matches own package (the Id+ notification reader).
m39753S(String...)boolean S(...)”never ask again” detector (see 12.3).
m39755U()int U()Xiaomi-only: reflect AppOpsManager.checkOpNoThrow(10008 = OP_AUTO_START, uid, pkg) -> 1/0/-1.
m39756V(AbstractActivityC3498e, Fragment, Intent)int V(...)Launch an OEM autostart screen; for Huawei ProtectActivity it falls back to Runtime.getRuntime().exec("am start -n com.huawei.systemmanager/.optimize.process.ProtectActivity --user " + userSerial). Request code 82.
m39757W(Fragment, String, boolean)W(...) -> m39759Y(source, "android.app.role.CALL_SCREENING", ...).
m39758X(Object, String[], int)void X(...)Filter to un-granted, fire analytics functional_permission_dialog_show (one event per permission, {"permission": <name>}), then ActivityCompat.requestPermissions / Fragment.requestPermissions; also calls AbstractActivityC3498e.m3717l0(false).
m39759Y(String source, String role, Object, boolean)void Y(...)Role-request router (see 12.2).
m39760Z(Activity, Fragment, int, boolean)boolean Z(...)Overlay-permission screen; MIUI alert_permission variant, else MANAGE_OVERLAY_PERMISSION; on failure shows m3722q0("", "DAR_1", null).
m39770e0(AppCompatActivity, Runnable)boolean e0(...)If SDK>=29 and remote flag should_show_caller_id_denied_dialog, shows the “caller ID denied” DialogFragment (R.layout.caller_id_denied_dialog_container_view, R.string.caller_id_denied_dialog(_button)) and fires analytics no_caller_id_role_dialog_show.
m39772F0(String[] granted, int[] results, String source)Batch grant bookkeeping -> m39768D0(permShortName, source, granted) per permission, then posts RunnableC16004e(25,...) to the AbstractC17527m.f44092b analytics executor.
m39773G() / m39794W()IntentNotification-listener settings (NOTIFICATION_LISTENER_DETAIL_SETTINGS API30+, legacy :settings:fragment_args_key) targeting com.eyecon.global.IdPlus.NotificationReader.NotificationReaderService; W() = MIUI AppPermissionsEditor prober (5 class names, then 4 more).
m39774G0(Activity, String...)void g0(...)The “never ask again” state machine (see 12.3).
m39776H0(List)void h0(...)When a permission becomes granted externally, flip its stored status out of the terminal 2 state.
m39790S()ArrayList s()PackageManager.getPackageInfo(pkg, GET_PERMISSIONS).requestedPermissions - the full declared permission list.
m39791T()String t()Human-readable autostart state ("Enabled"/"Disabled"/"required"/"required and click go"/"not required") - telemetry string.
m39792U()ArrayList u()The core permission bundle: READ_CONTACTS, WRITE_CONTACTS, GET_ACCOUNTS, READ_PHONE_STATE, CALL_PHONE, [API26+] ANSWER_PHONE_CALLS, READ_PHONE_NUMBERS, READ_CALL_LOG, WRITE_CALL_LOG, PROCESS_OUTGOING_CALLS.
m39796Y(String)String y(String)android.permission.FOO_BAR -> "foo_bar", else "unknown_permission".
m39767D(View, int)boolean d(View,int)Recursive CrackleAdView/CrackleMediationAdView.destroy() tree-walk (third-party code absorbed into this file).
m39771F(String, StringBuilder)JSON string escaper (\n->%0A, \r->%0D, "->%22).
m39775H .. m39797Xkotlin-serialization / OkHttp / Facebook helpers (third-party).

OEM autostart / power-whitelist targets (OBSERVED, m39736B): MIUI com.miui.securitycenter/...AutoStartManagementActivity; Huawei StartupNormalAppListActivity, StartupAppControlActivity, ProtectActivity; OPPO com.oppo.safe...StartupAppListActivity; ColorOS com.coloros.safecenter...; iQOO/vivo com.iqoo.secure.* and com.vivo.permissionmanager...BgStartUpManagerActivity; Letv com.letv.android.letvsafe.AutobootManageActivity; ASUS com.asus.mobilemanager.powersaver.PowerSaverSettings; Evenwell/Nokia com.evenwell.powersaving.g3...PowerSaverExceptionActivity.

12.2 Role-request routing (m39759Y) - OBSERVEDh5
analytics: role == "android.app.role.DIALER" ? "dialer_role_requested" : "callerid_role_requested" (source=<caller>)
if (z10 /*interactive*/ && RoleDontAskMeAgainHandler enabled && handler says "don't ask again")
-> Intent("android.settings.MANAGE_DEFAULT_APPS_SETTINGS") // send user to Settings
else if (role == DIALER)
-> SDK>=29 ? Intent("android.telecom.action.CHANGE_DEFAULT_DIALER")
.putExtra(EXTRA_CHANGE_DEFAULT_DIALER_PACKAGE_NAME, ownPkg)
: RoleManager.createRequestRoleIntent(DIALER)
else -> SDK>=29 ? Intent("android.settings.MANAGE_DEFAULT_APPS_SETTINGS")
: RoleManager.createRequestRoleIntent(CALL_SCREENING)
requestCode = (role == DIALER) ? 85 : 120

Note the SDK>=29 DIALER path uses the deprecated CHANGE_DEFAULT_DIALER action rather than RoleManager - i.e. MICO deliberately takes the “make default dialer” route on Android 10+ too (see section 12.5, consent finding).

12.3 The “never ask again” counter state machine (m39753S / m39774G0) - OBSERVEDh5

Two regimes, split on the originally-installed version SP key SP_KEY_ORIGNALY_INSTALL_VERSION (threshold 463):

Legacy (< 463) - SP key = the raw permission string, value "" or "never_ask_again_mode":

// m39774G0
if (SP_KEY_ORIGNALY_INSTALL_VERSION < 463) {
if (AbstractC16468f0.m32000J(strArr)) return; // all already granted
boolean show = ActivityCompat.shouldShowRequestPermissionRationale(activity, strArr[0]);
MyApplication.m10347i().m32076c(show ? "" : "never_ask_again_mode", strArr[0]);
return;
}

Modern (>= 463) - two derived SP keys per permission: "PS-" + permission (status int) and "PS-Counter-" + permission (int):

// m39774G0, modern branch
int old = SP.getInt("PS-" + p, 0);
if (!granted(p) && old != 2) {
int n = SP.getInt("PS-Counter-" + p, 0) + 1;
SP.putInt("PS-Counter-" + p, n);
if (n >= 5) SP.putInt("PS-" + p, 2); // terminal: stop asking
else if (old == 0) SP.putInt("PS-" + p, showRationale ? 1 : 0);
else if (old == 1) SP.putInt("PS-" + p, showRationale ? 1 : 2);
}
// m39753S
if (SP.getInt("PS-Counter-" + p, 0) > 4) return true; // >=5 attempts -> never ask again
if (SP.getInt("PS-" + p, 0) == 2) return true;

So after 5 request attempts for the same permission the app permanently marks it “never ask again” and switches the UI to the Settings deep link.

12.4 Permission-grant analytics (m39762A0 / m39764B0 / m39766C0 / m39768D0) - OBSERVEDh5
a0(source): m39768d0("draw_above", source, m39746L()) // + SP "DrawAbove"
b0(source): m39768d0("full_screen_permission", source, !m39750P()) // + SP "fullScreenNotification"
c0(source): m39768d0("thirdparty_read", source, m39752R()) // + SP "readNotification"
d0(event, source, granted):
Analytics.m3074u(event + (granted ? "_granted" : "_not_granted"), "source", source, false);
if (granted && AbstractC20656z.m37803a()) AbstractC20656z.m37805c();

Event names actually emitted: draw_above_granted / draw_above_not_granted, full_screen_permission_granted / _not_granted, thirdparty_read_granted / _not_granted, plus functional_permission_dialog_show and no_caller_id_role_dialog_show.

12.5 zq security findingsh5
SevFindingEvidence
MEDIUMThe Toki plaintext-audio destination port is remote-config controlled (toki_ports), and remote config is fetched over CLEAR-TEXT HTTP (http://cloudfront-cdn-all.eyecon-app.com/configs/ab-test.txt, AbstractC15804d:300). A network attacker can change toki_ports and redirect unencrypted microphone audio to an arbitrary port on the (also configurable, §2.2) host.p720zq/AbstractC22312g.java:221-252; p236d5/AbstractC15804d.java:300
MEDIUMThe app shells out via Runtime.exec("am start -n com.huawei.systemmanager/... --user <serial>") to bypass the normal startActivityForResult path for its app-protection screen.p720zq/AbstractC22312g.java:473
LOWOEM permission probing via PackageManager.resolveActivity on 14 foreign vendor package names and MIUI AppOpsManager reflection. Not exploitable, but it fingerprints the OEM and is a privacy signal.m39736B, m39755U, m39794w()
NONEConsent text is presented before any permission is requested, and the OK button only sets SP_KEY_DID_EYECON_CONSENT_APPROVED = true and finishes; actual grants happen afterwards. Ordering is correct.p047c7/ViewOnClickListenerC3880l.java case 10
NONEOkHttp platform layer has no TLS weakening. No custom TrustManager / HostnameVerifier / cert pinner anywhere in zq.zq.a..zq.o

13. com.eyecon.global.Toki - the Toki wrapper classes and the full lifecycle (4 files, all FIRST-PARTY)h4

Section 2 covered the obfuscated Toki package z7 and cross-referenced C6a for wl/yl. This section covers the four thin first-party wrappers and stitches the whole feature together end-to-end, answering the assignment’s flow #1.

13.0 Class maph5
FileKindSizeRole
TokiActivity.javaActivity extends AbstractActivityC3498e3.1 KBHost activity. Two intent actions only: INTENT_ACTION_START_TOKI, INTENT_ACTION_REFOCUS_TOKI.
TokiService.javaService implements SensorEventListener5.2 KBForeground service + proximity-sensor monitor that emits com.eyecon.global.isonear.
TokiContactsChooserActivity.javaActivity extends AbstractActivityC3498e11.4 KBMulti-select contact picker used by Toki (and other “share/can-talk/toki” flows).
ClearFocusOnBackEditText.javaCustomEditText subclass1.2 KBonKeyPreIme clears focus on BACK(4)/ENTER(6); performClick() swallows Throwable.
13.1 Flow #1, step by step - the complete Toki lifecycleh5

STEP 1 - ENTRY (user taps the Toki action on a contact). z7.v0.m39382O0(...) (C22122v0.java:640-697) is the single entry point. For a 1<1> call it goes through m39390W0(MyApplication, cis, cli, private_name, type, source) (C22122v0.java:757-764):

Intent i = new Intent(app, TokiActivity.class);
i.setAction("INTENT_ACTION_START_TOKI");
i.addFlags(268435456 /*FLAG_ACTIVITY_NEW_TASK*/);
i.addFlags(65536 /*FLAG_ACTIVITY_SINGLE_TOP*/);
i.putExtras(bundle { INTENT_KEY_CLI, INTENT_KEY_CIS, INTENT_KEY_TYPE, INTENT_KEY_NAME, INTENT_KEY_SOURCE });

For a group/conference Toki it uses m39389V0(source, contact) -> {INTENT_KEY_PEOPLE_JSON = c19862i.participantsJson, INTENT_KEY_TYPE=0, INTENT_KEY_CIS, INTENT_KEY_SOURCE} and sets the action explicitly (C22122v0.java:685-690). Gating before entry (OBSERVED): remote flag isTokiEnableForIphone must be true when the callee is iOS (c19864j.osType == 2) else R.string.os_not_suppoort_wt; already in a TokiActivity -> re-target the existing fragment via m39386G1.

STEP 2 - TokiActivity.onCreate -> m10583C0(intent) (TokiActivity.java:16-47, 57-61).

setContentView(R.layout.activity_fragments);
m10583C0(getIntent());

m10583C0:

  • action INTENT_ACTION_REFOCUS_TOKI or intent flag FLAG_ACTIVITY_LAUNCHED_FROM_HISTORY (0x100000) -> try C22122v0.m39384W0(activity, null, null) (resume the existing fragment). If that fails: log "onNewAction canceled, unable to resume the toki" (+ the _after_launched_from_history variant) then finishAndRemoveTask(), and for the history case additionally startActivity(MainActivity.class).
  • any other action -> log "onNewAction canceled, unable to start toki action not supported = %s", finishAndRemoveTask().
  • action INTENT_ACTION_START_TOKI -> if a fragment tagged "TokiFragment" already exists (m39380C0()), replace(...) + hide(...) + show(...); else C22122v0.m39386G1(this, intent.getExtras()). onNewIntent re-runs the same dispatcher, so the service notification’s refocus action reuses the same task (FLAG_ACTIVITY_SINGLE_TOP).

STEP 3 - z7.v0.mo10006K0(Bundle) (Fragment view creation) starts the service. C22122v0.java:2157-2177:

Intent i = new Intent(context, TokiService.class);
if (Build.VERSION.SDK_INT >= 26) {
if (TokiService.f13076b == null) AbstractC0046d0.m190I(i, "START_TOKI_SERVICE"); // ContextCompat.startForegroundService
else TokiService.f13076b.onStartCommand(i, 0, 0); // reuse singleton
} else {
context.startService(i);
}
m39426t0(false);
f60449c0 = true;
f60417B0 = MyApplication.m10348j().getBoolean("clicked_toki_btn_v2", false);

AbstractC0046d0.m190I = ContextCompat.startForegroundService (so the service must call startForeground within 5 s - it does, in onCreate).

STEP 4 - TokiService.onCreate() -> startForeground(29, ...) + proximity listener. TokiService.java:44-84:

m10586a(); // builds and posts the foreground notification
SensorManager sm = (SensorManager) MyApplication.f12163h.getSystemService("sensor");
Sensor prox = sm.getDefaultSensor(8); // TYPE_PROXYIMITY
if (prox != null) f13078a = prox.getMaximumRange();
sm.registerListener(this, prox, 3); // SENSOR_DELAY_NORMAL

m10586a() (TokiService.java:44-61):

if (26 <= Build.VERSION.SDK_INT) AbstractC18372n.m34531s("toki_service", 2, "Eyecon Toki", false); // create notification channel
NotificationCompat.Builder b = new NotificationCompat.Builder(this, "toki_service");
Intent i = new Intent(this, TokiActivity.class)
.addFlags(268435456).addFlags(65536)
.setAction("INTENT_ACTION_REFOCUS_TOKI");
PendingIntent pi = PendingIntent.getActivity(this, 777, i, FLAG_IMMUTABLE /*0x4000000 via m185D*/);
b.setSmallIcon(R.drawable.ic_eyecon_icon_shape)
.setLargeIcon(AbstractC18543s.m34745f(R.drawable.ic_eyecon_icon_shape))
.setContentIntent(pi)
.setVisibility(1 /*VISIBILITY_PUBLIC*/) // <-- see finding below
.setContentText(getString(R.string.toki_noti_text))
.setContentTitle("Eyecon");
try { startForeground(29, b.build()); } catch (Exception e) { FacebookAppEvents.logException(e); }

Foreground-service type id 29 = FOREGROUND_SERVICE_TYPE_MICROPHONE (API 30+ constant). INFERRED (strong): the notification is VISIBILITY_PUBLIC and the FGS type is microphone, i.e. this is Android 14’s “microphone in use” FGS. See finding 13.3. onStartCommand re-posts the notification and then bails out if no live Toki fragment exists:

if (C22122v0.m39381H0()) return START_STICKY /*1*/; // any fragment with G0 != 9
stopSelf(); return START_STICKY /*1*/;

STEP 5 - TokiService.onSensorChanged -> proximity broadcast (TokiService.java:96-118).

float v = event.values[0];
if (event.sensor.getType() == 8) { // TYPE_PROXYIMITY
if (v < f13078a) { // something is near the earpiece
if (f13077c) return; // already "near", no re-broadcast
f13077c = true;
sendBroadcast(new Intent("com.eyecon.global.isonear").putExtra("isonear", true)
.setPackage(MyApplication.f12163h.getPackageName()));
} else {
if (f13077c) {
f13077c = false;
sendBroadcast(new Intent("com.eyecon.global.isonear").putExtra("isonear", false)
.setPackage(...));
}
}
}

setPackage(own) means the broadcast cannot be spoofed by other apps. Good. onDestroy unregisters the listener and nulls the static singleton.

STEP 6 - z7.p0 (C22110p0, the BroadcastReceiver registered in mo10007M0) consumes it. C22122v0.java:2147-2151:

f60451e0 = new C22110p0(this, i11); // synthetic case index
IntentFilter f = new IntentFilter();
f.addAction("EYECON.NOTIFICATION_ANSWER_CLICK_ACTION");
f.addAction("EYECON.NOTIFICATION_NOT_NOW_CLICK_ACTION");
ContextCompat.registerReceiver(MyApplication.f12163h, f60451e0, f, 4 /*RECEIVER_NOT_EXPORTED*/);

C22110p0.onReceive (C22110p0.java:32-57):

case 0: // com.eyecon.global.isonear
isonear == false ? C22122v0.m39399L0() // pulled away from ear -> (stop / reset)
: C22122v0.m39400M0(); // held to ear -> (arm talk-to-ears)
default: // notification action
AbstractC0077t.m416A0(30, null); // 30 s screen-off watchdog
"EYECON.NOTIFICATION_NOT_NOW_CLICK_ACTION" -> m39401N0();
"EYECON.NOTIFICATION_ANSWER_CLICK_ACTION" -> { if (!m39395G0()) { m39404R0(); post(RunnableC21876d(this,17)); } m39409Y0(); }

ContextCompat.registerReceiver(..., RECEIVER_NOT_EXPORTED) - the invite-answer receiver is not reachable by other apps. Good. m39395G0() is the RECORD_AUDIO gate (see §2.1).

STEP 7 - RECORDING. m39404R0() is reached from exactly three places (§2.1 + this one):

CallerTrigger
z7.l0 (ViewOnTouchListenerC22102l0) case defaultR.id.IV_talk ACTION_DOWN (0) -> m39404R0(), ACTION_UP (1) -> m39403Q0() - classic press-and-hold
z7.p0 case defaultSingle tap on the incoming-Toki notification’s answer button - no press-and-hold
p457t9.C20641qthe “hold to ear to answer” accessibility/animation path

CORRECTION / REFINEMENT to section 2.1. The briefing asks whether Toki “streams continuously or on demand”. The answer is on demand, and that now has a tighter proof than “case 11 has two call sites”:

// z7.l0, ViewOnTouchListenerC22102l0.java:30-41
int action = motionEvent.getAction();
if (action == 0) { if (z7.v0.f60438R) z7.v0.m39403Q0(); z7.v0.m39404R0(); } // DOWN -> start
else if (action == 1) { z7.v0.m39403Q0(); } // UP -> stop
return true; // consumes the event

So the mic can only run between an ACTION_DOWN and the matching ACTION_UP/ACTION_CANCEL, because m39403Q0() is the only other entry and it is invoked from ACTION_UP and from the “was already pressed” guard on ACTION_DOWN. There is no timer, no service, and no broadcast anywhere that calls m39404R0() on its own. Nuance worth recording: the notification-answer path (EYECON.NOTIFICATION_ANSWER_CLICK_ACTION) starts recording with one tap and no release event - in that case the stop depends on the remote end (TALK_DONE / SESSION_STATUS) or on the user opening the app and pressing again. INFERRED (the stop path from a notification-answered session is not a literal ACTION_UP).

STEP 8 - TEARDOWN. z7.v0 teardown (C22122v0.java:2826-2834):

MyApplication.f12161f = null;
m39407U0(false);
if (TokiService.f13076b != null) {
((SensorManager) MyApplication.f12163h.getSystemService("sensor")).unregisterListener(TokiService.f13076b);
TokiService.f13076b.stopSelf();
}
MyApplication.f12163h.stopService(new Intent(MyApplication.f12163h, TokiService.class));

Before that, it fires a survey dialog (C22128y0 "tokiQualitySurvey" if SP SP_KEY_DID_LOVE_SURVEY_SHOWN, else C22086d0 "tokiDoYouLike").

13.2 TokiContactsChooserActivity - the shared contact chooserh5

Not Toki-exclusive; it is the app-wide multi-select contact chooser (used by Toki, CanTalk and share flows).

Field / memberTypePurpose
f13052q0, f13053r0static HashMap(1)r0 maps a request key -> InterfaceC16133e (p262e7, the caller’s filter/callback) so the chooser can call back without holding a strong ref.
f13054UdoubleINTENT_KEY_REQUEST_KEY (default -1.0) - the lookup key into f13053r0.
f13055VStringINTENT_KEY_CONTACT_CLI - pre-selected CLI.
f13056WintINTENT_KEY_REQUEST_CODE.
f13057XInterfaceC16133eThe caller’s filter; null -> RuntimeException("originalFilter is null") + finish().
f13063d0 / f13064e0C22093h / C22097j (z7.h/z7.j)Two LruCache-backed adapters (contacts + selected). evictAll() in onDestroy.
f13065f0/f13066g0RecyclerViewRV_contacts_selector, RV_selected_contacts.
f13071l0C21867v (y5.v)Progress/sharing dialog; receives onActivityResult for request codes 89 and 100 (C21867v.m39116t(intent)).
f13072m0C3892xBlocking progress dialog; if a "mWaitingDialog" fragment survives a config change it is removed and the resume runnable is re-posted.
f13073n0HashSetINTENT_KEY_PRE_SELECTED_CONTACTS (serializable).
f13074o0booleanInsets applied once.
f13058Y / f13060a0ArrayListfull contact list / filtered-visible list.
f13062c0Stringcurrent lower-cased search text.
f13075p0intmonotonically increasing search-generation counter used to discard stale Thread results.

Search: m10584C0(activity, str) lower-cases + trims, bumps the generation counter, and on a non-empty query spawns new Thread(new RunnableC17180l0(activity, gen, strTrim, snapshot, 7)); on empty it copies the unfiltered list into the adapter and calls m10585D0() (fades TV_no_contacts). onBackPressed clears a non-empty query instead of finishing. Layout/theme: R.layout.activity_contacts_chooser_old, background R.drawable.ic_launcher_white_icon_blue_bg_background + new C22111q() (z7.q), search icons R.drawable.toki_old_search_icon, toki_old_microphone_search, R.drawable.close_white, range selector C22101l (z7.l) over tolki_old_thumb_drawable / toki_old_line_drawable. No endpoints, no pref keys, no secrets. f13059Z = new C22119u() is the Toki sound/talk-mode enum instance reused for the “click” feedback. Data read: it reads contacts from the local DBContacts.f11707J executor (RunnableC20051e(7, db, new C20875a0(18, this, false))) - local only, nothing is uploaded by this class.

13.3 Toki wrapper-class security findingsh5
SevFindingEvidence
MEDIUMThe Toki foreground notification uses VISIBILITY_PUBLIC on the toki_service channel (importance 2 = IMPORTANT_LOW). On the lock screen this can expose the fact that a live walkie-talkie session is running, and the app name/title “Eyecon” plus R.string.toki_noti_text is rendered by the system without redaction.com/eyecon/global/Toki/TokiService.java:55 (setVisibility(1))
MEDIUMAnswering a Toki invite from the notification starts the microphone with a single tap and no press-and-hold and no release event on that path; stopping then depends on the remote peer or on re-entering the app. Users who answer a push while the phone is not at their mouth will still transmit.p703z7/C22110p0.java:49-55; p703z7/C22122v0.java:1120-1192
LOWAbstractC16468f0.m32028w(getIntent()).getString("source", "Missing source") and several analytics events are emitted with the raw source string; clicked_toki_btn_v2, toki_ports, isTokiEnableForIphone are all server-controlled, so a remote-config attacker can change Toki’s on/off state per install cohort.Themes/CustomThemePreviewActivity pattern; z7.v0
NONE (positive)The proximity broadcast is setPackage(own); the invite-action receiver is registered with RECEIVER_NOT_EXPORTED; TokiService does not bind or export anything (onBind returns a local BinderC16946j).TokiService.java:107; C22122v0.java:2151
NONE (positive)RECORD_AUDIO is checked immediately before any recording (m39395G0), and refusal is persisted as "never_ask_again_mode" / PS-*=2.z7.g0; §12.3

Cross-reference: the audio wire format, the AMR encode/decode, the OAR/XOR FEC, the 15-byte control header, the CHANNEL_* JSON frames and the e-auth credential are documented in §2.2-2.3 of this file and, for the wl/yl internals, in cluster C6a. They are not repeated here.h2

14. Social Media Share - com.eyecon.global.SocialMediaShare.ShareMediaJob + obfuscated engine w7 - FIRST-PARTYh4

The first-party wrapper is a single 29-line JobService. All logic lives in p621w7 (w7, 12 files). w7 is outside the 14-package C5 scope but is the real implementation of the SocialMediaShare module the briefing names, so it is documented here.

14.1 ShareMediaJob (wrapper)h5
public class ShareMediaJob extends JobService {
onStartJob(JobParameters p) -> C18416a.o("ShareMediaJob","onStartJob");
C21295o.m38470l(new C19867k0(11, this, p)); // w7.o.l
return true;
onStopJob(JobParameters p) -> log; return true;
}

Return codes from C19867k0 case 11: success -> jobFinished(p, false); failure -> C18416a.o("ShareMediaJob","onStartJob#onFailed"); jobFinished(p, false) (note: false = do not reschedule, so a failed fetch is simply dropped until the next scheduled run).

14.2 w7 class map (all FIRST-PARTY MICO)h5
Disk fileReal classKindPurpose
C21295o.java (13 KB)w7.ofinal class (singleton f56545b, f56546a=true)The share engine. Endpoint construction, provider detection, WebView HTML builders, job entry point.
C21293m.javaw7.mmodelOne shared media item. Fields: a=url, b=id, c=share_ts, d=seen, e=cli, f/h=flags, g=alias, i=type, j=provider, k=?. ContentValues/JSONObject/Cursor marshalling.
C21296p.javaw7.pw7.m subclassTikTok-specific item (extra HTML field f56547l, f56548m = hasRawEmbed).
C21294n.javaw7.nsingleton f56542cLocal DB layer. Thread named "ShareVideoDB". Table table_share_media (in contactdb).
C21284d.javaw7.dsingleton f56488g”first card” cache refresher; log tag "MediaShareFirstCardC..".
C21289i.java (32 KB)w7.iDialogFragmentThe “ShareVideoHelper” modal (WebView + 3 CTAs).
DialogC21290j.javaw7.jDialogSmall confirm dialog.
RunnableC21281a.javaw7.aRunnableInjects the provider-specific JS video-URL scraper into the WebView.
C21282b.javaw7.bAbstractC16134f + @JavascriptInterfaceJS bridge: close(), onLoadedMetaData(), onVideoUrlFound(String), onPageSizeChanged(int).
C21283c.javaw7.cholderWebView + w7.m + provider + mediaType + generation for one card render.
EnumC21291k.javaw7.kenumTIKTOK("tiktok"), SHARE_CHAT("share_chat"), FACEBOOK("facebook"), INSTAGRAM("instagram").
EnumC21292l.javaw7.lenumVIDEO("video"), POST("post").
14.3 Flow #4 - what data is attached to shared media (step by step, OBSERVED)h5

STEP 1 - fetch. w7.o.m38470L(presenter) -> executor -> v6.f RunnableC20880f default branch:

if (!AbstractC9039h.m10665v()) { presenter.m31515h(); return; } // must be a logged-in Eyecon user
if (AbstractC15804d.m31086g("share_video_list_sync_interval") == -1) { presenter.m31515h(); return; }
int[] code = { -1 };
String body = C21273a.m38423g(new ArrayList(),
AbstractC0039a.f168L1 + "share", // "https://api.eyecon-app.com/app/share"
"GET", code, false, "mysettings", null, true, true, null, "");
// ... (e-auth headers are added by C21273a.m38423g / C21280h, see C1)
if (code[0] != 200) presenter.m31518o("Error code " + code[0]);
else if (!isEmpty(body)) presenter.m31518o(body);
else presenter.m31518o("Result is empty");

⇒ GET https://api.eyecon-app.com/app/share with source=mysettings and the standard e-auth header pair. HTTPS. Remote-config key share_video_list_sync_interval (-1 = feature disabled).

STEP 2 - parse. w7.o.m38463E(JSONObject) builds one w7.m per server object:

String id = j.getString("id");
String type = j.getString("type");
String cli = j.getString("cli"); // <-- the Eyecon client-id of the POSTER
String content = j.getString("content"); // <-- provider-specific payload blob
String provider = j.optString("provider", "");
long share_ts = j.getLong("share_ts");
m.f56533c = share_ts; m.f56532b = id; m.f56539i = AbstractC19949a.m36764b(type);
m.f56535e = cli; m.f56540j = EnumC21291k.valueOf(provider);

So the server tells the app: here is a URL, here is a content blob, here is the Eyecon CLI of the account that shared it, here is when. The app does not generate this content.

STEP 3 - persist. w7.n.m38457D(item) -> insertWithOnConflict("table_share_media", null, item.m38452a(), 4 /*CONFLICT_REPLACE*/). Columns written (w7.m.m38452A): url, provider, cli, two booleans, seen, share_ts, type, id, alias. Because table_share_media lives in the contactdb SQLite database, these server-supplied social posts are also carried inside the Google-Drive backup described in §3.

STEP 4 - filter for display. w7.n.m38454A(w7.n):

boolean instagramBlocked = remoteConfig("notSupportedKeys").getObj("instagram").contains("instagram");
for (item : list)
if (item.seen == 0 && (item.provider != INSTAGRAM || !instagramBlocked)) keep;

Remote-config key notSupportedKeys is a per-provider CSV of blocked keywords; w7.o.m38467I additionally blocks any item whose url contains one of the blocked substrings and shows the empty dialog.

STEP 5 - render. w7.o.m38469K(item, presenter):

ProviderWhat is rendered
TIKTOKFirst GET https://www.tiktok.com/oembed?url=<url> via C16942f HTTP executor; the returned embed HTML is loaded into the WebView (w7.p.f56547l) when hasRawEmbed, otherwise after the oembed call. mediaType = VIDEO.
FACEBOOKRuns w7.o.m38460B(text) (regex from remote-config key facebooke_Extract_URL_Regex) to extract the canonical FB URL, then w7.o.m38459A(height, url) builds <iframe src="https://www.facebook.com/plugins/post.php?href=<url-encoded>&width=auto&show_text=true&height=...">.
INSTAGRAM<blockquote class="instagram-media" data-instgrm-permalink="<url>" data-instgrm-version="12"><script async src="https://www.instagram.com/embed.js"></script></blockquote>.

STEP 6 - video-URL extraction (JS bridge). w7.a case 0 injects a provider-specific scraper; w7.b.onVideoUrlFound(src) receives it back:

  • Instagram: walks document + every iframe.contentDocument, queries video, audio, returns videos[0].src (or '').
  • TikTok: walks the same iframe set, queries [data-testid=play-video], returns videos[0].src.
  • Fallback (case default): dumps document.documentElement.outerHTML into w7.o.m38461C(text) with remote-config regex findFirstMp4UrlForInstagramRegex to extract the first .mp4 URL. w7.o.m38466H(url, isPost) then wraps the resolved CDN URL in a generated HTML5 <video> page that calls EyeconWebView.onLoadedMetaData() from onloadedmetadata and scrolls to clientHeight*0.25.

STEP 7 - user actions on the card. w7.i exposes R.id.EB_x (close), R.id.contacts, R.id.eyeconUsers, R.id.lottieWebView. Both contacts and eyeconUsers open com.eyecon.global.Contacts.ContactsChooserActivity:

i.putExtra("INTENT_KEY_REQUEST_KEY", iAddAndGet);
i.putExtra("eyecon.INTENT_KEY_SHOW_ABOVE_LOCK_SCREEN", true);
i.putExtra("INTENT_KEY_PRE_SELECTED_CONTACTS", hashSet);
i.putExtra("INTENT_KEY_REQUEST_CODE", 106);
i.putExtra("INTENT_KEY_UNIQUE_BEHAVIOR_MODE", c19870n.f51233d);
i.putExtra("INTENT_KEY_DONE_BUTTON_TEXT", R.string.share); // "Share"
i.putExtra("INTENT_KEY_TITLE_TEXT", R.string.share_video_to); // "Share video to"
startActivityForResult(i, 106);

STEP 8 - data actually attached to a shared item - SUMMARY

FieldSourceReaches third parties?
urlserverYes - the URL is loaded into a WebView which then iframes facebook.com / instagram.com / tiktok.com / the TikTok CDN, so those four origins see the client IP, UA, cookies and the fact that the user opened this post.
content blobserverYes - Facebook uses the fb_mobile_* AppEvents path (§8.2).
cli (poster’s Eyecon id)serverStays local (table_share_media.cli).
share_ts, seen, type, id, aliasserverStays local.
The user’s contactslocalOnly handed to ContactsChooserActivity for the OS/share-sheet flow; not sent to facebook/instagram/tiktok by any code in w7.
The user’s Eyecon CLIe-auth header on GET /app/shareSent to api.eyecon-app.com only (HTTPS).

Verdict: no PII exfiltration to facebook/instagram/tiktok was found in this flow. The exposure is (a) third-party origins learn the app opened a specific post, and (b) the remote config controls the URL-extraction regexes and the blocked-keyword lists.

14.4 Social-share security findingsh5
SevFindingEvidence
HIGHServer-driven arbitrary-URL rendering in a JavaScript-enabled WebView. The list, the URLs and the provider tags all come from GET /app/share. A compromised or MITM’d backend can therefore make the app load any URL in a JS_ENABLED WebView that also exposes a @JavascriptInterface bridge (close, onLoadedMetaData, onVideoUrlFound, onPageSizeChanged) plus the EyeconWebView JS object. Transport is HTTPS so passive MITM is out, but this is a full server-side content-injection channel.p598v6/RunnableC20880f.java:199-227; p621w7/C21295o.java:99-122, 210-232; p621w7/C21282b.java
MEDIUMThe two scraping regexes and the blocked-keyword lists are remote-config controlled and remote config is fetched over cleartext HTTP (AbstractC15804d:300).AbstractC15804d.java:272, 301
MEDIUMMenifaFragment.onDestroy broadcasts ACTION_RELOAD_SHARED_MEDIA_WEBVIEW; the receiver must be package-internal. (grep found the sender only in this cluster; the receiver is outside C5 scope)com/eyecon/global/Menifa/MenifaFragment.java:6887
LOWThe Facebook card is rendered with facebooke_Extract_URL_Regex scraped out of a blob, then injected unescaped into an iframe src. The value is URLEncoder.encoded first (m38459A), so this is not injectable.w7.o.m38459A
LOWw7.o.m38462D returns the provider home pages (https://www.tiktok.com, https://www.instagram.com, https://m.facebook.com) as ACTION_VIEW targets.w7.o.m38462D
NONE (positive)ShareMediaJob never touches contacts, call logs, photos or location.w7 + ShareMediaJob

15. com.eyecon.global.Themes, .Menifa, .Spam, .Permissions, .DynamicArea (all FIRST-PARTY)h4

15.1 Themes (3 files)h5
FileKindRole
CustomThemePreviewActivity.java (13 KB)Activity extends AbstractActivityC3498eCustom-theme paywall + preview + purchase
CustomThemeView.java (4.5 KB)FrameLayoutPhone-frame preview widget with a theme enum
ThemeException.javaExceptionMarker only

CustomThemePreviewActivity (OBSERVED):

  • Extras: "source" (default "Missing source"), used as the analytics Source dimension.
  • Remote-config keys: custom_background_theme_product_id (Play Billing product id), custom_theme_purchase_options (one of "only_premium" / "only_one_time" / "both|only_one_time" - controls which of EB_premium / EB_product is visible).
  • Purchase flow: new C16708j(productId, "inapp") -> C16707i.f41454h.m32534M(list, new C19867k0(18, ...)) (Play Billing BillingClient); the listener is stored in f13042X and torn down with C16707i.f41454h.m32535O(...) in onDestroy.
  • Photo swap: EB_change_photo -> ViewOnClickListenerC21874b(...,1); the resulting Bitmap is held in the static f13038c0 and applied via CustomThemeView.setThemeBackgroundBitmap(bitmap) -> RunnableC20908f(27, view, bitmap) (writes into the theme cache C21884l.f59019g / C16942f).
  • Analytics: "Custom_theme_preview_locked" (Clicked, Source), "Custom_theme_preview_unlocked" (Swap image, boolean).
  • SP key: SP_KEY_WAITING_FOR_CUSTOM_THEME_PENDING_PURCHASE (read in CustomThemeView.m10582c; a pending purchase forces the cached custom background).
  • Theme enum y7.j (EnumC21882j): SYSTEM_DEFAULT, CUSTOM_THEME, plus the built-in ones (field f59016f = colour res, f59017g = “is solid colour” flag).

Security findings (Themes): no PII, no endpoints of its own, no crypto. The photo used as a custom theme is stored locally in the app’s theme cache; OBSERVED: it is written through the same Eyecon/ContactsPhotos / theme cache path as ordinary photos and there is no encryption for it (theme bitmaps live in C16942f / C21884l, not in m7.d’s Blowfish store). LOW.

15.2 Menifa (1 file, 457 KB) - the Contacts tab, not a themed featureh5

CORRECTION: MenifaFragment is the main Contacts list / contact-details screen (R.layout.menifa_layout), not the “Themes”-adjacent feature the briefing implies. It implements InterfaceC20338h, InterfaceC20676t, InterfaceC21859n (InterfaceC21859n is the Dynamic-Area ViewPager2 page callback wired in §1).

OBSERVED structure:

  • 12 onClick handlers on distinct View.OnClickListener anonymous classes (f54614b captured in 12 separate synthetic classes, lines 1762 / 2192 / 2618 / 3044 / 3470 / 3896 / 4322 / 4749 / 5175 / 5601 / 6028 / 6454) - a JADX duplication artefact, each with a big switch.
  • Resource strings show the screen’s action set: add_contact, block, block_are_you_sure, block_number, call_details, cancel, change_xx, choose_phone, choose_sin_or_phone, contact_details, contact_settings, delete_contact, Do_you_want_to_schedule, edit_contact, edit_xx, favorite, more_options_title, no_contact_option_message, private_number, remove_xx_link, save, send_later, send_now, share_contact, sim_not_set, sms, unblock, unfavorite, view_contact_on, whatsapp.
  • R.id.contacts, R.id.eyeconUsers, R.id.LL_explanation, R.layout.bubble_layout, R.layout.dynamic_layout.
  • Toki entry (OBSERVED, 12 duplicated occurrences): if (AbstractC15804d.m31081b("isWalkieTalkieEnabled") || SP.getBoolean("SP_KEY_IS_TOKI_ENABLED", false)) -> enables EnumC19861h0.TOKI on the per-contact social-action row. This is the same predicate the Toki entry in §13.1 uses, i.e. Toki can be force-enabled for a cohort from the cleartext remote config.
  • Role-request counter: SP_KEY_RV_APPEARANCES_BEFORE_CALLER_ID_ROLE_REQUESTS - incremented once per appearance, and when it reaches the remote-config value AbstractC15804d.m31086g("...") the screen requests the CALL_SCREENING role (line 7084-7096).
  • Shared-media WebView: onDestroy sends ACTION_RELOAD_SHARED_MEDIA_WEBVIEW (§14.4).
  • CanTalk: imports p367jh.C17629d (jh.d) -> the “Can you talk” bubble from §5 is hosted here.
  • Cards: imports com.eyecon.global.DynamicArea.SwipeRefreshLayout, C3468a/C3469b/C3477j/C3479l (ad units), and RunnableC17219d1 -> the Dynamic-Area carousel of §15.5 is embedded in this screen.

Security findings (Menifa):

SevFindingEvidence
MEDIUMThe Toki entry point is remote-config gated (isWalkieTalkieEnabled over cleartext HTTP, §16.5). A network attacker can add/remove the walkie-talkie button for a cohort; combined with §2.2 they can also redirect the audio.MenifaFragment.java (12 occurrences)
LOWThe screen holds Bitmap f12092w plus a HashMap of contacts and a ConcurrentHashMap, and runs 4 ValueAnimators; all are released in onDestroy. No leak observed.MenifaFragment.java:6885-6916
LOWC19859g0 “social manager” enumeration includes TOKI, CAN_TALK, WhatsApp, SMS, Telegram, Facebook, Instagram and more; Regex matching of FB/IG profile URLs is remote-config driven (regexForAddSocial, AbstractC15804d:301).AbstractC15804d.java:301
15.3 Spam / Block (1 file)h5

SpamAndBlockFragment extends AbstractC15860d, R.layout.fragments_spam_and_block. A two-tab ViewPager:

  • tab 0: cf.C3920h.m4174B(R.string.block, R.drawable.ic_block, R.drawable.block_empty_list, f13036p)
  • tab 1: cf.C3920h.m4174B(R.string.spam, ...)
  • delegating fragments come from p648x7 (x7: C21656c, C21663j, C21664k, C21665l, C21666m, C21669p, ViewOnClickListenerC21662i).
  • SP keys used by the page chrome: SP_KEY_CALL_RECORDING_MENU_BUBBLE_SHOWN_V1, SP_KEY_SHOW_TIME_LIMIT_BANNER. No endpoints, no secrets, no crypto. Block/spam list persistence is the block_list / spam_list tables in contactdb (see §20.2) - i.e. they travel in the Google-Drive backup (§3).
15.4 Extended permission prompt (1 file) - flow #5, part 1h5

com.eyecon.global.Permissions.ExtendedPermissionPromptActivity extends AbstractActivityC3498e, R.layout.extended_permission_prompt_layout.

Step 1 - onCreate (OBSERVED):

View v = inflate(R.layout.extended_permission_prompt_layout, null, false);
ViewBinding f12410U = new C17065b(container, EB_continue, I_call_information, I_contacts_call_logs,
I_default_dialer, SV_scroll, TVagreement, 2);
setContentView(container);
new C3465f("reg consent message show").m3671c("list", "type").m3673e(false);
// three benefit blocks:
m10414C0(callInfo, R.drawable.ic_eyecon_phone, R.string.prompt_dialer_title, R.string.prompt_dialer_sub_title);
m10414C0(contactsLogs, R.drawable.ic_unlock_call, R.string.prompt_call_title, R.string.prompt_call_sub_title);
m10414C0(permissions, R.drawable.ic_premissions_icon,
R.string.prompt_permissions_title, R.string.prompt_permissions_sub_title);
// consent text, with [x]/[y] turned into an https://www.eyecon-app.com/ link
m10415D0((TextView) TVagreement, getString(R.string.get_started).replace("XXX", getString(R.string.continue_).toUpperCase()));
if (!getIntent().getExtras().getBoolean("showAgreement", false)) TVagreement.setVisibility(GONE);
if (getIntent().getExtras().getBoolean("showLanguageMenu", false)) { LL_language.setVisibility(VISIBLE); ... }
EB_continue.setOnClickListener(new ViewOnClickListenerC3880l(this, 10));
LL_language.setOnClickListener(new ViewOnClickListenerC3846e(this, 14));

m10415D0 uses Html.fromHtml + LinkMovementMethod and replaces each URLSpan with a tracking click-listener C17511z. The only endpoint in this activity is the marketing site https://www.eyecon-app.com/.

Step 2 - the OK handler (p047c7/ViewOnClickListenerC3880l case 10, OBSERVED):

MyApplication.m10347i().m32077d("SP_KEY_DID_EYECON_CONSENT_APPROVED", true); // boolean put
MyApplication.m10347i().m32074a(null); // commit
new C3465f("reg consent message click ok").m3671c("list", "type").m3673e(false);
((ExtendedPermissionPromptActivity) obj).finish();

Step 3 - what happens next is outside this class. The actual permission requests are issued by zq.g.m39758X(...) / m39759Y(...) (§12.1/12.2) from the screens that need them; RecordingsFragment.m10002I0 (§18) is a concrete example.

Security findings (prompt flow):

SevFindingEvidence
LOWThe prompt is skippable by intent flag (showAgreement=false hides the consent text) and the OK button records consent without any server-side or cryptographic record - it is a single boolean SP key that is trivially writable on a rooted device / via a backup restore.ExtendedPermissionPromptActivity:154; ViewOnClickListenerC3880l case 10
LOWThe click-through link is the marketing site only; there is no PII in the prompt.m10414C0
NONE (positive)No permission is requested from this activity; consent is captured before any grant.onCreate
15.5 Dynamic Area “cards” - flow #5, part 2h5

Config fetch (OBSERVED, ir/AbstractC17448b.m33507A, log tag DynamicAreaServerDa..):

C18533l url = new C18533l(); // OkHttp HttpUrl (re-used class, see below)
url.m34730I("https");
url.m34726E(AbstractC15804d.m31091l("dynamic_area_url", false)); // default "cards.eyecon-app.com"
url.m34722A("getCards"); url.m34722A("");
// POST body (Gson):
{
"country": <SIM country, lower-cased>, // Locale(displayCountry, ENGLISH).toLowerCase()
"language": <SIM language, lower-cased>,
"cli": AbstractC9039h.m10649f(), // the user's own Eyecon CLI
"cid": AbstractC9039h.m10662s(), // the user's session/registration id
"version_name": "4.2026.04.23.1145",
"version_code": 749,
"is_premium": <bool>,
"cards": [<card/ticket ids currently shown>], // deduped set of f58865a
"tags": [...remote-config "da_json_tags" + locally collected tags...],
"bundle_id": SP "SP_KEY_DA_LAST_BUNDLE_ID"
}
C21273a.m38417a(request, true) // same e-auth-header HTTP client as the rest of the API
if (code != 200) throw new EyeconException();
m33503W(response.byteStream(), listA, listB, listC, out); // multi-part stream decode
C21856k.f58892g ... RunnableC18728q(...) // persist into contactdb

⇒ Endpoint: POST https://cards.eyecon-app.com/getCards (host itself remote-config controlled). HTTPS. Sends the user’s Eyecon CLI + registration id (cid) - the same credential pair as the main API - plus the SIM country and language, the premium flag, and the list of card/ticket ids the user has already been shown (behavioural targeting). SP_KEY_DA_LAST_TAGS is stored so that a tag change resets SP_KEY_DA_LAST_BUNDLE_ID ("empty_bundle_id"), i.e. the server-side experiment bundle identity is derived from the tag set.

Persistence (OBSERVED, contactdb): tables dynamic_area_tickets (each card the user has been shown, with impression counters) and dynamic_area_subjects (card bodies; columns c16458a12 = “path” default "Has no path", c16458a13 = description default "Has no description", c16458a14 on tickets default '["all"]'). Cleared wholesale on a config change (C21856k:167-168). These two tables therefore also leave the device inside the Google-Drive contactdb backup (§3).

Ad slot inside the carousel (OBSERVED, b6.a = C3468a): AbstractC15804d.m31091l("native_ad_dynamic_area", false) (default ca-app-pub-2959190743815944/9389009478) selects a GAM native unit; C19836g is rendered and tagged "dynamic_area" via AbstractC19051d.m35339a(view, "dynamic_area").

com.eyecon.global.DynamicArea (3 wrapper files):

FileKindRole
BaseCardViewLayout.javaCardViewdispatchTouchEvent forwards to a setCustomTouchListener(...) hook so the carousel can intercept swipes. No logic.
CardsException.javaExceptionMarker.
SwipeRefreshLayout.java (10.8 KB)androidx.swiperefreshlayout-styleDrop-in pull-to-refresh used by Menifa and the cards list. No MICO logic.

Security findings (cards):

SevFindingEvidence
MEDIUMThe cards host is remote-config controlled (dynamic_area_url) and the request body carries the user’s Eyecon CLI and cid registration id to it. A cleartext-HTTP remote-config attacker (§16.5) can repoint dynamic_area_url at a host of their choosing and thereby receive cli + cid.ir/AbstractC17448b.java:593-649; AbstractC15804d.java:122, 300
MEDIUMCard bodies (dynamic_area_subjects.description, .path) are server-supplied strings that are rendered into the UI and carry deep-link actions. Rendered in-app, so the impact is UI-level, but it is a server-driven content channel like §14.C21851f (c16458a12, c16458a13)
LOWImpression/ticket state lives in contactdb and is uploaded to Google Drive on every backup (§3) - a minor behavioural-profile leak beyond the contact data.§3.1; C0041b:132,160

16. Ad mediation - flow #6, consolidatedh4

16.1 Bootstrap: every SDK is initialised eagerly at app starth5

o4.g.m35343A(MyApplication) (p448o4/AbstractC19054g.java:83-110, log tag "AdsHelper"):

int consent = C19064q.m35366e(ctx); // 0 UNKNOWN, 1 NOT_REQUIRED, 2 REQUIRED, 3 OBTAINED
MolocoLogger.setLogEnabled(false);
AdRegistration.getInstance(getString(R.string.amazon_app_key), ctx); // Amazon APS
AdRegistration.setMRAIDSupportedVersions(new String[]{"1.0","2.0","3.0"});
AdRegistration.setMRAIDPolicy(MRAIDPolicy.CUSTOM);
AudienceNetworkAds.buildInitSettings(ctx).withInitListener(new C19052e()).initialize(); // Meta FAN
MBridgeSDKFactory.getMBridgeSDK().setConsentStatus(ctx); // Mintegral/Mbridge
m35358p(consent == 3, consent == 2 || consent == 3); // Fyber/FairBid consent
CrackleMediationAdapter.crackleInitialize(ctx); // Moloco
MobileAds.initialize(ctx, new C19053f()); // Google AdMob / GAM
AudienceNetworkAds.initialize(ctx);
if (MyApplication.f12170o) { BaseAfterCallActivity.m9925o1("Application on create", null);
AbstractC16794a.m32647J("Application on create", null); // CallerIdHelper pre-load
AbstractC21232a.m38324I("Application on create", null); } // DefaultDialerTools pre-load

⇒ 8+ ad SDKs initialise unconditionally on first launch, before any ad slot is shown and irrespective of premium status at the SDK level (premium only gates requests, see m35345C).

MechanismOBSERVED detail
Google UMPC19064q.m35366E(ctx) reads SP file __GOOGLE_FUNDING_CHOICE_SDK_INTERNAL__, key consent_status (int). C19064q.m35364A(i) maps it to "UNKNOWN"/"NOT_REQUIRED"/"REQUIRED"/"OBTAINED". Error events cmp error with Source and mapped names INTERNAL_ERROR / INTERNET_ERROR / INVALID_OPERATION / TIME_OUT.
GDPR-by-SIM heuristicC19064q.m35365C() - 32 ISO codes: at, be, bg, hr, cy, cz, dk, ee, fi, fr, de, gr, hu, is, ie, it, lv, li, lt, lu, mt, nl, no, pl, pt, ro, sk, si, es, se, gb, ch. Returns null when the SIM country is unknown. Log: "ConsentHelper", "isGdprCountryBySimCard: iso = %s, result = %s".
npa (non-personalised) signal to AdMobAbstractC19054g.m35355M(builder):<br>if ((status == REQUIRED || (status == UNKNOWN && gdprBySim)) && status != OBTAINED) addNetworkExtrasBundle(AdMobAdapter.class, {npa:"1"})
FairBid / Fyber network extrasaddNetworkExtrasBundle(FyberMediationAdapter.class, {mute_video:false})
MbridgeMBridgeSDKFactory.getMBridgeSDK().setConsentStatus(ctx) (IAB TCF read)
Mintegral COPPAp274eo/C16303b:976 MintegralMediationAdapter.f21500b.setCoppaStatus(ctx, MobileAds.getRequestConfiguration().getTagForChildDirectedTreatment() == 1)
Pangleir/AbstractC17448b:154 returns AdError(103, "MobileAds.getRequestConfiguration() indicates the user is a child. Pangle SDK V71 or higher does not support child users.", "com.google.ads.mediation.pangle")

Finding: the app relies on Google UMP for consent but does not forward a consent signal to the non-Google SDKs it initialises (InMobi, Vungle/Liftoff, Pangle/ByteDance directly, Moloco, Singular). npa=1 only reaches the AdMobAdapter extras bundle.

16.3 Custom targeting attached to every AdManager request (OBSERVED, m35346D)h5
AdManagerAdRequest.Builder b = new AdManagerAdRequest.Builder();
m35355M(b); // npa / mute_video extras (16.2)
if (AbstractC15804d.m31081b("is_gam_key_value_enable")) { // remote flag
b.addCustomTargeting("ev", String.valueOf(749)); // app version_code
b.addCustomTargeting("adapterversion", Build.VERSION.RELEASE); // Android version
b.addCustomTargeting("custom", AbstractC16468f0.m32005O(simCountry.toLowerCase())); // SIM country
b.addCustomTargeting("m", AbstractC16468f0.m32005O(Build.MANUFACTURER.toLowerCase()));
b.addCustomTargeting("ua_n", AbstractC16468f0.m32005O(currentNetworkType.toLowerCase()));
b.addCustomTargeting("uid", oneOf("Not joined","Missing auth info","Identity established",
"Identity not found","Not ready"));
}

⇒ Every ad request to the Google stack carries: app version code, Android release, SIM country, device manufacturer, current network type, and a coarse account-identity status. AbstractC16468f0.m32005O normalises (lowercase, no hashing - the call site already lower-cases before passing it, so the “hashing” is only the lowercase). This is device fingerprinting for ad targeting, not PII.

16.4 Networks, slots and refresh triggersh5

Networks (from imports, adapter classes and remote-config unit ids):

NetworkEvidenceRole
Google AdMob / GAMR.string.admob_app_id = ca-app-pub-2959190743815944~6092014645; 26 ca-app-pub-… unit ids in remote-config defaults; MobileAds.initializePrimary mediation
Meta / Facebook Audience NetworkAudienceNetworkAds.buildInitSettings(...).initialize(); com.google.ads.mediation.facebook.FacebookMediationAdapter; facebook_app_id = 1716660955257637; p8.d AppEventsAdapter + own SDK init
Fyber FairBid / Inneractivecom.fyber.fairbid.*, com.fyber.inneractive.sdk.external.*, FyberMediationAdapter, com.fyber.fairbid.adapters.APSAdapter, MintegralAdapter, fb_ts_sdk_version = "FairBid SDK %s"FairBid mediation wrapper (u4, §11)
Amazon APSR.string.amazon_app_key = 905c0238-13dd-4cf9-af3d-90f64672a4a9; com.fyber.fairbid.adapters.APSAdapter; com.amazon.aps.shared.metrics.model.ApsMetricsDataMapFairBid adapter
Moloco / Crackletech.crackle.*, com.moloco.sdk.*, CrackleMediationAdapter, R.string.crackle_key = fai=1:917722272265:android:af67d…Adapter + own rewarded-video/OMID SDK (ts, §10)
Pangle / ByteDancecom.bytedance.sdk.openadsdk.TTAdConstant, com.bytedance.sdk.component.pglcrypt.PglCryptUtils, com.google.ads.mediation.pangleGAM adapter only
Mintegral / Mbridgecom.mbridge.msdk.*, MintegralAdapter, MintegralMediationAdapterGAM + FairBid adapter
InMobicom.inmobi.sdk.InMobiSdk, com.inmobi.media.* (≈25 classes absorbed into v6.f)Standalone SDK init
Vungle / Liftoffcom.vungle.ads.*, com.vungle.mediation.VungleInterstitialAdapter, com.vungle.ads.internal.ui.VungleWebClientAdapter + standalone
AppLovinR.string.applovin_key = UZvHsQBqsDCf0qv_…Adapter
Singularcom.singular.sdk.Singular, com.singular.sdk.internal.Constants (used in v6.a0)Attribution
AdjustR.string.adjust_app_token = m23t9uwrtvy8Attribution
AppVestorapp_vestor_api_token = 980d4e3f-…, app_vestor_app_key = 8c830f45-…Attribution
Amplitudehttps://api2.amplitude.com/2/httpapi, https://api.eu.amplitude.com/2/httpapiAnalytics
Firebase Performance / CrashlyticsFirebasePerfUrlConnection.instrument(...), com.google.firebase.crashlyticsAPM
Play Billingcom.android.billingclient.api.PurchaseBilling (not ads)

Ad unit ids in remote-config defaults (OBSERVED, AbstractC15804d): ac_ad_unity_id_v2, ac_native_ad_unit_id, photo_picker_ad_unit_id, interstitial_for_add_contact, interstitial_for_photo_picker, interstitial_for_reverse_lookup, interstitial_for_features_dialog, history_ad_unit_id, rewarded_ad_unit_id, native_ad_dynamic_area, incall_default_dialer_admob_banner_ad_unit_id, com_google_sticky_banner_ad_id, google_sms_ad_unit_id, sms_window_admob_banner_ad_unit_id, ac_ib_banner_ad_unit_id, notificationReaderAdmobBannerAdUnitId, ac_lock_admob_banner_unit_id, rewarded_admob_features_dialog_ad_unit_id, incall_fs_feed_{gam,admob}_{first,second,third,endless}_ad_id, incall_feed_{top,middle,bottom}_admob_ad_id, ac_admob_rewarded_ad_unit_id, lock_screen_no_video_ad_unit_id, incall_fs_feed_gam_* (BANNER/interstitial/interstitial/interstitial/REWARDED via b6.a).

Refresh triggers (all remote-config gated, OBSERVED):

TriggerKeyBehaviour
In-banner (“IB”)use_IB_ad_loading, ac_ib_refresh_time_msu4.z re-binds the unit into Map<ViewGroup, u4.v> on the cadence (§11.1)
Morning pre-loadmorning_ads_refresh_job.is_enable, .when (default "7:00")com.eyecon.global.Ads.MorningAdRefreshJob (§16.5)
Ad-free windowduration_without_ads_v2m35351I(): if now < installTs + duration -> no ads
Ad-free windowduration_without_popup_and_aftercallm35353K(): same for popups + after-call
Fullscreen caller-ID feedincall_full_screen_enable_ads, incall_fs_feed_enablew0.a.m38324I pre-load, gated also on CallStateService.m10199X()
In-call feedCallerIdHelper.m32645G()gp.a.m32647J pre-load
Lock-screen no-video fallbackuse_no_video_ad_only_when_not_paidm35356N swaps to lock_screen_no_video_ad_unit_id when the last ad went unpaid and the screen is locked
MediaShare fetch cadenceshare_video_list_sync_interval§14
Remote config cadenceRemoteConfigUpdater (com.eyecon.global.Analytics.RemoteConfig)WorkManager worker; -1 short-circuit via dev_remote_config_tester == 1

Premium gating (OBSERVED, m35345C): every ad request goes through

Boolean premium = AbstractC19327e.m35662U(null); // null while async, resolved in a callback
premium == TRUE -> log "source = %s, premium user 1" ; presenter.m31516i(); // show ad
premium == FALSE -> log "source = %s, NOT premium user 1"; presenter.m31515h(); // suppress
16.5 com.eyecon.global.Ads.MorningAdRefreshJob (FIRST-PARTY wrapper)h5
public class MorningAdRefreshJob extends Worker {
static void b() { // enqueue
if (!AbstractC9039h.m10665v()) { log("enqueue canceled, not eyecon user"); return; }
JsonObject cfg = AbstractC15804d.m31088i("morning_ads_refresh_job").m14606i();
if (!cfg.get("is_enable").getAsBoolean()) { log("enqueue canceled, disabled by remote"); return; }
long delay = c(m32029x(cfg,"when","7:00").m14605h().get(0).mo14598m()); // "hh:mm"
WorkManager.beginUniqueWork("MorningAdRefreshJob_v2", ExistingWorkPolicy.KEEP,
new OneTimeWorkRequest.Builder(MorningAdRefreshJob.class)
.setInitialDelay(delay, MILLISECONDS).addTag("MorningAdRefreshJob_v2").build()).enqueue();
}
static long c(String hhmm) throws FormatException { /* sets HOUR_OF_DAY/MINUTE/SECOND/MILLISECOND on a Calendar
clone, rolls to tomorrow if already past */ }
Result doWork() {
if (!AbstractC9039h.m10665v()) return success; // "not eyecon user"
if (!is_enable) return success; // "disabled by remote"
AbstractC16794a.m32647J("Morning job", null); // CallerIdHelper in-call feed pre-load
AbstractC21232a.m38324I("Morning job", null); // DefaultDialer fullscreen pre-load
AbstractC0077t.m439X0(30000L); // 30 s delay
C16945i.m32772d(new RunnableC19065r(), 2000L); // +2 s, then re-enqueue
return success;
}
}

⇒ A daily, server-scheduled (time-of-day server controlled) pre-load of caller-ID-feed and fullscreen-dialer ads, gated only on “is a registered Eyecon user” + two remote booleans. Premium is checked inside m35345C at request time.

16.6 com.eyecon.global.Ads / .AdsIB other wrappersh5
FileKindRole
Ads/CustomMediaView.java (2.6 KB)extends com.google.android.gms.ads.nativead.MediaViewRounded-corner MediaView: caches a Path from R.dimen.default_corner_radius (only the TL/BR corners) and canvas.clipPath in onDraw; onMeasure honours an XML attr (0=wrap, 1=square by width, 2=square by height). No PII, no network.
Ads/Google/FullscreenAds/FullscreenNativeAdsActivity.java (4.4 KB)ActivityFullscreen GAM native ad. Extras ad_key, screen. Resolves C19837h.m36529F(ad_key); requires r4.g (C19836g, i.e. native-only) else finish() + C15987c("Only native ads supported here.", 2). Tags the view "fullscreen_native_ad". Skip button countdown from remote key fsn_screen_hold_time_in_seconds (floor of 5 s because Google enforces a minimum). Handler messages drained in onDestroy.
AdsIB/TemplateView.java (4.8 KB)FrameLayoutFairBid/GAM in-banner template. Inflates R.layout.native_banner_style_ad (XML-overridable via AbstractC18701d.TemplateView), binds NAV_ad (NativeAdView), TV_headline, body_or_price, EB_action_button, ad_media, ad_app_icon, bottom_area_background. setLayoutType(EnumC20594l) can override the theme (u4.l layout enum). m9919A(int) resolves a theme attr to a colour. No PII.
16.7 Ad-mediation security findingsh5
SevFindingEvidence
MEDIUMEight-plus ad SDKs are initialised unconditionally at app start, including Meta FAN, Amazon APS, Mbridge, Moloco and AdMob, regardless of premium status. The only thing that gates the requests is the premium check inside m35345C. Consent is gathered via Google UMP but the npa flag is delivered only via the AdMobAdapter network-extras bundle; the directly-initialised InMobi / Vungle / Pangle / Moloco / Singular SDKs receive no equivalent app-level consent signal in the code paths in this cluster.p448o4/AbstractC19054g.java:83-110, m35355M
MEDIUMAd unit ids, placement keys, refresh cadences, ad-free windows, host overrides and the consent branch itself are all remote-config controlled over cleartext HTTP (AbstractC15804d:300), i.e. a network attacker can change which ad network is targeted and when.AbstractC15804d.java (400 keys)
MEDIUMaddCustomTargeting sends SIM country, device manufacturer, Android release, current network type and account-identity status on every AdManager request.m35346D
LOWAd failure analytics send ad_type, error_name, error_message, error_cause_message, error_cause_code, unit_id and a normalised ad_provider (derived from the mediation adapter class name) to MICO’s own analytics.m35354L
LOWFullscreenNativeAdsActivity keeps a Handler with a pending countdown; it is drained in onDestroy, so no leak. Remote key fsn_screen_hold_time_in_seconds < 5 is logged and clamped.FullscreenNativeAdsActivity.onDestroy
NONE (positive)No contact, call-log, photo, SMS or location data is assembled by any class in u4, ts, dn, a9, p8, o4, r4 or b6. Verified by grep.-

17. Photos - flow #3, complete (wrapper classes + the two Facebook/Google harvesters)h4

Section 6 covered the obfuscated package n7 (PhotoPicker UI) and the Blowfish m7.d storage. This section covers the com.eyecon.global.Photos wrappers and the two automated photo-harvest paths the briefing does not mention.

17.1 com.eyecon.global.Photos wrapper classes (4 files, FIRST-PARTY)h5
FileKindRole
PhotoPicker/PhotoPickerActivity.java (44 KB)ActivityThe photo picker host. R.layout.activity_photo_picker.
PhotosTrackerWorker.java (12 KB)androidx.work.WorkerAutomated Facebook profile-photo harvester - see 17.3
PhotoRevealAnimationView.java (15 KB)custom ViewReveal animation for the photo grid/pager. No logic, no I/O.
ZoomImageView.java (25 KB)custom ViewPinch-zoom for the full-screen photo. No logic, no I/O.

PhotoPickerActivity intent contract (OBSERVED):

ExtraMeaning
name, cliContact name + Eyecon CLI being decorated
deepLink (bool)launched from a deep link
EXTRA_KEY_IS_FROM_NEW_CONTACT_ACTIVITYNewContactActivity mode -> no disk write, return via m10425J0
AbstractC0039a.f210dcontact id
contact_cis_list (ArrayList from c19862i.m36624E())the contact’s CLI list
contact_image_tagpicLastApproveTag
AbstractC0039a.f207cprimary_raw_id
EXTRA_SOURCE, EXTRA_IS_EYECON_USER, change_picanalytics + “already has a photo”

Static entry points:

  • m10419M0(FragmentActivity, C19862i, String source) -> startActivityForResult(PhotoPickerActivity, 93)
  • m10420N0(String cli, String name, NewContactActivity) -> startActivityForResult(PhotoPickerActivity, 74)
  • m10421E0() re-enables EBapprove (R.string.set_photo) after a save
  • m10423H0(String, int, EnumC18737z) - the “no results / switch tab” callback (the n7.e presenter tail, §6.1)
  • m10425J0(int, Bundle) - return path to NewContactActivity (NewContactActivity.f11749G0 = bitmap)
  • m10426K0() - R.string.permissions_needed + R.string.permission_app_setting_message + R.string.go_to_settings dialog
  • m10418G0(String) - analytics contact_photo_<x>_update_fail

Photo source tiles: camera (takePictureFromCamera(View)), gallery (getImageFromGallery(View) -> AbstractC0085x.m464O0(this) -> startActivityForResult(..., 71), analytics contact_photo_click_gallery, error code PMA_1), Facebook (R.id.FL_facebook + R.id.TV_facebook, gated on remote flag display_facebook_link) and Google Images (the R.id.LL_survey_option_no_result “can you find …” flow, §6.2). onDestroy (OBSERVED) tears down the Google-Images HttpUrl object: c18533l.f46818e = null; WebView[] wv = c18533l.f46817d; ... wv[i].destroy(); c18533l.f46820g.clear(); c18533l.f46817d = null; - no leaked WebViews. onPause/onResume also null out the array.

17.2 Facebook photo search - a scraped mobile-web Facebook profile search (m7.h)h5

com.eyecon.global.Photos.PhotoPicker.m10416C0(PhotoPickerActivity, String[] names) (PhotoPickerActivity.java:266-282):

if (!AbstractC15804d.m31081b("display_facebook_link")) { activity.m10423H0("", 200, EnumC18737z.f47504b); return; }
C18525h fb = new C18525h(names, new C18622a(activity, 1)); // m7.h
activity.f12464s0 = fb;
if (fb.f46788e == null) return;
fb.f46791h = -1;
fb.f46786c = new C18519e(fb, fb.f46784a, 0); // m7.e WebViewClient
fb.f46787d = new C18519e(fb, fb.f46784a, 1);
fb.m34719B(fb.f46788e, fb.f46784a[0]); // <-- the actual search

m7.h.m34719B (C18525h.java:107-110):

webView.setWebViewClient(f46786c);
webView.loadUrl("https://m.facebook.com/search/people/?q=" + str); // str = the CONTACT NAME

and m7.h.m34718D configures each WebView (C18525h.java:95-103):

webView.setAlpha(0.0f); // invisible
webView.getSettings().setBuiltInZoomControls(true);
webView.getSettings().setJavaScriptEnabled(true);
webView.getSettings().setUserAgentString(
"Mozilla/5.0 (Linux; Android 8.0; Pixel 2 Build/OPD3.170816.012) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/79.0.3945.130 Mobile Safari/537.36");
webView.getSettings().setLoadsImagesAutomatically(false);

One main WebView plus names.length - 1 extra WebViews (f46789f), each tagged "main" / "extra_<i>", so N contacts are searched in parallel. Results are accumulated in LinkedHashMap f46793j (item_id -> JSONObject) and HashSet f46792i; m34717A serialises the map to a JSONArray. m34720C destroys every WebView and clears the JS bridge reference (f46790g = null).

⇒ OBSERVED: MICO performs a hidden Facebook mobile-web people search for the user’s contact names, in an invisible JS-enabled WebView, with a spoofed, hardcoded Pixel 2 / Chrome 79 user-agent, and parses the results out of the page. m7.e (C18519e, 13 KB) is the WebViewClient that does the DOM scraping; mn.a (C18622a, 18 KB) is the @JavascriptInterface bridge.

17.3 Google Images “photo search” (m7.l = C18533l) - a remote-config-controlled scraperh5

Constructor (C18533l.java:64-84) loads the whole config from remote:

this.f46822i = AbstractC15804d.m31088i("google_photos_link_loader").m14606i();

which is the 2.4 KB JSON default at AbstractC15804d:272 containing search_url, max_links: 75, links_script, big_url_script. m34727F(PhotoPickerActivity) (C18533l.java:199-221):

String ua = AbstractC15804d.m31091l("user_agent", false);
String search = ((JsonObject) f46822i).get("search_url").getAsString(); // default "https://www.google.com/search?hl=en&udm=2&q="
WebViewClient wc = new C18519e(this);
JavascriptInterface js = new C18531k(this);
for (int i = 0; i < schemes.length; i++) {
WebView wv = new WebView(activity);
wv.getSettings().setBuiltInZoomControls(true);
wv.getSettings().setJavaScriptEnabled(true);
wv.getSettings().setUserAgentString(ua);
wv.getSettings().setLoadsImagesAutomatically(false);
wv.setWebViewClient(wc);
wv.setTag(schemes[i]);
wv.addJavascriptInterface(js, "Android"); // <-- JS bridge named "Android"
wv.loadUrl(search + schemes[i]);
}

links_script (from remote config) walks document.getElementsByTagName("a"), descends into each <img>, reads data-src/src, tags each anchor id = "item"+n, and JSON.stringifys the results. big_url_script (remote config) does document.getElementById('[ITEM_ID]').click(), waits a tick, re-reads q_item.href, regex-extracts the imgurl= parameter, decodeURIComponents it and calls Android.onBigLink(big_url, '[WEBVIEW_TAG]', '[ITEM_ID]') - i.e. the extracted original-size image URL is handed back to Java. Driver call site: RunnableC18375q:214 (webView2.evaluateJavascript(links_script.replace("[WEBVIEW_TAG]", tag).replace("[MAX_LINKS]", "75"), new C18529j(...))) and C16099a0:103 (evaluateJavascript(big_url_script...)).

⇒ OBSERVED: a Google Images scraper whose search URL, link-extraction JavaScript and user-agent are all remote-config controlled and delivered over cleartext HTTP.

17.4 PhotosTrackerWorker - background harvesting of Facebook profile photos of all contactsh5

com.eyecon.global.Photos.PhotosTrackerWorker (FIRST-PARTY, Worker). Log tags: PhotosTrackerWorker.

Schedule (m10431C, OBSERVED):

if (!AbstractC15804d.m31081b("isPhotosTrackerEnabled")) { m10430B(); return; } // remote master switch
WorkManager.beginUniqueWork("PhotosTrackerWorker", policy,
new OneTimeWorkRequest.Builder(PhotosTrackerWorker.class)
.setInitialDelay(3L, TimeUnit.DAYS)
.setConstraints(new Constraints.Builder()
.setRequiredNetworkType(NetworkType.UNMETERED) // wifi only
.setRequiresBatteryNotLow(true)
.setRequiresCharging(false).build())
.addTag("PhotosTrackerWorker").build()).enqueue();

⇒ Every 3 days, on unmetered (Wi-Fi) network, battery-not-low.

Selection (m10432D(boolean all), OBSERVED): reads all contacts from DBContacts.f11707J, sorts them, dedupes by phone_number_in_server, then keeps only contacts that have a Facebook social link: if (c.m36637S(EnumC19861h0.FACEBOOK) != null). Then it windows the result to 20 at a time, resuming from SP "fpt_last_tracked_cis".

Token (m10433E, OBSERVED):

String t = AbstractC15804d.m31091l("fct", false);
return !isEmpty(t) ? t : MyApplication.m10343e().getString(R.string.fct);

R.string.fct = 1716660955257637|afa3f38c55f24c73b177afc962969b15 - i.e. facebook_app_id|facebook_app_secret, a Facebook application secret hardcoded in res/values/strings.xml, used directly as access_token=.

Probe (m10434F, OBSERVED): for each selected contact it downloads

Bitmap b = AbstractC18543s.m34747H("https://graph.facebook.com/" + socialId + "/picture?width=50&access_token=" + token);
if (b == null || isEmpty(c18535m.m34731A(b))) return; // compare against a known-size baseline
eyeconRoomDB.m10353C(); // store into the Room DB "eyecon_room"

Commit (m10435G, OBSERVED): on the second pass (ptw_tracked_before == true) it fetches the 600 px and 200 px variants and attaches them to the Eyecon contact record:

url600 = "https://graph.facebook.com/" + socialId + "/picture?width=600&access_token=" + token;
url200 = "https://graph.facebook.com/" + socialId + "/picture?width=200&access_token=" + token;
C16470g0 upd = new C16470g0();
upd.m32041J(AbstractC0039a.f216f, contact.phone_number_in_server);
upd.m32039H(AbstractC0039a.f187T, 2);
upd.m32041J(AbstractC0039a.f182Q0, url600);
upd.m32041J(AbstractC0039a.f184R0, url200);
upd.m32041J(AbstractC0039a.f190U0, new C19932m(EnumC19861h0.FACEBOOK, socialId, url200, url600).toString());
DBContacts.f11707J ... RunnableC0081v(dbContacts, updates, new C3502i(updates, contacts, 20), true);
AbstractC3464e.m3659J("Found new FB photos", {amount: <n>}, false);

So the harvested photos become the Eyecon contact avatar, which then propagates through the ordinary contacts pipeline (§6.2 step 7) into addcontacts.jsp and into the Google-Drive contactdb backup (§3). First-run behaviour: on the first run (ptw_tracked_before == false) it fetches the 50 px probe for everyone but commits nothing, and only records ptw_tracked_before = true.

17.5 Photo security findingsh5
SevFindingEvidence
CRITICALA Facebook application secret is hardcoded in the APK and used as a Graph API access_token to download profile pictures of the user’s contacts. R.string.fct = "1716660955257637|afa3f38c55f24c73b177afc962969b15" (app-id | app-secret). The secret is also remote-config overridable (fct), so it can be rotated without an app update. Extracting it lets anyone query the Graph API as this app (subject to app-level review/permissions).res/values/strings.xml fct; com/eyecon/global/Photos/PhotosTrackerWorker.java:127-130, 138, 160-161
HIGHBackground harvesting of third-party profile photos without an in-app prompt. PhotosTrackerWorker runs every 3 days on Wi-Fi and silently attaches Facebook profile pictures of the user’s contacts as Eyecon contact avatars. Gate is a single remote boolean (isPhotosTrackerEnabled); there is no per-contact consent, no setting in the Settings screen, and no disclosure in ExtendedPermissionPromptActivity. The harvested avatars then leave the device via addcontacts.jsp (§6.2) and via the Google-Drive backup (§3).PhotosTrackerWorker.doWork/m10432D/m10434F/m10435G
HIGHHidden Facebook people-search scraping. m7.h loads https://m.facebook.com/search/people/?q=<contact name> in an alpha=0 (invisible) WebView with JavaScriptEnabled and a spoofed UA (...Android 8.0; Pixel 2...Chrome/79.0.3945.130), once per contact name in parallel. Facebook therefore sees N synthetic “mobile web” searches for the user’s contact names, attributed to this app, without the user visiting Facebook.p420m7/C18525h.java:54-110
HIGHRemote-config-controlled scraping JavaScript. google_photos_link_loader supplies the Google Images search_url and two arbitrary JavaScript programs (links_script, big_url_script) that are evaluateJavascript-ed into a JS_ENABLED WebView that also exposes addJavascriptInterface(js, "Android"). Because remote config is fetched over cleartext HTTP (AbstractC15804d:300), a network attacker can substitute arbitrary JS.p420m7/C18533l.java:64-84, 199-221; RunnableC18375q:214; C16099a0:103; AbstractC15804d:272, 300
HIGHContact photos at rest are Blowfish/ECB with a <=1000-value key and land in world-readable shared storage (Documents/Eyecon/ContactsPhotos) when external storage reports mounted. (carried over from §6.4 - restated here because §17.4 makes the volume of stored photos much larger).p420m7/C18517d.java; p379k5/RunnableC17750b.java:400-415
MEDIUMuploadpic.jsp posts the user’s own Eyecon profile picture as a raw JPEG with msisdn = clientId; the base URL is a mutable static field. (restated from §6.2.)p620w6/C21280h.java:437-497
MEDIUMfct is also remote-config overridable, so the Graph token is not pinned to the APK value and could be swapped to a token the server operator controls.PhotosTrackerWorker.m10433E
LOWPhotoRevealAnimationView / ZoomImageView are pure UI; no findings.-
NONE (positive)PhotoPickerActivity.onDestroy destroys every WebView and clears the JS bridge; PhotoPickerActivity is launched with plain startActivityForResult (request codes 71/74/93) and is not exported for other apps.PhotoPickerActivity.onDestroy:843-861

18. AudioRecording / RecordingsFragment - what RecordedCalls genuinely doesh4

18.1 Verification of the C3 conclusion (OBSERVED, independent re-check)h5
CheckCommand / scopeResult
Any new AudioRecord(...)whole sources tree1 hit - p599v7/RunnableC20908f.java case 11 (Toki, §2.1)
MediaRecorderp* packages0 hits
MediaProjectionp* packages0 hits
VOICE_DOWNLINK / VOICE_UPLINK / VOICE_CALLp* packages0 hits (only an unrelated constant inside androidx.core.content.ContextCompat)
AudioRecord guarded by RECORD_AUDIOz7.v0.m39395G0yes

CONFIRMED: the app contains no call-audio capture of any kind. There is no code path that can produce a RecordedCalls file. Documents/Eyecon/RecordedCalls is therefore purely a legacy read/migrate/save/share folder for files produced by something else (a legacy build, or a companion/dialer-role app).

18.2 RecordedCalls / RecordedNotes file format (OBSERVED, e5.f = C16104f)h5

File name grammar (C16104f.m31479A, plus the MediaStore query in e5.d.m31471E):

"+____+__[0-9]+(__s|__c)?(__-1|__1|__2)?__(ern|erc)\\..*"
^^ ^^^^ ^^^^^^^^^ ^^^^^^^^^^^ ^^^^^^^^^
| epoch ms dir flag call/note extension
display name ( "_" -> " " )

m31479A:

String[] p = str.replaceAll("__+", "__").split("__");
if (p.length < 4 || !(str.contains("ern.") || str.contains("erc."))) throw new ParseException(...);
return p.length <= 6 ? p : Arrays.copyOfRange(p, p.length - 6, p.length);

Fields: p[0] = display name, p[1] = phone number, p[2] = epoch-ms start, [3] = s/c (saved / call), [4] = direction/party index, [5] = ern (RecordedNote) or erc (RecordedCall). f39639a = !f39647i.contains("ern.") ? 1 : 0 - i.e. erc. == “this is a call recording”. Per-record defaults: duration 14000L, f39641c. The companion table recorded_notes in contactdb stores the same rows (AbstractC0039a.f200Z0 = start ts, f221g1 = duration, f213e/f216f/f219g = name/number/country, f227i1 = “is on device”).

18.3 What RecordedCalls genuinely does - the four real operationsh5

OP 1 - INDEX / DISCOVER (e5.d.m31471E(boolean), OBSERVED). Queries MediaStore.Files.getContentUri("external_primary") on API 29+ with

(relative_path = ? OR relative_path = ?) AND _display_name REGEXP ? -- "Documents/Eyecon/RecordedCalls/" , ".../RecordedNotes/" , the grammar above

and on older APIs _data LIKE '%<MyApplication.f12172q>%' AND _display_name REGEXP ?. Each hit is then opened and read one byte (inputStream.read() > -1) purely to skip unreadable MediaStore ghosts, and de-duplicated by start timestamp preferring the f39652n (on-device) copy. m31473G is the pre-Q path: plain File.listFiles() over AbstractC16098a.m31460C(0|1).

OP 2 - MIGRATE to scoped storage (e5.d, OBSERVED). Guarded by SP_KEY_DID_MIGRATE_TO_SCORE_STORAGE:

File notes = new File(dir, "RecordedNotes"), calls = new File(dir, "RecordedCalls");
for (File f : notes.listFiles(new C16100b(2))) if (!f.isDirectory()) { /* copy/replace */ }
for (File f : calls.listFiles(new C16100b(2))) { ... }

plus AbstractC16098a.m31461D(name, "RecordedCalls"/"RecordedNotes") which builds a MediaStore content:// Uri (scoped-storage insert) and a raw 1024-byte copy loop (m31458A, with a progress float = bytesRead / max(1, length)). Storage-migration probe: writes "test_file" into RecordedNotes and reads it back, caching the result in f39632f.

OP 3 - SAVE (promote call recording -> note), e5.v case 0 + e5.w, OBSERVED:

if (Build.VERSION.SDK_INT > 29 && c16104f.f39642d < AbstractC9039h.m10661r()) {
C16123y.m31502c(listAdapter, new File(AbstractC16098a.m31460C(c16104f.f39639a), c16104f.f39647i).getAbsolutePath());
return; // too old -> "cannot un-share", direct path
}
c16104f.f39652n = !c16104f.f39652n; // toggle "moved to notes"
C16102d.m31477K(new RunnableC16101c(c16104f, 1));
// move the row from f11509p (calls) into f11508o (notes), notifyItemRemoved / notifyItemInserted

OP 4 - SHARE to any app via the OS share sheet, e5.v default case (OBSERVED) - the only “egress” path:

Intent i = new Intent("android.intent.action.SEND");
i.setType("audio/*");
Uri uri = c16104f.f39654p; // MediaStore Uri, or
if (uri == null) uri = Uri.parse(c16104f.m31483E().getAbsolutePath());
i.putExtra("android.intent.extra.STREAM", uri);
i.addFlags(1 /*FLAG_GRANT_READ_URI_PERMISSION*/);
recordingsFragment.startActivity(Intent.createChooser(i, "Share Recorded Note"));

⇒ The app’s only action on RecordedCalls is ACTION_SEND audio/* through the chooser - the user picks the target. There is no server upload of RecordedCalls from this screen.

OP 5 - BACKUP (this is where RecordedCalls does leave the device, §3.1). f5.w (RunnableC16439w) case 2 uploads every file from AbstractC16098a.m31460C(0) and m31460C(1) as an individual Drive object with MIME audio/amr into eyecon_backup_for_notes_records / eyecon_backup_for_calls_records.

18.4 RecordingsFragment (FIRST-PARTY wrapper, 731 lines)h5

extends p238d7.AbstractC15858b, R.layout.activity_records, 3-page ViewPager (C16099a0) over RV_recorded_notes on pages calls / saved / notes.

MethodPurpose
m9991A0()free-space check: AbstractC0042b0.m171T(external ? Environment.getExternalStorageDirectory() : getFilesDir()) > 50 (MB).
m9992B0()API 29+ tutorial-bubble flag SP_KEY_CALL_RECORDING_MODES_BUBBLE_SHOWN.
m9993C0(View)width-collapse animation on the permission tile.
m9994R0(fragment)analytics Rec Permission = {Rec Audio permission, Storage permission, Legal}.
m9995V0()call-recording mode: SP_KEY_RECORD_CALLS_MODE defaulting to remote call_recording_mode; if the stored mode is 1 it is downgraded to 0 when the user is premium or BaseAfterCallActivity.m9922F1().
m9996W0()missing permissions: RECORD_AUDIO, READ_EXTERNAL_STORAGE, WRITE_EXTERNAL_STORAGE.
m9997D0() / m10003J0()remove the overlay “Toast” view / release the WakeLock + FLAG_KEEP_SCREEN_ON.
m9998E0(String)analytics on Recorded notes page: Source, Create note type ("Didn''t create note" / "create recording"), Action, Call action ("Interact with recording" / "action (any action)").
m10001H0(int)premium gate: SP_KEY_PREMIUM_ACTIONS_LEFT vs remote free_rec_calls_actions; when exhausted shows R.string.upgrade_to_use + R.string.cancel / R.string.upgrade_now, otherwise decrements the counter and shows a WindowManager overlay toast (R.string.actions_left / one_premium_action_left / last_premium_action) of type TYPE_APPLICATION_OVERLAY (layoutParams.type = 2). Returns true when blocked.
m10002I0()first-run legal dialog R.string.record_calls + R.string.record_calls_legal_msg (icon R.drawable.record_call), then the permission request (request code 84) or the R.string.cr_remove_title / R.string.cr_remove_msg “call recording removed” dialog.
m10008S0(int) / m10011X0() / m10012Y0() / m10013Z0()the three page bodies (calls / saved / notes); m10012Y0 injects R.string.sample_note into an empty “notes” list.

SP keys: SP_KEY_LEGAL_MESSAGE_SHOWN_V2, SP_KEY_PREMIUM_ACTIONS_LEFT, SP_KEY_CALL_RECORDING_MENU_BUBBLE_SHOWN_V1, SP_KEY_SHOW_TIME_LIMIT_BANNER, SP_KEY_RECORD_CALLS_MODE, SP_KEY_CALL_RECORDING_MODES_BUBBLE_SHOWN, SP_KEY_DID_MIGRATE_TO_SCORE_STORAGE, SP_KEY_FIXED_RC_TIMESTAMP, SP_KEY_IS_CALL_RECORDING_ACTIVITY_ENABLE. Remote keys: call_recording_mode, free_rec_calls_actions. Endpoints: none. Secrets: none. Crypto: none.

18.5 Recordings security findingsh5
SevFindingEvidence
HIGH (confirmed, not a false positive)The UI asks for RECORD_AUDIO and shows a “call recording” legal notice, and offers premium “save”/“share” actions on RecordedCalls - but there is no recording capability behind it. On any device where RecordedCalls is empty (all current installs) the paid “premium action” counter can still be decremented against a no-op, and the RECORD_AUDIO grant is collected for a feature that does not exist in this build.RecordingsFragment.m9996W0/m10001H0/m10002I0; §18.1 table
HIGHEvery file in RecordedCalls and RecordedNotes is uploaded unencrypted (MIME audio/amr) to Google Drive appDataFolder (§3.1/§3.5). If a RecordedCalls folder ever is populated (legacy restore, or a contactdb restore that pulls older Drive objects), the audio leaves the device with no application-layer encryption.p282f5/RunnableC16439w.java case 2
MEDIUMThe TYPE_APPLICATION_OVERLAY “actions left” toast is added directly via WindowManager.addView inside try/catch(Throwable) - it silently prints the stack trace if the overlay permission is missing rather than degrading.RecordingsFragment.m10001H0:302-318
MEDIUMRecordings live in Documents/Eyecon/RecordedCalls / RecordedNotes on shared external storage when Environment.getExternalStorageState() == "mounted"; on API 29+ MICO moves to scoped MediaStore but still keeps the raw path for direct File access (m31483E).AbstractC16098a.m31460C
LOWm9991A0() reports raw free-space; m10001H0 shows an exact remaining-quota count to the user.RecordingsFragment.m9991A0
LOWFile names embed the contact display name and phone number in cleartext (+<name>__<number>__<epoch>__s__-1__ern.amr), so even the file listing on shared storage is PII.C16104f.m31479A; C16102d.m31471E regex

19. com.eyecon.global.Backup, .MoreMenuAndSettings remaining wrappers (FIRST-PARTY)h4

19.1 Backup wrappers (3 files)h5
FileKindRole
BackupActivity.java (3 KB)ActivityHost for the f5.j upload / f5.u restore dialogs
ProgressTracker.java (2.2 KB)FrameLayoutTwo RoundedCornersFrameLayout children; setProgress(float) sets the front bar’s width to getWidth()*f. Pure UI.
BackupOrRestoreException.javaExceptionMarker.

BackupActivity.onCreate (OBSERVED): extras EXTRA_BACKUP_INFO_JSON_STR else falls back to SP SP_KEY_LAST_TIME_BACKUP; if both are empty -> BackupOrRestoreException("missing EXTRA_BACKUP_INFO_JSON_STR") + finish(). Extras forTest, EXTRA_RETRY. m10014C0() shows C16426j (f5.j, the upload dialog) as a non-cancelable dialog with FLAG_DIM_BEHIND (addFlags(128)) tagged "BackupRestoreDialog", cancel callback RunnableC16126b(this, 1).

19.2 MoreMenuAndSettings remaining wrappers (8 files)h5
FileKindRole / findings
MoreSettingsFragment.java (47 KB)FragmentSettings hub. Remote-config gated rows: R.string.ai_call_assistant, call_handler_sub_text, cant_talk_right_now, customize, default_call_handle_v, popup, premium_page, recording, shortcut, system_default, text_size, backup, get_gift, get_premium, do_you_like. Hosts Toki (TokiContactsChooserActivity) and Backup (zq.g-style settings plumbing). No secrets.
SettingActivity.java (32 KB)ActivityToggle list: about, block_list_, change_language, compact, dual_sim_setting_title, dynamic_area_enable_setting_, enable_auto_rec, faq, full_screen, message_for_incoming_calls, show_missed_calls_notitification, show_sms_window, show_telegram_window, show_whatsapp_window, write_to_us, cancel. Each toggle is a dynamic SP key; a9’s SettingActivity.m10331H0(key, !value) + MyApplication.m10347i().m32077d(key, !value) + AbstractC9039h.m31543W(activity, key) emits an eyecon://idPlus?source=<key> deep link for the change.
MyProfileActivity.java (59 KB)ActivityOwn profile + own avatar. R.string.my_profile, R.string.premium_user, R.string.facebook_allready_connect, R.string.facebook_app_id, R.string.please_wait, R.string.messege. On photo change: RunnableC20880f(...,1) -> EyeAvatar.setPhotoAndRescaleWhenNeeded(bitmap), then C16945i.m32774F(C21280h.f56470F.f56472A, 0, new RunnableC21075c(1, ...)) and analytics profileModifiedPhoto / profileModified. This is the code path behind UploadProfileWorker -> C21280h.m38441N() -> uploadpic.jsp (§6.2).
UploadProfileWorker.java (1.4 KB)WorkerdoWork(): int r = C21280h.m38441N(); log("doWork result, uploadPhotoResult = %s", "NOT NEEDED"/"SUCCESS"/"FAILED"/"UNKNOWN"); return r == 0 ? retry() : success(); — retry only while SP_KEY_NEED_UPDATE_PROFILE_PIC is pending.
AboutActivity.java (10.7 KB)ActivityR.string.version + the social/FAQ/legal links already listed in §7. No secrets.
LicensesActivity.java (4.4 KB)ActivityRenders LicenseItem rows. No logic.
LicenseItem.java (23 KB)modelOne open-source licence entry (TV_artifact / TV_license / TV_title, §7). No logic.
C5848a.java (668 B)RecyclerView.ViewHolder/* JADX INFO: renamed from: com.eyecon.global.MoreMenuAndSettings.a */ - holds a p526ri.C19932m (which is the view-binding class defined in ri.m, §8.1). A genuine com.eyecon.global class that JADX renumbered because its real package already contained an a.

20. Remote config - verification of the briefing’s claims (OBSERVED)h4

20.1 Transport: CLEARTEXT HTTP, unauthenticated - CONFIRMEDh5
ItemValue / evidence
Default URLhttp://cloudfront-cdn-all.eyecon-app.com/configs/ab-test.txt - AbstractC15804d.java:300 (key eyecon_remote_config_url)
Fetcherp005a4/RunnableC0031c.java case 17 -> C21273a.m38423G(new ArrayList(), url, "GET", code, /*accept json*/false, "getUpdatedEyeconRemoteConfig", null, false, false, null, "")
Clientw6.a = C21273a (/* JADX INFO: renamed from: w6.a */, TAG = "NetworkHelper"), HttpURLConnection wrapped by FirebasePerfUrlConnection.instrument(...)
AuthNONE. m38417A(request, /*withAuth*/true) is what injects e-auth-v / e-auth / e-auth-c / e-auth-k; the remote-config call uses m38423G which does not call m38417A, so the fetch is unauthenticated.
HeadersUser-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) ... Chrome/87.0.4280.88 Safari/537.36 (spoofed desktop UA), accept: */*, 60 s connect/read timeouts
Schedulercom.eyecon.global.Analytics.RemoteConfig.RemoteConfigUpdater (WorkManager Worker); blocked entirely if dev_remote_config_tester == 1
StoreSP EyeconRemoteConfigVersion (int) vs remoteConfigVersion; d5.c.m31073A performs the A/B bucketing (isNotMatchTargetExposure / isNotMatchTargetCountry / isNotMatchTargetLanguage / isNotMatchTargetVersion, random_result, _variant_chance)
Package-local wrappercom.eyecon.global.Analytics.RemoteConfig.CountrySpecificConfig, EyeconRemoteConfigException, RemoteConfigDebugTestReceiver

Key count (OBSERVED): AbstractC15804d (d5.d, 661 lines) initialises map.put(...) 400 times - matching the briefing’s “~400 A/B flags”.

20.2 Every C5-relevant remote key, and what an attacker gainsh5
Remote keyEffect if flipped by a cleartext-HTTP attacker
eyecon_remote_config_urlPoint the whole config at a server of their choice (chicken-and-egg, but the default is what ships, and the attacker only needs the first fetch).
toki_portsRedirect the plaintext Toki audio stream (§2.2, §12.5).
dynamic_area_url (default cards.eyecon-app.com)Repoint the cards host and receive the user’s cli + cid (§15.5).
google_photos_link_loader (search_url, max_links, links_script, big_url_script)Arbitrary JavaScript executed in a JS_ENABLED WebView with an addJavascriptInterface bridge (§17.3).
user_agentChange the UA the Google/Facebook scrapers present.
facebooke_Extract_URL_Regex, findFirstMp4UrlForInstagramRegex, regexForAddSocial, notSupportedKeysChange which social content is extracted / blocked (§14, §15.2).
isWalkieTalkieEnabledAdd/remove the Toki button app-wide (§15.2).
isPhotosTrackerEnabledEnable/disable the background Facebook photo harvester (§17.4).
shouldSendNameToServerToggle whether the user’s contact name is transmitted to the backend (RunnableC16004e:399, set in AbstractC15804d:420).
fctSwap the Facebook app-secret/access-token used for Graph photo fetches (§17.5).
use_IB_ad_loading, ac_ib_refresh_time_ms, native_ad_dynamic_area, morning_ads_refresh_job.{is_enable,when}, incall_full_screen_enable_ads, incall_fs_feed_*, all ca-app-pub-* unit idsRetarget the ad stack (§16.4).
duration_without_ads_v2, duration_without_popup_and_aftercall, free_rec_calls_actionsRemove ad-free windows / change the free quota (§16.4, §18.4).
use_no_video_ad_only_when_not_paid, lock_screen_no_video_ad_unit_idForce a specific ad on the lock screen.
share_video_list_sync_interval, is_media_share_cache_enableControl the social-share feed (§14).
is_gam_key_value_enable, custom_theme_purchase_options, custom_background_theme_product_idAd targeting + theme pricing/purchase options (§15.1).
dev_remote_config_tester, should_show_caller_id_denied_dialogDisable the consent dialog / suppress the “caller ID denied” nag (§12.1).
open_whatsapp_intentsRewrite the WhatsApp intent templates used for contact messaging.
mobitech_*_html_script, com_google_sticky_banner_ad_id, call_contest_report_calls_url, welcome_back_notification_urlAd creative HTML and campaign URLs (note welcome_back_notification_url posts to a Google Form formResponse).
isFacebookSecond, is_notification_feature_enable, no_ad_lifetime_product_id, …~370 others.
20.3 contactdb table inventory (what actually leaves the device in the Drive backup)h5

OBSERVED from p008a7/C0041b.java onCreate/onUpgrade (database name contactdb, version 57):

contacts (main contact table, ~30 columns incl. name/number/e-mail/address/
birthday/notes/caller-id tags/revision/timestamps)
history (call history)
merge_contacts, merge_history
block_list, spam_list
social (+ column added in upgrade)
names_photos_cache
fresh_pics
recorded_notes
sms_table
ttm (TalkiToMe)
statistics_questions
dynamic_area_tickets, dynamic_area_subjects <-- §15.5
table_share_media <-- §14
table_api_requests
db_table_notification_reader, db_table_reverse_lookup_id
shared_preference (BLOB), shared_preference_v2

⇒ Everything in this list is inside the single contactdb file that §3 uploads verbatim to Google Drive.

20.4 Remote-config security findingsh5
SevFindingEvidence
CRITICALThe entire remote-control plane is fetched over cleartext HTTP from a single unauthenticated URL. Any on-path attacker (hostile Wi-Fi, ISP, transparent proxy) can rewrite ~400 behaviour flags. The concrete impact set is enumerated in 20.2 and includes: redirecting plaintext microphone audio, repointing the backend that receives the user’s cli+cid, and injecting arbitrary JavaScript into two WebViews that carry @JavascriptInterface bridges.p236d5/AbstractC15804d.java:300; p005a4/RunnableC0031c.java case 17; p620w6/C21273a.m38423G
HIGHThe remote-config fetch spoofs a desktop Chrome UA and uses 60 s connect/read timeouts, so it is also slow-failing and fingerprint-anomalous.C21273a.m38423G
MEDIUMshouldSendNameToServer is a remote flag that gates transmission of the user’s contact name to the backend. A network attacker can flip it either way.AbstractC15804d.java:420; RunnableC16004e.java:399
MEDIUMThe request-key constant rc_key = "PgdtSBeR0MumR7fO" is hardcoded in w6.a and sent as the e-auth-k header on every authenticated call; it is a static shared secret baked into every install.p620w6/C21273a.java (field rc_key)
LOWEyeconRemoteConfigVersion and the whole resolved config are kept in a plain SharedPreferences file - readable on a rooted device, and trivially rewritable to re-enter A/B buckets.d5.c.m31073A; RunnableC0031c SP writes

21. Consolidated C5 findings and explicit corrections to prior claimsh4

21.1 Corrections (each stated with its proof)h5
Prior claimVerdictProof
Briefing / C1: “f7.f0.m32019n wraps the backup key with RSA/ECB PKCS1v1.5 and encrypts backup content with Blowfish/ECB”WRONG. Those primitives exist but are used to encode error text before it is sent to the Eyecon error endpoint. The Google-Drive backup payloads are plain byte copies (no Cipher in m456G0, MIME application/x-sqlite3, and the restore copy is opened directly as SQLiteDatabase).§3.2
Briefing: “Photos … upload via uploadpic.jsp”WRONG for contact photos. uploadpic.jsp is the own-profile-picture endpoint (msisdn = clientId, raw JPEG of <filesDir>/.Eyecon/temp_photo.jpg). Contact photos go through addcontacts.jsp as part of the contacts JSON body.§6.2
Briefing: “Cluster C3 concluded RecordedCalls is scan/index/upload only”CONFIRMED, and sharpened. Re-verified independently: 1 new AudioRecord in the whole APK (Toki), 0 MediaRecorder, 0 MediaProjection, 0 VOICE_* audio sources. RecordedCalls does exactly four things: index via MediaStore/File, migrate to scoped storage, promote into RecordedNotes, and ACTION_SEND audio/* through the OS chooser. Its only egress is the §3 Google-Drive backup.§18.1-18.3
Briefing: “Ad config from cards.eyecon-app.com”CORRECTED. cards.eyecon-app.com is the Dynamic-Area cards config host (dynamic_area_url, POST /getCards). The ad configuration is Google AdMob/GAM via ca-app-pub-2959190743815944/* unit ids in remote config, mediated by FairBid/AdMob.§15.5, §16
Briefing: “p462ol is the Photos module”WRONG. ol is Glide’s Downsampler/BitmapLoader. The Photos module is n7 + m7 + com.eyecon.global.Photos.§4
Briefing: “ri is MoreMenuAndSettings”WRONG. ri is com.google.firebase.messaging internals (EnhancedIntentService, InstanceId topic subscribe, ProcessIntentHelper, GCM task prefs).§8.1
Briefing: “p8 is MoreMenuAndSettings”WRONG. p8 is a Facebook SDK + OkHttp HttpUrl + RxJava JADX merge.§8.2
Briefing: “Toki streams a live microphone”REFINED. It is push-to-talk, and the proof is tighter than the case-11 call-site argument: z7.l0.onTouch maps ACTION_DOWN -> m39404R0() and ACTION_UP -> m39403Q0() and consumes the event, and no timer/service/broadcast calls m39404R0() on its own. However there is one non-press path (answer-from-notification) that starts the mic with no release event.§2.1, §13.1
§2.3 (this doc, INFERRED): “e-auth-c is a per-connection signature/challenge response”CORRECTED to OBSERVED. C21273a.m38421E(e-auth) = split e-auth on -, sum Character.getNumericValue(first char) of each segment, return the decimal string. It is a trivially derivable checksum, not a cryptographic challenge. It adds no authentication.p620w6/C21273a.m38421E
§0 (this doc): “zq is MIXED”CONFIRMED and refined. 14/15 files are OkHttp platform/Platform classes; only AbstractC22312g (zq.g) is first-party (PermissionsHelper).§12
Briefing: “Contact-photo crypto: ObjectInputStream.readObject() reached over decryptable file bytes”CONFIRMED (RunnableC17750b case 10 path, §6.2 step 6). The key space is <=1000 because the middle component is Random().nextInt(999).§6.2, §6.4
21.2 Master findings table for cluster C5 (highest severity only, per area)h5
SevAreaFinding
CRITICALRemote configWhole control plane (400 flags) over cleartext HTTP, unauthenticated, with a spoofed desktop UA. Enables audio redirection, host repointing and JS injection. (§20.1-20.2)
CRITICALPhotosFacebook app secret 1716660955257637|afa3f38c55f24c73b177afc962969b15 hardcoded in strings.xml and used as a Graph access_token; also remotely overridable via fct. (§17.5)
HIGHPhotosPhotosTrackerWorker silently harvests Facebook profile photos of every Facebook-linked contact every 3 days and installs them as Eyecon avatars, with no prompt and no setting. (§17.4)
HIGHPhotosHidden Facebook mobile-web people-search in an invisible JS WebView with a spoofed Pixel 2 / Chrome 79 UA, once per contact name. (§17.2)
HIGHPhotosRemote-config-controlled arbitrary JS executed in a JS_ENABLED WebView carrying an addJavascriptInterface bridge. (§17.3)
HIGHPhotosContact photos at rest: Blowfish/ECB, no IV, <=1000 keys + shared external storage. (§6.4, §17.5)
HIGHTokiMicrophone (AMR) streamed over plaintext UDP with OAR/XOR FEC only, no DTLS/SRTP/AES/HMAC; host + port partly server-controlled. (§2.2, §2.5)
HIGHTokiThe e-auth session credential is replayed verbatim into every Toki signalling frame; e-auth-c is a trivial checksum and adds nothing. (§2.3, §21.1)
HIGHBackupThe entire contactdb (contacts, call history incl. geocoded_location + SIM id, SMS, block/spam, dynamic_area_*, table_share_media), the full call log JSON, the address book JSON and all .amr recordings are uploaded unencrypted to Google Drive appDataFolder. (§3, §20.3)
HIGHRecordingsRECORD_AUDIO is requested and a premium “actions” counter is decremented for a call-recording feature that does not exist in this build. (§18.5)
HIGHSocial shareServer-driven arbitrary-URL rendering in a JS_ENABLED WebView with a @JavascriptInterface bridge (GET /app/share -> table_share_media -> iframe). (§14.4)
MEDIUMAds8+ ad SDKs initialised unconditionally at app start; consent (npa=1) reaches only the AdMob extras bundle, not InMobi/Vungle/Pangle/Moloco/Singular. (§16.7)
MEDIUMAdsEvery AdManager request carries SIM country, manufacturer, Android release, network type and account-identity status as custom targeting. (§16.3)
MEDIUMPhotosThe photo flow writes the chosen postal address into the user’s system Contacts via applyBatch("com.android.contacts"). (§6.3)
MEDIUMPermissionsRuntime.exec("am start -n com.huawei.systemmanager/...") shell-out; OEM probing across 14 vendor package names. (§12.5)
MEDIUMTokiForeground notification is VISIBILITY_PUBLIC; FGS type 29 (microphone). (§13.3)
MEDIUMBackupRestore is a blind renameTo swap with rollback only on rename failure - no integrity gate. (§3.4)
LOWSettingsEvery settings toggle emits an eyecon://idPlus?source=<key> deep link (cross-surface telemetry channel). (§19.2)
LOWPermissionsConsent is a single writable SP boolean, and the consent text can be hidden by an intent flag. (§12.5)
LOWRecordingsRecording filenames embed contact name + phone number in cleartext. (§18.5)
21.3 Coverage accounting for this clusterh5
  • Briefing scope: 14 packages / 229 files - all 14 documented (§1 a6, §2 z7, §3 f5, §4 ol, §5 jh, §6 n7, §7 v6, §8 ri/p8, §9 dn, §10 ts, §11 u4, §12 zq, and p010a9/a9 in §8.3). File counts re-measured on disk and matching: 1+51+27+2+4+27+22+22+6+13+7+31+15+1 = 229.
  • First-party vs third-party split (final):
    • FIRST-PARTY MICO: z7 51, f5 27, v6 22, n7 27, jh 4, a6 1, zq.g 1 = 133 files
    • THIRD-PARTY SDK: ri 22 (Firebase Messaging), u4 31 (FairBid), ts 7 + dn 13 (Moloco/Crackle), ol 2 (Glide), p8 6 (Facebook/OkHttp/RxJava), zq 14 (OkHttp), a9 ~0.9 (FairBid/InMobi/Vungle merged) = ~95 files
    • FIRST-PARTY inside merge artefacts: n7.e (9-way merge, 1 first-party class), f5.n/f5.x (~0 first-party), a9 (1 first-party payload: the eyecon://idPlus settings-consent deep link), zq.g (~1 class of ~30 in the file)
  • Named wrapper modules covered: Photos (4 files), Toki (4), Backup (3), Ads (3), AdsIB (1), DynamicArea (3), Menifa (1), MoreMenuAndSettings (8), Themes (3), Spam (1), Permissions (1), SocialMediaShare (1), AudioRecording (1) = 34 wrapper files.
21.4 Cross-references to other clusters (no duplication)h5
TopicCanonical location
wl / yl / as.u2 UDP internals, jitter buffer, keep-alive, CHANNEL_*cluster C6a and §2.2-2.3 here
e-auth derivation server-side, cid, registration, addcontacts.jspcluster C1 / C2
f7.f0.m32019N, R.string.pk_enc, error-report scrubbingcluster C1 (§3.2 here states the correction)
contactdb schema in full, Room DB eyecon_roomDATA-LAYER.md (§20.3 here gives the table list)
Dialer / SMS / call state, CallStateService, IncomingSMSDTOcluster C3
MainActivity, MainFragment, ForYouFragment, HistoryFragmentcluster C4
Manifest, exported components, network_security_config, permissions declaredMANIFEST-AND-SECURITY.md
All api.eyecon-app.com/app/*.jsp endpoints, request/response shapesBACKEND-API.md
  • No MediaRecorder, no MediaProjection, no AudioManager.MODE_IN_CALL capture, no VOICE_DOWNLINK/UPLINK/CALL audio source. No call recording.
  • No TrustManager/HostnameVerifier/certificate-pinning override, no ALLOW_ALL, no OkHttp CertificatePinner bypass. No TLS weakening in any class in this cluster.
  • No keychain/keystore use for backup, contacts, photos, recordings or share media.
  • No contacts, call log, SMS, photo, location or device-ID payload assembled for any ad SDK.
  • No background socket, no SocketServer, no local network listener in this cluster.
  • No second hardcoded credential beyond R.string.fct (Facebook app secret) and rc_key = "PgdtSBeR0MumR7fO" (static request key).

22. Appendix - full endpoint / key / secret inventory for cluster C5h4

22.1 Endpoints touched by C5 codeh5
EndpointTransportWhere
https://api.eyecon-app.com/app/uploadpic.jspHTTPSown profile picture, msisdn = clientId, multipart/form-data, raw JPEG body (6.2)
https://api.eyecon-app.com/app/addcontacts.jspHTTPScontacts incl. photo bytes in the contacts JSON (6.2)
https://api.eyecon-app.com/app/join.jsp / getcontactslang.jspHTTPSsignup + language negotiation (6.2)
https://api.eyecon-app.com/app/shareHTTPSGET ?source=mysettings, social-share list (14.3)
https://voice2text.eyecon-app.com/start-ttmHTTPSTTM / voice2text {mycli,acli,calltag,purchases} (11, u4.o case 6)
https://cards.eyecon-app.com/getCards?HTTPSDynamic-Area cards {country,language,cli,cid,version_*,is_premium,cards,tags,bundle_id} (15.5)
http://cloudfront-cdn-all.eyecon-app.com/configs/ab-test.txtHTTP (CLEARTEXT), unauthenticatedremote config / A-B flags (20.1)
ptt.eyecon-app.com:<toki_ports>plaintext UDPToki voice channel + signalling (2.2)
https://graph.facebook.com/<socialId>/picture?width={50,200,600}&access_token=<fct>HTTPSFacebook avatar harvest (17.4)
https://m.facebook.com/search/people/?q=<name>HTTPS (WebView)Facebook people search scrape (17.2)
https://www.google.com/search?hl=en&udm=2&q=<term>HTTPS (WebView)Google Images scrape (17.3)
https://www.facebook.com/plugins/post.php?href=<url>HTTPS (iframe)share card (14.3)
https://www.instagram.com/embed.jsHTTPS (iframe)share card (14.3)
https://www.tiktok.com/oembed?url=<url>HTTPSshare card (14.3)
Google Drive appDataFolderHTTPS (Google CA trust store)backup/restore (3.3/3.4)
https://www.eyecon-app.com, /docs/private_policy.html, /docs/eula.html, https://faq.eyecon-app.com/HTTPSsettings/about links (7)
https://www.facebook.com/eyeconapp, https://m.facebook.com, https://www.instagram.com/eyeconapp/, https://www.tiktok.com/@eyecon.app, https://twitter.com/EyeconApp, https://www.youtube.com/channel/UCYI84aNWZ6Q0TxaCVTjiaFQHTTPSsocial links (7)
https://docs.google.com/forms/d/17QjnHJanVGPt0V_AkVj_baVmE2xZZGOyjAKjB22oeOU/viewform?edit_requested=trueHTTPSToki in-app feedback (2.5)
https://docs.google.com/forms/e/1FAIpQLSczgi6BudeGMX_kHxfkxQHRlrNS08Ib5NcTVHhhHjySIbJMCA/{viewform,formResponse}HTTPSwelcome_back_notification_url (20.2)
https://apps.eyecon-app.com/call-contest/a/report-callsHTTPScall_contest_report_calls_url (20.2)
https://api2.amplitude.com/2/httpapi, https://api.eu.amplitude.com/2/httpapiHTTPSAmplitude (11.2)
https://the-news-desk.com/js/displaytag{,_master}.min.js, https://s3.amazonaws.com/embed.footylight.com/aniview/footylight-eyecon-test-outstream-1.jsHTTPSmobitech_*_html_script remote-config ad creative (20.2)
https://www.google.comHTTPSac_ads_test_internet_connection_url (20.2)
https://youtu.be/2lxRSxgbxmgHTTPSinvite_youtube_link (20.2)
22.2 Hardcoded secrets / identifiersh5
ConstantWhereNote
R.string.fct = 1716660955257637|afa3f38c55f24c73b177afc962969b15res/values/strings.xmlFacebook app-id | app-secret, used as a Graph access_token. Remote-overridable via fct. CRITICAL.
R.string.facebook_app_id = 1716660955257637strings.xmlMeta app id
rc_key = PgdtSBeR0MumR7fOp620w6/C21273a.javastatic request-key header e-auth-k
R.string.pk_enc = MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A… (RSA-2048 SPKI)strings.xmlerror-scrubber public key (§3.2, not backup)
R.string.cipher_key = Ewustrings.xmlBlowfish contact-photo key prefix (§6.2)
R.string.admob_app_id = ca-app-pub-2959190743815944~6092014645strings.xmlGAM app id (26 unit ids in remote-config defaults)
R.string.amazon_app_key = 905c0238-13dd-4cf9-af3d-90f64672a4a9strings.xmlAmazon APS
R.string.applovin_key = UZvHsQBqsDCf0qv_…strings.xmlAppLovin SDK key
R.string.crackle_key = fai=1:917722272265:android:af67d7515574ed8c689ed8~~~fak=AIzaSyBxXQfYa0khnAZ2-NiK3WQV-4JbMWCw6ls~~~fpi=eyecon---eyecon-caller-id~~~s5=417687~~~s6=234363strings.xmlMoloco/Crackle (also embeds a Firebase API key)
R.string.adjust_app_token = m23t9uwrtvy8strings.xmlAdjust
R.string.app_vestor_api_token / _app_keystrings.xmlAppVestor
e-auth-c derivation: C21273a.m38421E(e-auth) = sum( Character.getNumericValue(first char of each "-"-separated segment) )p620w6/C21273a.javanot a cryptographic challenge (§21.1)
Hardcoded UA "Mozilla/5.0 (Linux; Android 8.0; Pixel 2 Build/OPD3.170816.012) … Chrome/79.0.3945.130 Mobile Safari/537.36"p420m7/C18525h.java:99spoofed, used for FB/Google scraping
Hardcoded UA "Mozilla/5.0 (Windows NT 10.0; Win64; x64) … Chrome/87.0.4280.88 Safari/537.36"p620w6/C21273a.java (m38423G)spoofed, used for all HttpURLConnection calls incl. remote config
"Blowfish" key = SecretKeySpec(("Ewu" + sp["cipher_key"] (0..999) + "sTQHGK").getBytes(), "Blowfish")p379k5/RunnableC17750b.java:400-415<=1000 key space
22.3 SharedPreferences keys touched by C5 code (non-exhaustive, all MyApplication.m10347I()/m10348J())h5

Preference and flag keys reached by C5 code. The list is not exhaustive; it covers every key passed to MyApplication.m10347I() or m10348J() in this cluster.

  • SP_KEY_NEED_UPDATE_PROFILE_PIC
  • SP_KEY_LAST_TIME_BACKUP
  • SP_KEY_STARTING_RESTORE_PROCESS
  • SP_KEY_ORIGALY_INSTALL_VERSION
  • SP_KEY_WAITING_FOR_CUSTOM_THEME_PENDING_PURCHASE
  • SP_KEY_IS_MISSED_CALL_NOTIFICATION_ENABLED
  • SP_KEY_LEGAL_MESSAGE_SHOWN_V2
  • SP_KEY_PREMIUM_ACTIONS_LEFT
  • SP_KEY_CALL_RECORDING_MENU_BUBBLE_SHOWN_V1
  • SP_KEY_SHOW_TIME_LIMIT_BANNER
  • SP_KEY_RECORD_CALLS_MODE
  • SP_KEY_CALL_RECORDING_MODES_BUBBLE_SHOWN
  • SP_KEY_DID_MIGRATE_TO_SCORE_STORAGE
  • SP_KEY_FIXED_RC_TIMESTAMP
  • SP_KEY_IS_CALL_RECORDING_ACTIVITY_ENABLE
  • SP_KEY_DID_LOVE_SURVEY_SHOWN
  • SP_KEY_IS_TOKI_ENABLED
  • SP_KEY_RV_APPEARANCES_BEFORE_CALLER_ID_ROLE_REQUESTS
  • SP_SCHEDULER_POPUP_SHOWN_AMOUNT
  • SP_KEY_DID_EYECON_CONSENT_APPROVED
  • SP_KEY_DA_LAST_TAGS
  • SP_KEY_DA_LAST_BUNDLE_ID
  • SP_REG_AUTO_START_SHOWN
  • SP_KEY_CLICKED_ON_AUTO_START_GO_BTN
  • SP_KEY_ORIGINALY_INSTALL_VERSION
  • Eyecon.isNeedAutoStart
  • cipher_key
  • clientId
  • authenticatedCli
  • fpt_last_tracked_cis
  • ptw_tracked_before
  • clicked_toki_btn_v2
  • fb_notification_cash_v2_*
  • fb_notification_EYECON_TYPES_v2
  • fb_notification_timestamp_five_stars
  • SP_KEY_SELECTED_QUOTES_LANGUAGE_v1
  • SP_KEY_CURRENT_APP_LANGUAGE_v4
  • "DrawAbove"
  • "fullScreenNotification"
  • "readNotification"
  • "PS-" + perm
  • "PS-Counter-" + perm
  • <perm> = "never_ask_again_mode"
  • "empty_bundle_id"
  • EyeconRemoteConfigVersion
  • "__GOOGLE_FUNDING_CHOICE_SDK_INTERNAL__"/"consent_status"
  • "spam_page"
  • "spam_pager"

Cluster C6a — Unclassified Packages, Part 1 of 2h3

APK: com.eyecon.global (MICO — caller ID / spam blocker) v4.2026.04.23.1145 Scope: 192 packages, 2,426 .java files (briefing docs/briefings/C6a-unclassified.txt) Tree: C:\Users\btc_e\Downloads\mico-analysis\src\sources

Methodh4

Obfuscated top-level packages are named on disk pNNN<short> where short is the JADX-original package name (e.g. p008a7 -> a7, as recorded in the /* JADX INFO: renamed from: ... */ comment in each file). Every package in the briefing was scanned for:

  • full import graph (which other pNNN packages it consumes — reveals the module graph)
  • API-surface regex scan for HTTP/net, crypto, WebView, DB/prefs, and privacy identifiers
  • manual full read for anything in priority classes 1-3

A second obfuscation axis exists: several third-party SDKs ship their own proguarded classes with short root package names (tc, sr, ro, zm, md, nc, mp, vg, vc, vi, th, …) and C17xxx/C20xxx numeric class-name suffixes. These are not in the C6a briefing but are consumed by briefing packages; they are named inline where relevant.


1. Executive summaryh3

Of the 192 packages in this briefing, only a small tail is MICO’s own code. The bulk is third-party code that R8 flattened into the app’s obfuscation namespace:

BucketPackagesNotes
Third-party libraries / SDKs177ExoPlayer (incl. Mintegral’s bundled fork), Glide, OkHttp, Ktor, RxJava2, kotlinx.coroutines/serialization/json, Lottie, Coil, okio, AndroidX/AppCompat/Material, Joda-Time, ZXing, ZXing-Code128, Guava, OpenCensus, protobuf encoders; Firebase (Crashlytics, Performance, Remote Config, In-App Messaging, Installations, JobScheduler transport); Google Mobile Ads + 10 mediation adapters; Meta/Facebook SDK + Audience Network; Mintegral; Moloco (tech.crackle); Vungle/Liftoff Monetize; Fyber/DT Exchange; InMobi; Pangle (ByteDance); Amazon Device Ads; Amplitude + Session Replay; Singular; Play Core; SLF4J; libphonenumber; iVorbis
FIRST-PARTY MICO9b6 (remote-config card schema), ct (Mintegral/Moloco mediation glue), dt (Inneractive/Fyber glue), s7 (Reminders card), s8 (spam-report flow), o6 (navigation-app chooser), o7 (feature config / cantalk), w4 (AdMob revenue wrapper), z5 (card-impression adapter)
FIRST-PARTY-authored glue over a 3rd-party SDK2xs, ys — MICO wrappers around Moloco tech.crackle RTB
Cannot be classified confidently4p293fh, gj, g8, yi (see section 5)
TOTAL1922,426 files (matches the briefing exactly)

The cluster is dominated by two ad-mediation stacks: Google Mobile Ads (AdMob/Ad Manager) acting as the mediation layer, and underneath it Mintegral, Moloco (tech.crackle), Liftoff Monetize (Vungle), Fyber/DT Exchange, InMobi, Pangle (ByteDance) and Meta Audience Network. MICO’s own code in this cluster is essentially the glue that drives those SDKs plus the remote-config “card” schema that decides what gets rendered.


2. NEW security-relevant findings (not already in the cross-reference list)h3

2.1 HIGH — Google Advertising ID is read and the Limit-Ad-Tracking flag is deliberately discardedh4

p256e1/C16062a.java (real pkg e1, Singular SDK device-info collector)

Object objInvoke2 = objInvoke.getClass().getMethod("isLimitAdTrackingEnabled", null).invoke(objInvoke, null);
Boolean bool = objInvoke2 instanceof Boolean ? (Boolean) objInvoke2 : null;
if (bool != null) { bool.booleanValue(); } // <-- result never tested, never stored
Object objInvoke3 = objInvoke.getClass().getMethod("getId", null).invoke(objInvoke, null);
this.f39537a = (String) objInvoke3; // <-- GAID always harvested

The identifier is fetched by reflection (Class.getMethod(...).invoke(...)) rather than a direct call, so the call is invisible to a static import/API-usage scan of the DEX. On Amazon devices it reads Settings.Secure limit_ad_tracking / advertising_id instead. Consequence: even a user who has enabled “Limit Ad Tracking” still has their GAID exported to the Singular attribution endpoint.

2.2 HIGH — Precise GPS location read and reverse-geocoded to a country code for ad attributionh4

p256e1/C16063b.java + C16062a.java

C16063b.m31416c() enumerates all enabled location providers and calls LocationManager.getLastKnownLocation() for each, then picks the freshest fix; on SecurityException it logs "Failed to get most recent location" and continues. C16062a then runs Geocoder.getFromLocation(lat, lon, 1) and takes Address.getCountryCode(), falling back to TelephonyManager.getPhoneType()==GSM → getNetworkCountryIso() → Locale.getCountry().

Requires ACCESS_COARSE_LOCATION or ACCESS_FINE_LOCATION (checked with ContextCompat.checkSelfPermission). The coarse-only grant is enough to get a country code — i.e. a location-derived signal can be collected without the “precise” toggle being on.

2.3 HIGH — Third-party SDKs read the Contacts provider for photos (no READ_CONTACTS needed)h4

p593v1/C20863m.java (Glide ContactPhotoDataSource-equivalent, real pkg v1)

ContactsContract.Contacts.openContactPhotoInputStream(resolver, uri, true) is invoked for contacts/lookup/*, contacts/#/photo, contacts/#/display_photo, phone_lookup/*. This is reachable through Glide model loaders, so any Uri that ends up in an image request can pull a contact’s photo. Combined with the contact-photo encryption already found in the Blowfish/ECB work, this is the read path behind that crypto.

2.4 MEDIUM-HIGH — Embedded voice/audio stack: AudioRecord capture + RTP/Opus transporth4

p633wl (wl) and p687yl (yl)

  • p633wl/RunnableC21441d.java holds an AudioRecord instance (audioRecord field) and a Recorder Stopped state, alongside amr_decoded.length, packet_jitter_avg_100, and signalling keys channel_id, invitation_id, session_id, CHANNEL_INVITE, CHANNEL_JOIN, CHANNEL_BROADCAST_COMMAND.
  • p633wl/C21450m.java hard-codes the app version 4.2026.04.23.1145 in the signalling client.
  • p687yl implements the RTP audio path: send Keep Alive, socket connected, jitter packet save/retransmit, fec none, close UDPSocket.

This is a push-to-talk / voice-message feature with a raw UDP RTP socket. Combined with p439nk (ZXing) and the signalling keys, MICO ships an in-app voice channel that can open the microphone and stream UDP audio. This deserves an explicit runtime-permission + consent review.

2.5 MEDIUM — Amplitude Session Replay (rrweb) is embedded: full screen recordingh4

p000/C0000a.java (real pkg p000, 2 files, 19 KB)

Imports com.amplitude.android.sessionreplay.config.ConfigManager, com.amplitude.android.internal.locators.ComposeViewTargetLocator, androidx.datastore.core.DataStore, and emits " rrweb events. " / " bytes reached, already sent" / "config" / "today". p667y0 confirms the plugin: "plugin-session-replay-android/0.20.12". Session replay masks inputs by default but still records view hierarchies and text; because MICO renders caller-ID cards, contact names, and phone numbers, a replay recording is a contact-graph + phone-number exfiltration surface on top of everything already collected.

2.6 MEDIUM — Google Advertising ID harvested twice more, by Amplitude and by Facebookh4

  • p028b1/C3405f.java (Amplitude core): event/device properties adid, android_id, android_app_set_id, app_version, carrier, city, branch.
  • p452o8/AbstractC19095f.java (Facebook SDK): advertiser_id, advertiser_id_collection_enabled, advertiser_tracking_enabled, anon_id, app_user_id.

So there are three independent GAID readers (e1/Singular, b1/Amplitude, o8/Facebook) with no shared consent gate visible in this cluster.

2.7 MEDIUM — Hardcoded Play Store signing-certificate public keys in a device-check helperh4

p387kf/C17844b.java

8P1sW0EPJcslw7UzRsiXL64w-O50Ed-RBICtay1g24M
GXWy8XF3vIml3_MfnmSmyuKBpT3B0dWbHRR_4cgq-gA
dev-keys
"Play Store package certs are not valid. Found these sha256 certs: ["

Play Core in-app-review / app-update code that refuses to run when the installed package is not signed by a Play-recognised key. Not itself an exploit path, but it means the app ships and compares against a pinned list of signing certs — a useful fingerprint for attribution/cloning.

2.8 LOW-MEDIUM — String-obfuscated Crashlytics endpoint tableh4

p602vb/C20945a.java

Literal constants such as hts/frbslgiggolai.o/0clgbthfra=snpoo, hts/frbslgigp.ogepscmv/ieo/eaybtho, AzSCki82AwsLzKd5O8zo, IayckHiZRO1EFl1aGoK alongside "Extra is not a valid encoded LegacyFlgDestination" — a character-shifted obfuscation of the CCT log endpoint plus two 20-byte constants that are used as transform keys. This is the same “hide the endpoint from string scanners” pattern already seen in the Blowfish key material; it means endpoint discovery by grep will under-report.

p450o6/AbstractC19085d.java (o6.d, FIRST-PARTY)

String str2 = "https://www.google.com/maps/dir/";
...
if (!str2.startsWith("https")) { ... return; }
str2 = "http://www.google.com/maps/dir/"; // cleartext retry

If the HTTPS intent throws, MICO silently retries the same navigation URL over cleartext HTTP.

2.10 LOW - Two further ad/user-info endpoints reachable from SDK codeh4

p382k8/C17793b.java (Mintegral): /privacy_sandbox/pa/logic, ?daily&app_id=, ?bidding, ?trusted_bidding. p708zd/p707zc are ExoPlayer (already-known).

2.11 MEDIUM - Moloco banner ad creative can drive host JavaScript and launch arbitrary intentsh4

p374js/C17700b.java (real pkg js, Moloco tech.crackle.cracklertbsdk) is a custom WebView subclass that calls getSettings().setJavaScriptEnabled(true). p374js/C17699a.java (a WebViewClient used both for the banner and for ShowFullScreenVideo) then calls, on onPageFinished:

webView3.evaluateJavascript("beginVideo(" + objValueOf + ", true)", null); // objValueOf from VAST/ad data
webView2.evaluateJavascript("countdownPaused = false;", null);

and on shouldOverrideUrlLoading it hands the creative-supplied URL straight to an external ACTION_VIEW intent with no scheme allow-list:

Intent intent = new Intent("android.intent.action.VIEW");
intent.setData(Uri.parse(url));
intent.addFlags(268435456); // FLAG_ACTIVITY_NEW_TASK
context.startActivity(intent);

The same class also registers the OM SDK viewability session with Partner.createPartner("crackletech", "1.5.2") and fires fireMraidReadyEvent / fireMraidStateChangeEvent?newAdState=.... Practical impact: any URL the ad creative navigates to (including non-http schemes the device has handlers for) is launched from MICO’s own task, and JS in the creative is executed with host-driven calls. There is no addJavascriptInterface bridge, so the exposure is intent-scheme-laundering rather than direct host object access - but a creative pointing at intent:// or a locally-registered scheme is a known Android abuse pattern.

2.12 INFO - p625wb is Amazon Publisher Services, not ExoPlayerh4

Correcting an initial mis-ID: wb (36 files) is the Amazon Device Ads library (com.amazon.device.ads.DtbDeviceData) bundled with Google Play services logging protos (ExternalPRequestContext, BatchedLogRequest, ComplianceData{privacyContext=}, LogSource). It contributes country, mccMnc, mobileSubtype, networkConnectionInfo (including EHRPD/BLUETOOTH/EVDO_*/ETHERNET discrimination) and complianceData to the logging payload, i.e. a carrier/MCC-MNC signal is collected on top of the already-known carrier collection.


3. Classification table (all 192 briefing packages)h3

FP = FIRST-PARTY MICO logic · 3P = third-party · ? = could not classify confidently

#Disk dirReal pkgFilesClassResponsibility / SDKNotable classes & markers
1p627wdwd483PExoPlayer UI — RecyclerView PlayerView adaptersAbstractC21367d0 extends RecyclerView.Adapter; listens on p344ic/p365jd
2p708zdzd163PExoPlayer display/AVC/HEVC (Surface.setFrameRate, avc1.%02X%02X%02X)already known (skip-list)
3p693yryr563PMoloco / tech.crackle SSP adapter wrappers (MICO-authored glue)C21986f1 implements CrackleAdListener; import tech.crackle.core_sdk.ssp.SSP
4p652xcxc293PMintegral’s bundled ExoPlayer2 extractors (TS/ADTS/WebVTT/FLAC)import com.mbridge.msdk.playercommon.exoplayer2...
5p324hghg803PKotlin stdlib collections (Iterable, Map, List internals)hg.a/hg.a2; uses ir.AbstractC17448b = Intrinsics
6omom693PKtor HTTP client core + pluginsio.ktor.client.plugins.HttpCallValidator, HttpPlainText, HttpTimeout
7gqgq793Pkotlinx.serialization-jsonimplements KSerializer, kotlinx.serialization.encoding.Decoder
8aoao533PRxJava 2 operators (PublishProcessor, Flowable, prefetch queues)rx2.buffer-size, Prefetch queue is full?!
9p547spsp743Pkotlinx.coroutines Flow/Channel operatorsBuffer size should be non-negative, BUFFERED, or CONFLATED
10p240d9d9103PAndroidX AppCompat R.attr ID table + RecyclerView/ViewGroup internal listenersRecyclerView.OnScrollListener, ViewGroup.OnHierarchyChangeListener; the android_id hit here is an AndroidX resource-ID constant (ViewStubCompat_android_id, Fragment_android_id), not device-ID access
11p590urur473Pkotlinx.serialization core (descriptors, SerializersKt)must include generic type, Dalvik/RoboVM checks
12p653xdxd303PExoPlayer MediaSession / playback-state (PlaybackStateCompat)android.support.v4.media.session.PlaybackStateCompat
13p707zczc193PExoPlayer MediaCodec async adapter + device quirks (baffin, c2.android.aac.decoder)MediaCodec.Callback, ExoPlayer:MediaCodecAsyncAdapter:
14p709zeze213PMaterial Components shape drawables / tint / rippleclass C22176g extends Drawable implements TintAwareDrawable
15p311h0h0303PLottie JSON parser / compositionLottie doesn't support expressions. / ... 3D layers.
16p548sqsq253POkHttp internal RealCall, connection pool, TLSHTTP/1.1, bytes but received, CONNECT tunnels
17p278f0f0103PLottie content model (__container, PRE_COMP, images/fonts)data URL did not have correct base64 format.
18p532rprp223Pkotlinx.coroutines buffered channelsBuffered channel capacity must be at least 1
19p680ydyd243PExoPlayer util (GL, MIME, XML pull, Exif)com.google.android.exoplayer2.util.GlUtil$GlException
20p229ctct32FPMICO ad-mediation glue — Mintegral/Moloco RTB + banner/interstitial/native wrappersssp=s5&core=1.2.0&sv=, ssp=s12&..., CrackleAdListener, onShowFailRewarded=
21p610vqvq253POkHttp internal HTTP/2 framesPROTOCOL_ERROR padding, :authority, |PADDED
22p695zz203PLottie content model__container (2nd half of Lottie model)
23p710zfzf253POpenCensus / OkHttp instrumentationCannot initialize default OpenCensus HTTP propagation text format.
24p257e2e2293PGlide downsampler, bitmap pool, video-frame decodeDownsampler.DownsampleStrategy, DfltImageHeaderParser
25p508qqqq83POkHttp cache (Cache is corrupt:), interceptor, DiskLruCacheconsumes MICO p033b6.C3475h HTML-text config
26p543s7s75FPMICO Reminders card — persists reminder JSON in SP_KEY_EYECON2_REMINDERSRemindersDataHandler, reminder_text, creationDate, da_card
27p642x1x1293PGlide core Request/Engine/ResourceYou cannot modify locked T, consider clone()
28p452o8o8143PFacebook SDK core (install referrer, app events, ad attribution)advertiser_id, advertiser_tracking_enabled, anon_id, fb_mobile_activate_app
29p029b2b2373PGlide model loaders + LazyHeadersFailed to find any ModelLoaders for model:, LazyHeaders{headers=
30p608vmvm323PKtor HTTP Url, media types, headersFail to parse url:, Transfer-Encoding, TRACE
31p659xjxj103PGoogle libphonenumber (PhoneNumberMatch, country code, Externalizable)PhoneNumberMatch [, Country Code Source:
32p626wcwc113PiVorbis / Ogg decoder + Moloco media request glueNo ogg page can be found., adRequestId
33p401ldld113PExoPlayer subtitles (application/cea-608, application/ttml+xml)Unexpected decode error
34p615w1w143PSingular SDK device-attribution handler + ad DBSingularDeviceAttributionHandler, kind = 1 AND image_id = ?
35p580udud103PExoPlayer cue / ASS / WebVTT stylingA style block was found after the first cue.
36p033b6b626FPMICO remote-config “card” schema — the JSON model that defines every on-call UI cardincall_fs_feed_admob_first_ad_id, balwan_bg_color, pre_defined_action, shouldSendName-style flags, eyecon://url?link=…
37p253dtdt21FPMICO Inneractive/Fyber (DT Exchange) SSP adapter glueInneractiveAdSpot.RequestListener, ssp=s6&core=1.2.0&sv=
38p634wmwm143PKtor HTTP client (multipart, header limits)Header line length limit exceeded
39p603vdvd223PExoPlayer device/track selectionBRAVIA, Sony, sys.display-size, audio/ac4
40p293fhp293fh1?Firebase/encoder-side model with address / arch / baseAddress / binaries / buildId (binary/ABI model)only 1 class, fully read, no behavioural code
41p396l1l1253PAmplitude identify + disk-backed event storeamplitude-identify-intercept-disk-queue, .tmp
42p588upup243Pkotlinx.coroutines core (Dispatchers, continuations)Module with the Main dispatcher is missing.
43p375kk273PCoil Compose image loadingcoil.compose.requestOf (Utils.kt:21), Success(painter=
44p635wnwn33PMoloco/Mintegral shared media-request glueadRequestId, $adUnitInfo
45p564tptp303Pkotlinx.coroutines Flow/Channel internalsonBufferOverflow=, AbstractSharedFlowSlot
46p641x0x0143PSQLiteOpenHelper (Mintegral/Glide ad cache) + ExoPlayer glueextends SQLiteOpenHelper, Attempt to re-create existing legacy database file
47p002a0a0183PLottie keyframes / interpolatorsThis animation does not support split dimensions!
48p265ebeb183PAdMob mediation adapter: DT Exchange / Fyber Marketplacecom.google.ads.mediation.dtexchange, OnFyberMarketplaceInitializedListener
49p272emem113POkHttp HTTP/2 connection + HPACKFRAME_SIZE_ERROR, Header index too large, :authority
50p488q0q0213PAmplitude analytics core (internal)heavy Amplitude dep graph
51p030b3b3153PExoPlayer core player + MediaPlayer compat + subtitle controllerCSJ_VIDEO_MEDIA, setSubtitleAnchor
52p483pjpj143PFirebase Performance sessions + HTTP instrumentationapp_quality, sampling_rate, firebase_sessions_*, HttpsURLConnection
53p433n8n893PMintegral (MTS) reporting parameter builder_audiencePropertyIds, _eventName_md5, _MTSDK_Default_
54p334i0i033Pokio JsonReader (Nesting too deep, Expected a boolean but was)okio JSON
55p308gjgj4?Small plugin/loader glue (Moloco↔Mintegral bridge), 4 classes, no behavioural logicPackageManager use only
56p625wbwb363PAmazon Device Ads / Publisher Services + GMS logging protos (ExternalPRequestContext, BatchedLogRequest, ComplianceData{privacyContext=}, LogSource)import com.amazon.device.ads.DtbDeviceData; android.util.JsonReader/SparseArray; EHRPD/BLUETOOTH/EVDO_* network-type enum
57p373jrjr193PJoda-Time core (chronology, time zones)centuryOfEra, Africa/Addis_Ababa
58p003a1a163PIAB MRAID + Google Mobile Ads bootstrap (loads mraid.js, omsdk-v1_5_2.js from assets, text/html WebViews)file:///android_asset/mraid.js, omsdk-v1_5_2.js
59p624wawa113PFyber / DT Exchange Marketplace SDK (Inneractive* listeners, initOmidSdk)Firing Event 801 - AdLoadFailed - errorCode, IAConfigManager
60p544s8s87FPMICO spam-report flow classifier — regex-logger/suggested-event detector + shareSpam preferenceSUGGESTED_EVENTS_HISTORY, pref_key_share_spam_time_ms, (?i)(confirm.*password)|(password.*confirm)
61p426nn133PCoil Android resource/asset fetchers (+ SHA-256 header hash)Invalid android.resource URI:, HTTP , SHA-256
62p436ndnd93PExoPlayer Mp4Webvtt / cue parsingIncomplete Mp4Webvtt Top Level box header found.
63p443oo103PCoil core (disk-cache key, fetcher factory, request interceptor)coil#disk_cache_key, coil#is_sampled
64p459ogog93PFirebase core (ComponentDiscovery, BackgroundDetector, StartupTime)Make sure to call FirebaseApp.initializeApp(Context) first.
65p369jjjj133PFirebase Remote Config (fetch/ETag/backoff, /namespaces/)abt_experiments_key, last_fetch_etag, Fetch is throttled.
66p306g8g85?5 tiny classes, no strings, no APIs — glue/model onlydepends on p358j5, p675y8
67p331hqhq243Pkotlinx.serialization-json internals (JsonLiteral, primitives)Expected input to contain a single valid number, but got
68p579ucuc83PExoPlayer ID3 TLEN frame readerSearched too many bytes.
69p666xsxs9FP-glueMICO Moloco RTB wrapper (banner/interstitial/rewarded listeners)CrackleRtbInterstitialAdListener, errMsg
70p467oror113PJoda-Time tz data providerorg/joda/time/tz/data, Corrupt zone info map
71p667y0y063PAmplitude core + session-replay plugin bridgeplugin-session-replay-android/0.20.12, 9774d56d682e549c, adid
72p687ylyl133PVoice/RTP audio transport — jitter buffer, FEC, keep-alive UDP socketsend Keep Alive, socket connected:, fec none, close UDPSocket
73p633wlwl143POpus codec + voice-channel signalling; holds an AudioRecordchannel_id, invitation_id, CHANNEL_INVITE, amr_decoded.length, 4.2026.04.23.1145
74p354j0j093PLottie offscreen renderer + animatorlottie-…-thread-, Cannot nest start() calls
75p719zpzp103Pokio segments/buffers/sinksonly owner can write, Source is closed.
76p721zszs123PGoogle Mobile Ads custom-format load callbacks (AppOpen/Interstitial/Rewarded/AdManager)extends AppOpenAd.AppOpenAdLoadCallback
77p439nknk83PZXing barcode encoders (EAN/UPC/CODE_128)Can only encode EAN_13, but got
78p353jj113PCoil image cache + request interceptorsimage_cache
79p411lplp193Pkotlinx.coroutines sequencesThis sequence can be consumed only once.
80p335i1i1113PAmplitude event pipeline (storage, retry, enrichment)Event storage file not found:, #!maxRetryAttemptReached
81p243dcdc93PFirebase JobScheduler transport backend (uploads metrics/logs)SELECT next_request_ms FROM transport_contexts, FirebaseCrashlytics
82p513r2r2133PAndroidX core (ArrayMap, IoUtils, executors)extends ArrayMap, File too large to map into memory
83p549tt153PCoil request/lifecycle (GlobalLifecycle)coil.request.GlobalLifecycle
84p498qdqd53PExoPlayer ASS/SRT cue parsing[V4+ Styles], Failed to parse color expression:
85p413mm83PDiskLruCache (okhttp-style)cache is closed, directory == null
86p310gogo183PRxJava 2 schedulers/pluginsrx2.computation-threads, RxCachedThreadSchedulerShutdown
87p450o6o63FPMICO navigation-app chooser (Google Maps / Waze / Sygic)EyeBottomNavigateDialog, google.navigation:q=, com.sygic.aura://, cleartext http://www.google.com/maps/dir/ fallback
88p289fdfd33Pmp4/ID3 metadata writer (TXXX, image/jpeg)AbstractC16257t1
89p028b1b173PAmplitude core — device/user properties incl. android_id, adid, carrier, cityamplitude-android-, amplitude-kotlin-
90p551t1t143PGlide GIF header parser (NETSCAPE2.0)You must call setData() before parseHeader()
91p428n2n243PGlide Resource / RequestManagerTo indicate failure return a null Resource object
92p480pcpc83PExoPlayer AVI extractorAVI Header List not found, video/mp42
93p346igig93PGuava BaseEncoding + ByteStreamsIllegal alphabet length
94p454obob133PAdMob mediation adapter: Pangle (ByteDance)PAGAppOpenAdLoadListener, MediaOpenAd
95p336i2i293PGlide GIF drawableBufferGifDecoder, StreamGifDecoder
96p520rbrb83PAdMob bidding adapter: Liftoff Monetize (Vungle)getBidResponse(...), getWatermark(...)
97p593v1v1133PGlide data fetchers — incl. ContactsContract photo fetcher and HttpUrlFetcherContactsContract.Contacts.openContactPhotoInputStream, contacts/lookup/*, phone_lookup/*
98p617w3w373PGlide scheduling/CronField helpersCronField does not belong to day of week
99p679ycyc53PExoPlayer WAV reader + block alignmentUnsupported or unrecognized wav file type.
100p323hfhf163PGoogle Play Core In-App Review / App Update / request monitorerror requesting in-app review for %s, AppUpdateService
101p537s1s163PGlide disk cache (glide-disk-lru-cache-thread)LineReader is closed
102p451o7o74FPMICO feature config — cantalk_c/cantalk_n/cantalk_q, authentication, new_photo, share_media, call-log cardn_pics, callLog, eyecon://share, received
103p503qlql53PGlide animated AppCompatImageView / GifDrawableAnimation duration cannot be negative value.
104p629wgwg113PAndroidX / Guava SequentialExecutor, ScheduledExecutorServiceShutting down is not allowed.
105p042c2c273PGlide HttpGlideUrlLoader + MediaStore fetcherHttpGlideUrlLoader.Timeout, ACCESS_MEDIA_LOCATION
106p521rcrc43PExoPlayer MP3/ID3 (filepositions, keyframes)Audio format not supported:
107p565tqtq73POkHttp interceptor (100-continue, date parsing)100-continue, EEE dd MMM yyyy HH:mm:ss z
108p276eqeq123Pkotlinx.serialization descriptorsSerialDescriptor, descriptor, serialName
109p374jsjs33PMoloco banner WebView — custom WebView + WebViewClient for click/video trackingCLICK_BANNER_EXCEPTION, clickEndCard, beginVideo(
110p287fbfb143PAdMob mediation adapter: InMobi (banner/native, marketplace init)InMobi SDK failed to download native ad image assets.
111p663xpxp113Pkotlinx.coroutines sync (Mutex, Semaphore)Semaphore should have at least 1 permit
112p349ijij63PFirebase Installations (FID: last_fetch_status, fetch_timeout_in_seconds, _fpc)firebaseinappmessaging-adjacent FID store
113p651xbxb173PFirebase Crashlytics metrics proto (appNamespace, logSourceMetrics, eventsDroppedCount)Base64.encodeToString(bArr, 2)
114p618w4w44FPMICO AdMob revenue wrapper — maps ad types to AdRevenueEvent$AdTypegetAdType$app_googleRelease()Lcom/eyecon/global/AdsIB/AdRevenueEvent$AdType;, gam_ad_types_loading_mode
115p440nmnm83POkHttp Android engine bridgecouldn't send a body with the [Android] engine.
116p296fqfq43Pkotlinx.serialization Decoder/Encoder interfaces'null' is not supported by default
117p041c1c183PFacebook SDK Core + ConnectivityManager.NetworkCallback monitorAndroidNetworkListener, onNetworkAvailable., com.facebook.AccessTokenManager.SharedPreferences
118p255e0e0163PLottie model (ShapePath, MergePaths, TrimPath)Animation contains merge paths but they are disabled.
119p589uquq63POkHttp HTTP/1 codec + chunked writerDuplex connections are not supported for HTTP/1
120p378k2k2113PMeta Audience Network RM plugin + fragment (SupportRMFragment, ConnectivityMonitor)You cannot start a load for a destroyed activity
121p583uiui63PFirebase Performance FrameMetricsAggregator / fragment monitorFailed to record frame data for %s., _tsns
122p377k1k153PAmplitude identify interceptor ($identify, $groupidentify)Event is invalid for missing information like userId and deviceId.
123p658xixi23PAmplitude prefs-backed logger/SuccessContinuation multiplexeramplitude-events-
124p712zhzh153PFirebase In-App Messaging display (FirebaseInAppMessagingDisplay) + metricsFIAM.Display, BANNER_PORTRAIT/CARD_PORTRAIT
125p355j1j193PAmplitude event builder ($clearAll, $set, $unset)$groupidentify
126p655xfxf43PMintegral / Google API-client consent plumbingcom.google.api.client
127p668y1y1103PGlide bitmap pool (LruBitmapPool, array pools)Cannot pool recycled bitmap
128p595v3v323PSmall preconditions/interval utility (Glide-ish)The end instant must be greater or equal to the start
129p497qcqc23PExoPlayer FLAC extractorFailed to read FLAC stream marker.
130p694ysys4FP-glueMICO Moloco SSP wrapper (ssp=s13)ssp=s13&av=1.2.0.0
131p692yqyq53POkHttp AndroidPlatform (SSL socket factory, trust roots, CloseGuard)buildTrustRootIndex, .OpenSSLSocketFactoryImpl
132p039bpbp113Pokio Path/FileSystemCheck failed.
133p657xhxh83PFirebase In-App Messaging click/view metrics loggermessage click to metrics logger, FIAM.Display
134p409lnln153POpenCensus tracing (SpanContext, TraceComponentImpl)Couldn't load lite implementation for TraceComponent
135p568u0u043PAmplitude session-replay UI element/screen tracking (GestureDetector)[Amplitude] Element Interacted, [Amplitude] Hierarchy
136p407llll113PGlide animated drawable / GIF plumbingconsumes p503ql
137p600v8v853PFacebook Core Diagnostics (analysis_log_, /instruments)app_version, callstack, device_model
138p242dbdb53PAdMob mediation adapter: Meta Audience Network (banner/interstitial/native)Ad from Meta Audience Network doesn't have all required assets.
139p239d8d843PFyber / Inneractive marketplace SDK internalsInneractiveMediationDefs.GENDER_FEMALE
140sksk23Pzlib/PNG CRC + scanline helper (media image decoder)Invalid mask pattern, 0 polynomial
141p660xmxm63PKtor CIO buffers/char-array poolingktor.internal.cio.disable.chararray.pooling
142p000p00023PAmplitude Session Replay (rrweb screen recorder)com.amplitude.android.sessionreplay.config.ConfigManager, rrweb events.
143p368jiji133PFirebase In-App Messaging rate limiting / campaign storeAPP_FOREGROUND_ONE_PER_DAY_LIMITER_KEY
144p329hoho43PRxJava 2 core reactive interfaces2.12 violated: onSubscribe must be called at most once
145p294flfl73PFyber/Inneractive identity token client — AES/GCM/NoPadding + HttpsURLConnectionRefresh token expired, optout, Identity refreshed
146p051cjcj73PFirebase component registry / provider discoveryFirebase Background/Blocking/Lite/Scheduler
147p628wewe83PMaterial Components text-input / theme attrsTheme.MaterialComponents theme error
148p509qrqr33PSLF4J ServiceLoader provider lookupdoes not implement SLF4JServiceProvider interface
149p613ww133PGlide util / device-config tablesA3_Pro, A30ATMO, A6L-C, /proc/self/fd
150p682ygyg43PFirebase Crashlytics thread-check / ANR monitorFirebaseCrashlytics, interestedThreads
151p256e1e133PSingular SDK device-info collector — GAID (reflection, ignores LAT), App Set ID, GPS-derived country, TelephonyManagergetAdvertisingIdInfo, getAppSetIdInfo, getLastKnownLocation, isLimitAdTrackingEnabled (result discarded)
152p535ss53POkHttp cache-control (Date, Expires, If-None-Match)Unexpected header:
153p525rhrh73PFirebase protobuf @Protobuf annotation encoders@com.google.firebase.encoders.proto.Protobuf(tag=
154p501qhqh53PFirebase encoders core (ValueEncoderContext)Cannot encode a second value in the ValueEncoderContext
155p533rqrq33PGuava TaskRunnerCheck failed.
156p350ikik63PZXing Code128 writerCan only encode CODE_128, but got
157p326hihi93PFirebase In-App Messaging API clientfirebaseinappmessaging.googleapis.com, X-Goog-Api-Key, X-Android-Cert
158p559tete103PMaterial bottom-sheet behaviour + animatorOnBackAnimationCallback
159p271ekek53PFirebase In-App Messaging protobuf campaign messagescampaignId_, triggeringConditions_, experimentPayload_
160p384kbkb43PAdMob mediation adapter: Mintegral (AppOpen, native w/ code)Mintegral SDK failed to return a native ad.
161p299g1g183PAmplitude session store (last_event_id, opt_out)app_build, previous_session_id
162p612vsvs103PAdMob native mediation adapter (mintegral/moloco family)NativeAdMapper, NativeAd.Image
163p228cscs63PViewability/OMID measurement — WebChromeClient, ViewabilityRule, HTTP fetchpersistent-storage-current-file-uri, HttpURLConnection
164p530rmrm93PKtor server engine attributes (HttpRequestData, HttpResponseData)io.ktor.util.AttributeKey
165p496qbqb23PAdMob adapter base: Liftoff Monetize (Vungle)Failed to load bidding banner ad from Liftoff Monetize.
166p536s0s053PCompose UI test / view-hierarchy finderError while locating target in view hierarchy:
167p227cqcq93Pkotlinx.serialization polymorphic serializerskotlinx.serialization.PolymorphicSerializer(baseClass:
168p713zizi53PFirebase Performance trace + URL samplinggetResultUrl throws exception, firebase_performance_whitelisted_domains
169p505qnqn153PRxJava 2 core (Flowable, Observable, Scheduler)rx2.buffer-size, rx2.scheduler.drift-tolerance
170p297fsfs23PAd impression/creative model (imp_url, creative_width, q1_url)ad-server response model
171p403lflf93PPlay Core install-time service binding (linkToDeath, binder death)PhoneskyVerificationUtils
172p602vbvb33PFirebase CCT/Crashlytics backend with obfuscated endpoint table + TelephonyManagerhts/frbslgiggolai.o/0clgbthfra=snpoo, LegacyFlgDestination
173p245dgdg53Porg.json (JSONTokener, JSONStringer)Token is not a number
174p292fgfg33POkHttp canonicalisation / EscaperAlphanumeric ASCII characters are always 'safe'
175p270ejej73PAndroidX Compose frame metrics (_fr_fzn, _fr_slo, _fr_tot)ViewTreeObserver.OnDrawListener
176p328hlhl53PFyber/Inneractive identity storeuid2_identity.json, euid_identity.json, identity_status
177p522rdrd13PExoPlayer TTML cue parserSkipping invalid index:, <br>
178p701z5z51FPMICO card-impression RecyclerView adapteronCardSeen position = %s, data = %s, lifecycle-gated attach/detach logging
179p361j8j823PGoogle Play services Measurement (gps_ara_* outcome receiver)Failed to get measurement manager
180p382k8k823PMintegral user-info / protected-audience collection/privacy_sandbox/pa/logic, ?daily&app_id=, ?trusted_bidding
181p461ojoj53PFirebase Crashlytics session + component dependency graphEnum: CRASHLYTICS / MATT_SAYS_HI / PERFORMANCE
182p370jkjk33PAndroid graphics Rect/Matrix + a BaseEncoding charset enumNum bits must be between 0 and 32
183p049cbcb43PAdMob adapter: Meta Audience Network (init + rewarded)AudienceNetworkAds.InitListener
184p456odod23PExoPlayer ID3 frameextends AbstractC18336f
185p684yiyi3?OkHttp method-set wrapper over Firebase-Perf/Amplitude classes; no distinct behaviourCONNECT/DELETE/HEAD/OPTIONS
186p387kfkf33PPlay Core in-app review + hardcoded Play cert fingerprints8P1sW0EPJcslw7UzRsiXL64w-O50Ed-RBICtay1g24M
187p616w2w273PExoPlayer SurfaceView/TextureView wrappersSurfaceHolderCallback, TextureViewSurfaceTextureListener
188p507qpqp43Pkotlinx.coroutines Dispatchers.MainThe main looper is not available
189p298g0g053PLottie network cache (gzip/zip decoding)lottie_cache_, lottie_network_cache, application/gzip
190p248dldl53PFyber/Inneractive identity package modeladvertising_token, refresh_token, Phone(phone=
191p495q8q843PFacebook Core ReceiverService / custom eventsfb_mobile_purchase, CUSTOM_APP_EVENTS
192p569u1u1113PGlide Options (PREFER_ARGB_8888, DISPLAY_P3)Options{values=

4. Deep entries (priority classes 1-3)h3

4.1 FIRST-PARTY MICO — p033b6 / b6 — remote-config “card” schema (26 files)h4

The single most important first-party package in this cluster. AbstractC3476i is the base card model (card_description, card_path, action_button) and there are ~20 concrete card subclasses, each a JSON-driven layout descriptor:

ClassCard typeNotable remote keys
C3468aAd feed carddisabled_by_remote, home_feed_google_gam_combine_unit_ids, incall_fs_feed_admob_first/second/third_ad_id, incall_fs_feed_admob_endless_ad_id, incall_fs_feed_gam_endless_ad_id
C3469bImage/lottie cardbackground, foreground_lottie, isNewUserCard
C3470cCaller/date cardbalwan_bg_color, balwan_color, dateCall, pre_defined_action
C3475hHTML text blockis_html_text, text_color, horizontal_frame_percent, headers, referrer, size
C3478kDeep-link cardeyecon://url?link=https://faq.eyecon-app.com/, eyecon://url?link=https://player.vimeo.com/video/7462615…?h=ee63579ec3
C3479lYes/no question cardquestion, yes_button, no_button, use_photo_button, change_photo
C3483pReminder cardadd_first_reminder_button, content_reminder, bell_lottie
C3484q/C3485rIncoming-call answer screennameText, phoneText, messageText, bottomMessageText, first_place_bar_color
C3486s, C3492yTop-3 callers / podiumfirst_place_name, first_place_number, second_place_number, third_place_name
C3487tContact statcontact_name, value_name
C3491xPie chartloser_bubble_color, pie_winner_color, loser
C3493zFree-form HTMLchange_display_style, contentHtml, cors
EnumC3480mCard trigger enumautostart, battery_optimization, caller_id, da_permission_dismissed_, default_dialer, default_dialer_with_caller_id_priority, draw_above, notification_reader

Security relevance: the server can render arbitrary HTML into the call screen (is_html_text, contentHtml, and a cors flag) and can attach arbitrary headers + referrer to the fetch. C3493z performs the HTTP fetch with org.json and case-insensitive content-type matching. This is a remote HTML injection channel into the post-call / incoming-call UI, guarded only by whatever WebView policy the renderer uses. Combined with caller_id / default_dialer triggers, a compromised or MITM’d remote-config endpoint can rewrite the UI that the user trusts to decide whether a number is spam.

4.2 FIRST-PARTY MICO — p544s8 / s8 — spam-report flow (7 files) — privacyh4

  • C20076a holds the heuristic log-classifier regexes used to decide what a screen is: (?i)(confirm.*password)|(password.*(confirmation|confirm)|confirmation), (?i)(sign in)|login|signIn, (?i)(sign.*(up|now)|registration|register|(create|apply).*(profile|account)|open.*account|account.*(open|creation|application)|enroll|join.*now).
  • C20077b/C20078c/C20079d persist predicted events into shardPreferences key SUGGESTED_EVENTS_HISTORY (shard index _filteredKey, eligible_for_prediction_events, production_events).
  • RunnableC20081f — the actual spam report:
ArrayList arrayList = new ArrayList();
arrayList.add(new C21872a("name", str)); // reported caller NAME
arrayList.add(new C21872a("cli", buttonText)); // reported CALLER ID
C21273a.m38422f(arrayList, ...+"name_report", ShareTarget.METHOD_GET, ...);
...
sharedPreferences.m32080g("pref_key_share_spam_time_ms", System.currentTimeMillis());
C21867v.m39108u(3, "shareSpam", str, buttonText);

So when a user taps “Report spam”, the reported name and the reported phone number are placed in a URL query string and issued as an HTTP GET. This is the same anti-pattern already recorded for cantalk.jsp?recvcli=…&message=<SMS body>, and it is a first-party path rather than an SDK one. ShareTarget.METHOD_GET comes from androidx.browser.trusted.sharing.ShareTarget — the constant name is just R8 reusing an unrelated enum, but the METHOD_GET value is what is passed.

4.3 FIRST-PARTY MICO — p701z5 / z5 — card-impression adapter (1 file)h4

AbstractC22072a extends RecyclerView.Adapter implements LifecycleEventObserver. On onViewAttachedToWindow/onViewDetachedFromWindow gated on Lifecycle.Event state it logs:

C18416a.m34588y(this.f60180d, "onCardSeen position = %s, data = %s", Integer.valueOf(i3), abstractC3476i);

i.e. the entire card descriptor (including phoneText, nameText, messageText, ad unit ids and Lottie/asset ids from b6) is written to logcat on every impression. On a stock device logcat is readable by any app with READ_LOGS pre-4.1 or by ADB; on rooted/enterprise-managed devices it is a standing leak of the user’s call history.

4.4 FIRST-PARTY MICO — p229ct / ct and p253dt / dt — mediation glue (53 files)h4

These two are the MICO-authored wrappers that actually drive Mintegral, Moloco and Inneractive/Fyber. Each ad format gets its own class per SSP, and every request is tagged with an SSP id string:

  • ct: ssp=s5&core=1.2.0&sv=, ssp=s12&core=1.2.0&sv=, plus &av=1.2.0.0; implements CrackleAdListener, CrackleUserRewardListener, BidListennning, NewInterstitialListener, MBSplashLoadListener, BannerAdListener, NativeListener.NativeAdListener.
  • dt: ssp=s6&core=1.2.0&sv=; implements InneractiveAdSpot.RequestListener, InneractiveAdViewEventsListenerWithImpressionData, InneractiveFullscreenAdEventsListenerWithImpressionData.
  • C15763d0 / C15767f0 / C15777n / C15778o surface Moloco errors as onShowFailInterstitial=, onShowFailRewarded=, onShowFailRewardedInterstitial=.
  • C15785v reads a bid token (p0.bidToken) and logs Has no bid.

So the SSP slot map observed in this cluster is roughly s5/s12 = Moloco (Crackle), s6 = Fyber/Inneractive, s13 (p694ys) = another Moloco variant.

4.5 FIRST-PARTY MICO — p618w4 / w4 — AdMob revenue adapter (4 files)h4

Wraps AdManagerAdView / NativeAd, registers an OnPaidEventListener, and reflects over MICO’s own com.eyecon.global.AdsIB classes:

getAdType$app_googleRelease()Lcom/eyecon/global/AdsIB/AdRevenueEvent$AdType;
getLocationInApp()Lcom/eyecon/global/AdsIB/AdLocationInApp;

It records adUnitId, the AdType enum name, responseInfo.getMediationAdapterClassName() (i.e. which SSP won the impression) and the in-app location for every paid event. It also contains an explicit OS gate:

... new C20869c("Pangle ad is skipped on Android 8 and below")

— confirming Pangle is a live demand source and that MICO is filtering it by OS version.

C21253c also reads gam_ad_types_loading_mode, is_adaptive_ads_enable, only_adaptive_banner, only_fix_size_banner, MEDIUM_RECTANGLE from remote config.

4.6 FIRST-PARTY MICO — p450o6 / o6 — navigation-app chooser (3 files)h4

o6.c is a bottom-sheet dialog (EyeBottomNavigateDialog, layout navigation_app_chooser) that queries PackageManager.queryIntentActivities for a geo:0,0?q=… intent and hides the “Sygic/Google Maps/Waze” options that are not installed. o6.d builds the intents:

  • Google Maps: google.navigation:q=<uri> scoped to com.google.android.apps.maps
  • Waze: https://waze.com/ul?navigate=yes&q=… scoped to com.waze
  • Sygic: com.sygic.aura://coordinate|lat|lon|drive
  • fallback: https://www.google.com/maps/dir/?destination=…&api=1&travelmode=driving&dir_action=navigate, downgraded to http://www.google.com/maps/dir/ on failure (see §2.9)

The chosen provider is persisted in SP_KEY_DEFAULT_NAVIGATION_APP.

4.7 FIRST-PARTY MICO — p451o7 / o7 — feature config (4 files)h4

o7.a defines the remote feature-flag triple used by the call-blocking feature:

authentication | cantalk_c | cantalk_n | cantalk_q

o7.b AbstractC16130b defines new-user UI (n_pics, New photo, new_photo, received); o7.c handles eyecon://share + share_media/share_media1 deep links. Note the callLog token: this is the switch that turns the post-call log card on, i.e. the remote config that decides whether call history is surfaced to the user (and therefore what the app is allowed to read).

4.8 FIRST-PARTY MICO — p543s7 / s7 — reminders card (5 files)h4

RemindersHandler (C20075g) serialises reminders as a Gson JsonArray and writes the whole blob into SharedPreferences key SP_KEY_EYECON2_REMINDERS after every add/remove:

c11859p.m14611q("reminder_text", new String(str2.getBytes(StandardCharsets.UTF_8), StandardCharsets.ISO_8859_1));
sharedPreferencesEditorC16496x.m32076c(string2, "SP_KEY_EYECON2_REMINDERS");

Reminders are free-text user input. They are stored unencrypted, latin-1 round-tripped in a plain SharedPreferences file, on the same store that (per the cross-reference) holds cipher_key. Reminder text is also the kind of content that ends up in a screen-replay recording (§2.5).

4.9 p256e1 / e1 — Singular SDK device-info collector — privacy, read fullyh4

Three classes: e1.a (the collector), e1.b (the location/provider helper), e1.c (logger). Data assembled in e1.a, field by field:

FieldSource
f39537aGoogle Advertising ID via reflection; on Build.MANUFACTURER == "Amazon" uses Settings.Secure advertising_id
f39538bCountry code — Geocoder reverse-geocode of getLastKnownLocation(), else TelephonyManager.getNetworkCountryIso() when getPhoneType()==2, else Locale.getCountry()
f39539cPackageManager.getPackageInfo(...).versionName
f39540d..f39543gBuild.VERSION.RELEASE, BRAND, MANUFACTURER, MODEL
f39544hTelephonyManager.getNetworkOperatorName()
f39545idevice Locale.getLanguage()
f39546jGoogle App Set ID (AppSet.getAppSetIdInfo().getId())

Everything except GAID/AppSetId is fetched through Class.getMethod(...).invoke(...) so that the GMS dependency is resolved at runtime — which is why a normal static API scan misses it. Two caveats worth flagging in review:

  1. isLimitAdTrackingEnabled() is read and then thrown away (§2.1).
  2. The location branch only executes when e1.b.f39549b is true, but f39550c (collect GAID) and f39551d (collect App Set ID) are independent booleans, so a caller can request identifiers without requesting location.

4.10 p593v1 / v1 — Glide data fetchers — privacy, read fullyh4

13 classes. v1.k (C20861k) is HttpUrlFetcher: builds a HttpURLConnection wrapped in FirebasePerfUrlConnection.instrument(url.openConnection()), disables redirects and caps them at 5, applies caller-supplied request headers (so arbitrary Authorization/Cookie can ride along), and sets setUseCaches(false).

v1.m (C20863m) is the contact-photo data source (see §2.3) — it is the only class in the entire 192-package cluster that touches ContactsContract.

4.11 p633wl / wl + p687yl / yl — voice channel — privacy, readh4

wl (14 files, C21438a–C21451n): Opus encode/decode (AbstractC20288j subclasses = Opus codec tables), AMR length bookkeeping, AudioTrack.OnPlaybackPositionUpdateListener, a RunnableC21441d that owns the AudioRecord, and C21450m which is the signalling client — channel_id, invitation_id, session_id, sessions, stream_id, CHANNEL_INVITE, CHANNEL_JOIN, CHANNEL_BROADCAST_COMMAND, and the literal app version 4.2026.04.23.1145.

yl (13 files, C21942a–C21954m): the RTP layer. Strings show a full WebRTC-style audio path over raw UDP:

send Keep Alive socket connected: / socket sync failed:
audio packet1 saved: audio packet2 saved:
jitter packet saved: for socket id:
close UDPSocket fec none retry send count

There is no TLS in this transport. Recorder Stopped + AudioRecord means the feature can open the mic; the audio itself travels as Opus over unencrypted UDP.

4.12 p294fl / fl — Fyber/Inneractive identity — cryptoh4

fl (7 files) is the identity-token client: AES/GCM/NoPadding (the only strong cipher found in this cluster — contrast with the app’s own Blowfish/ECB), application/json, and a hard requirement for HttpsURLConnection ("HTTPS connection not available"). State machine:

EnumC16650c : NetworkRequest(type=…) C16651d : NetworkResponse(code=…)
C16653f : Identity established | Identity refreshed | Refresh token expired | User opt out
C16654g : ResponsePackage(identity=…)
EnumC16652e : success | unauthorized | invalid_token | expired_token | optout | client_error

fl.a throws if HTTPS connection not available, so the identity channel itself is TLS-pinned in practice. The identity material (advertising_token, refresh_token, refreshFrom, refresh_expires, identity_expires) is persisted by p248dl (IdentityPackage(valid=…)) and the status cached to euid_identity.json / uid2_identity.json by p328hl.

4.13 p602vb / vb — obfuscated Crashlytics/CCT backend — cryptoh4

C20945a implements InterfaceC21688k and carries the string table hts/frbslgiggolai.o/0clgbthfra=snpoo, hts/frbslgigp.ogepscmv/ieo/eaybtho, AzSCki82AwsLzKd5O8zo, IayckHiZRO1EFl1aGoK, guarded by "Extra is not a valid encoded LegacyFlgDestination". C20947c is the transport backend (CctTransportBackend, connectivity, country, application_build) and is one of only four classes in the cluster that touches TelephonyManager.

4.14 p641x0 / x0 — SQLite ad cacheh4

The only SQLiteOpenHelper subclass in the cluster (C21558f, extends SQLiteOpenHelper), with Attempt to re-create existing legacy database file. It sits in the Mintegral/Glide ad-cache layer (p2158x also carries ExoPlayer CrackleRtbInterstitialAdListener glue and io.reactivex imports), so the schema is SDK-owned, not MICO-owned.

4.15 p003a1 / a1 + p228cs / cs + p374js / js + p000 / p000 — the four WebView/recording surfacesh4

PackageWebView role
a1Loads file:///android_asset/mraid.js and omsdk-v1_5_2.js and serves ad creatives as text/html in WebViews (IAB MRAID + OM SDK). Also the Google Mobile Ads bootstrap (all_lib_included, applinks_lib_included, billing_client_lib_included).
csViewability/OMID measurement: WebChromeClient, ViewabilityRule(eventId=…, requiredPercentage=…, requiredDurationMs=…), and a raw HttpURLConnection fetch.
jsMoloco’s banner WebView: custom WebView subclass + WebViewClient implementing CLICK_BANNER_EXCEPTION, clickEndCard, beginVideo(. Note adData, changedView, bannerWebViewListener are exposed as fields.
p000Amplitude Session Replay (rrweb) — see §2.5.

Correction after full read of js (see 2.11): p374js/C17700b.java does call getSettings().setJavaScriptEnabled(true), and p374js/C17699a.java calls evaluateJavascript(...) twice. No addJavascriptInterface was found anywhere in the four packages (so there is no classic JS-bridge object exposed to the creative), but JS is enabled and is driven from the host.

4.16 om / p608vm / p634wm / p530rm / p660xm — Ktor HTTP client (142 files total)h4

MICO ships a second, independent HTTP stack (Ktor, JetBrains) alongside OkHttp and alongside HttpURLConnection. om is io.ktor.client.* core; vm is ktor.http.* (Url, media types, Transfer-Encoding, TRACE); wm is the client engine + multipart (Header line length limit exceeded, boundary=); rm is io.ktor.server.engine.* attributes (HttpRequestData(url=…), HttpResponseData=(statusCode=…)); xm is ktor.internal.cio.* buffers.

Two notes:

  • om includes io.ktor.client.plugins.HttpPlainText and DefaultRequest — the Ktor stack is capable of cleartext HTTP. Whether MICO’s own code ever selects HttpPlainText cannot be determined from this cluster (the call site is in an already-documented package), but the capability being reachable means the “all remote config is cleartext HTTP” finding has at least three independent client stacks that could each be the one doing it.
  • om also contains DefaultResponseValidation / HttpCallValidator with " because of security downgrade" — again a capability, not a call site.

5. Packages I could not classify confidently, and whyh3

PackageWhyWhat I ruled out
p293fh (1 file, 81 KB)Single class; a plain data holder whose only fields are address, arch, baseAddress, binaries, buildId. Imports p268eh (already-classified Crashlytics) and Firebase encoder classes p444o0/p501qh. No methods, no strings, no I/O.Not ExoPlayer, not an ad SDK — the field names are a binary/ABI descriptor, which points at Firebase’s App/Device proto model or a native-crash symbol reader, but the containing model class lives in an out-of-scope package.
p308gj (4 files, 59 KB)Four thin classes that only wire together p315h7, p324hg, p369jj and the vg/ki/nf root packages. Only observable side effect is one PackageManager query.Not Firebase Remote Config (that’s jj); not FIAM (p326hi). Most likely a Moloco↔Mintegral mediation plugin bridge.
p306g8 (5 files, 48 KB)Five classes, zero string literals, zero API calls. Purely structural glue over p358j5 and p675y8 (both out of scope).Not identifiable from signatures alone.
p684yi (3 files, 11 KB)yi.b extends AbstractC20745d and is an OkHttp method set (CONNECT/DELETE/HEAD/OPTIONS); yi.a/yi.c are a ViewTreeObserver.OnDrawListener. The package also pulls in androidx.room, Firebase-Perf and Amplitude classes, so it is a merge point of three libraries rather than a coherent unit.Not a Room database (no RoomDatabase/@Entity/@Dao anywhere).

5.1 Other caveatsh4

  • p708zd (zd) and p707zc (zc) are in the briefing but are already-classified ExoPlayer packages (they were named in the skip list). I re-verified them: zd = Surface/Display video-output and AVC/HEVC config parsing; zc = MediaCodec async adapter + device quirk tables. They are recorded in the table above for completeness but carry no new findings.
  • p433n8, p382k8, p655xf, p297fs, p640x* touch Mintegral reporting/privacy APIs (/privacy_sandbox/pa/logic, ?daily&app_id=, _audiencePropertyIds). These are SDK-internal and not individually actionable, but they confirm Mintegral is collecting protected-audience and daily user-info signals in addition to ad requests.
  • RxJava2 is present (ao, go, ho, qn) which is unusual for a 2026 Android app; combined with Ktor + OkHttp + raw HttpURLConnection + ExoPlayer’s own networking, MICO ships at least five HTTP stacks. Certificate-pinning and cleartext policy therefore need to be checked per stack, not globally.
  • p324hg (hg) is the Kotlin standard library, not MICO code. Its C-number range (C16984–C17063) sits inside the same global numbering as first-party code, which is why a purely numeric heuristic mis-classifies it.

Cluster C6b — Unclassified Packages, Part 2 of 2h3

APK: com.eyecon.global (MICO — caller ID / spam blocker) v4.2026.04.23.1145 Scope: 193 packages, 472 .java files (briefing docs/briefings/C6b-unclassified.txt) Tree: C:\Users\btc_e\Downloads\mico-analysis\src\sources Companion: docs/cluster-C6a-unclassified.md (192 packages / 2,426 files)

Methodh4

Obfuscated top-level directories are named pNNN<short> on disk, where <short> is the JADX-original package name (recovered from the /* JADX INFO: renamed from: ... */ comment in each file — e.g. p393ks -> ks, p424mi -> mi). Every one of the 193 briefing packages was:

  1. fully enumerated for file count and renamed from (real package name),
  2. scanned for its complete external import graph (all non-android*/java*/kotlin*/pNNN imports — this names the owning SDK unambiguously for most packages),
  3. scanned for API-surface hits on HTTP/OkHttp/Retrofit, Cipher/MessageDigest/SecretKey, WebView/JS-bridge, SQLite/SharedPreferences/ContentResolver, and privacy identifiers (contacts, SMS, call log, telephony, IMEI, MAC, android_id, ad ID, location, microphone),
  4. read in full for anything in priority classes 1-4; the remainder is tabulated compactly.

A second obfuscation axis appears throughout: several SDKs ship their own proguarded classes under short root package names (af, ag, bh, bi, er, ir, kh, ki, li, mp, nc, qo, sr, th, tj, vn, …) with C1xxxx/C2xxxx numeric class names. Those are named inline.



1. Executive summaryh3

Cluster C6b is the tail end of the obfuscated namespace: after the C6a packages (which held the bulk of the ad-mediation stacks and the real MICO glue), what remains is mostly

  • annotation / marker / interface-only packages with no logic at all (R8 hoisted interfaces and @interfaces out of their owning SDK into their own package),
  • pure Kotlin/Java support types (value classes, data-class equivalents, equals/hashCode boilerplate, Parcelable proxies) belonging to Amplitude, Firebase, Meta/Facebook, Coil, Glide, Moloco, Mintegral, InMobi, Pangle, Liftoff/Vungle, Fyber/DT Exchange, Digital Turbine Ignite, Amazon APS, Retrofit/OkHttp, Gson, Glide, Material Components, ZXing, OkHttp, Ktor, RxJava2,
  • the very last MICO-authored packages, which number two: i5 (PaidInappProductsCache) and lq (ShortcutBadger launcher-badge adapters, parameterised on com.eyecon.global.Others.MyApplication).
BucketPackagesNotes
Third-party SDK / library code176see the classification table (§4) for the owning SDK per package
FIRST-PARTY MICO2i5 (IAP purchased-product cache, called from a7), lq (ShortcutBadger badge adapters, typed on MyApplication)
Dead code / unreachable stubssee §6dominated by throw new UnsupportedOperationException() synthetic abstract /* synthetic */ classes that R8 failed to inline
Cannot be classified confidently3uh, un-adjacent orphans and p471p2 — see §5
TOTAL193472 .java files (matches the briefing exactly)

No WebView, no JS-bridge, no contacts/SMS/call-log/telephony/IMEI/MAC/location/microphone code exists anywhere in C6b. The only privacy-adjacent code is Meta’s NsdManager LAN-service registration (§3.5) and Meta’s ANR/stack-trace upload (§3.4).


2. NEW security-relevant findings (not in the cross-reference list)h3

2.1 MEDIUM — First-party IAP entitlement cache lives in i5; entitlement is still pure client stateh4

p338i5/C17151a.java (real pkg i5) — FIRST-PARTY MICO

This is the only first-party logic package in C6b, and it is directly on the premium-entitlement path already flagged in the cross-reference list (“Premium entitlement is 100% client-side with no Google purchase verification”). It adds the concrete storage location:

public C17151a() {
MyApplication.m10348j().getClass(); // force MyApplication init
C11856m cache = SharedPreferencesC16497y.m32083c("SP_KEY_INAPP_PRODUCTS_CACHE", null).m14605h();
if (cache == null) { this.f42621a = new C11856m(); g5.C16707i...post(new a7.C0076s0(18, this, false)); }
}
public final boolean m32991a(String productId) { // returns true if NEWLY added
C11860q key = new C11860q(productId);
if (this.f42621a.f22421a.contains(key)) { log("add productId = %s already added"); return false; }
this.f42621a.m14599n(key); return true;
}

Callers (both in the already-documented first-party package a7, i.e. com.eyecon.global’s purchase-verification callback):

  • p008a7/C0076s0.java — inside the billing-result handler: else if (str.equals("inapp")) \{ if (C17151a.f42620b.m32991a(str2)) \{ AbstractC16137c.m31542V(str2); \} z11 = true; \} then MyApplication.m10347i().m32079f("SP_KEY_INAPP_PRODUCTS_CACHE", cache) — i.e. the moment a purchase row is seen, the SKU is written to the prefs blob. m31542V(productId) is what flips the premium flag.
  • p008a7/C0076s0.java case 18 — iterates c16708j3.f41475j == 1 rows and calls c17151a.m32991a(str5) + m31542V(str5) for each.

Consequence: the entire premium state is a Gson-serialised Set<String> of SKUs in SharedPreferences under the key SP_KEY_INAPP_PRODUCTS_CACHE, written by MICO itself. Because it is in app-private SharedPreferences with no signature/MAC and no server check, a user (or anything running with the app’s UID — e.g. a rooted device, a BackupManager restore, or an unlocked bootloader) only has to inject one SKU into that set to obtain premium. Log tag "PaidInappProductsCache". This corroborates and localises the existing finding rather than duplicating it; the new information is (a) the exact prefs key, (b) that the write happens in first-party code inside a7, and (c) that the cache is a de-duplicating set that is re-read at construction time.

2.2 MEDIUM — Hardcoded Google/Firebase API key + Crashlytics report endpoint hidden by a string-interleave obfuscatorh4

p347ih/C17303a.java (real pkg ih) — Firebase Crashlytics (third-party, not in the C6a skip list)

Two constants are built at class-init time by m33283a(a, b), which interleaves two strings character-by-character (result[2i]=a[i], result[2i+1]=b[i]) — a split-and-shuffle string-hiding scheme rather than encryption. Decoded:

f43353c = "https://crashlyticsreports-pa.googleapis.com/v1/firelog/legacy/batchlog"
f43354d = "AIzaSyBrpSYT4FFL09reHJi6H9FYdeiSnUTOv2M" // 39-char Google API key

So the Crashlytics legacy ingest endpoint and the project API key are baked into the DEX. This is not a credential leak on its own (Firebase API keys are identifiers, and Crashlytics upload is authenticated by the Firebase installation’s registration token), but it is (a) a hardcoded key constant of the class the brief asks to be documented, and (b) confirmation that the app posts Crashlytics legacy non-fatal (“firelog”) batchlogs to crashlyticsreports-pa.googleapis.com — i.e. non-crash event batches are shipped to Google in addition to crash reports.

2.3 LOW-MEDIUM — Moloco request bodies are AES/GCM + GZIP + Base64 framed with a runtime keyh4

p393ks/C18251a.java + C18252b.java (real pkg ks) — Moloco (tech.crackle, confirmed: imported by tech/crackle/cracklertbsdk/network/ApiClient.java)

// decrypt: gunzip -> base64(NO_WRAP=11) -> first 12 bytes = IV, rest = ciphertext -> AES/GCM/NoPadding(128)
byte[] body = Base64.decode(gunzip(bArr), 11);
byte[] iv = Arrays.copyOfRange(body, 0, 12);
byte[] ct = Arrays.copyOfRange(body, 12, body.length);
Cipher.getInstance("AES/GCM/NoPadding").init(2, new SecretKeySpec(bArr2,"AES"), new GCMParameterSpec(128, iv));
// encrypt: SecureRandom 12-byte IV -> AES/GCM -> iv||ct -> base64(NO_WRAP) -> GZIP

The key is not hardcoded — it is passed in (decryptionKey / encryptionKey, non-null-checked by Kotlin intrinsics). Failures are swallowed and logged as "" / exception / "unzipAndDecrypt" and "encryptAndGzip". Everything here is standard, non-degenerate crypto (fresh SecureRandom IV per message, 128-bit GCM tag). No key material, no URLs, no PII in this package.

2.4 LOW-MEDIUM — Digital Turbine “one DT” device token is decrypted and persisted in SharedPreferences["odt"]h4

p699z3/BinderC22068b.java + BinderC22067a.java (real pkg z3) — Digital Turbine Ignite (Fyber’s mediation), called from a4 (p005a4) and d4 (p235d4)

onSuccess(String) receives the raw “one DT” string over AIDL, decrypts it through C19932m.m36727g(str) (a key held by the lazily-initialised kh.C17847a holder), and then:

JSONArray jSONArray = new JSONArray();
jSONArray.put(pair.first).put(pair.second);
((SharedPreferences) c17847a.f45464a).edit().putString("odt", jSONArray.toString()).apply();

i.e. the decrypted token is written as a two-element JSON array under prefs key odt. Failures are logged with the enum codes FAILED_INIT_ENCRYPTION / RAW_ONE_DT_ERROR / “received empty one dt from the service”. BinderC22067a.onSuccess separately parses new JSONObject(str).getJSONObject("data").getString("token") out of the Ignite service response — that is an auth/session token being handled in the clear on the Binder path (in-process AIDL stub, so not exposed off-device, but it is a raw token with no wrapping).

2.5 LOW — Retrofit’s full annotation set is shipped as 25 standalone @interface packagesh4

p639wr/InterfaceC21493a.java … InterfaceC21517y.java (real pkg wr, 25 files)

R8 hoisted every one of Retrofit 2’s method/parameter annotations into their own package. Decoded by shape: @Headers (a), @Header-style String value() (b), @Query(value, encoded) (c), @QueryMap(encoded) (d), @Path-style String value() (e/f/g), @FormUrlEncoded (h), @HTTP(method, path, hasBody) (i), @Url-style with allowUnsafeNonAsciiValues (j/k/l), @Body(encoded) (n/o), @Field(encoding) (s/t), @FieldMap(encoding) (q/r), @Part/@PartMap/@Multipart (u/v/w/x/y). This is the Retrofit annotation layer R8 orphaned; no endpoint URLs are declared here, so no network destinations can be recovered from this package. Noted because the presence of a Retrofit annotation layer inside the app’s own obfuscation namespace (rather than a readable retrofit2 package, which does also exist at …\sources\retrofit2) is an R8 merge artifact.


3. Deep entries — priority categoriesh3

3.1 Networking / HTTP / cryptoh4

Covered above: ks (§2.3, Moloco AES/GCM), ih (§2.2, Crashlytics endpoint), z3 (§2.4, Ignite token), vf (Mintegral HTTP request/response builder — see §4), vr (Retrofit/Fyber RequestBodyConverter/ResponseBodyConverter adapters, see §4), wr (§2.5). No HttpURLConnection, no WebView, no addJavascriptInterface, no JS bridge anywhere in C6b.

vf (p604vf) deserves a note: C20993c is a POST-request builder with a hard 10 MB body cap (f55741n = 10485760), Range header injection ("bytes */" + this.f55739l where f55739l defaults to "*" via androidx.webkit.ProxyConfig.MATCH_ALL_SCHEMES — a constant that R8 re-used, not a real WebView proxy), and a ServerCallback wrapper that retries on HTTP 5xx. C20994d logs "exception thrown while calling server callback" at Level.WARNING to java.util.logging. Owner: Mintegral / mbridge (p282f5, th, p626wc are mbridge packages).

3.2 Database / file storage / SharedPreferencesh4

  • z3 — SharedPreferences["odt"] (§2.4).
  • pi (p482pi, real pkg pi) — Firebase common (called from p459og/C19129g.java and C19125c.java, i.e. the Firebase/GMS common component used by ti Firebase-Perf and xg Crashlytics). Reads ContextCompat.createDeviceProtectedStorageContext() (so the prefs land in device-protected / direct-boot storage, not credential-encrypted storage) and opens "com.google.firebase.common.prefs:" + appId, then resolves the firebase_data_collection_default_enabled flag from prefs, falling back to PackageManager.getApplicationInfo(...,GET_META_DATA).metaData. So MICO’s Firebase telemetry opt-out is readable/writable from direct-boot storage.
  • mi (p424mi) — Firebase Installations (FID): SharedPreferences("com.google.android.gms.appid") plus m34899b() which rebuilds an RSA public key from the Base64 "|S||P|" pref, SHA-1s it, and returns the Base64 cert-fingerprint used for GCM sender auth. f47109c is the {"*","FCM","GCM",""} mime array. Auth/refresh tokens live in mi.a’s PersistedInstallationEntry\{firebaseInstallationId, registrationStatus, authToken, refreshToken, expiresInSecs, tokenCreationEpochInSecs, fisError\}.
  • bk (p037bk) — Glide disk-cache metadata reader. C3582b.m3798a(InputStream) builds a new ObjectInputStream(inputStream) and readExternal()s a metadata list out of it ("Unable to parse metadata file", "Empty metadata"). The stream is Glide’s own cache directory entry (written by Glide itself), so it is not attacker-reachable without filesystem write access; still, it is a ObjectInputStream sink and worth recording given the ObjectInputStream.readObject() note already in the cross-reference list.
  • lq (p412lq) — ContentResolver.call() on launcher badge authorities (§3.5).
  • c3, d3, e3 — Pangle/CSJ video preload cache: RandomAccessFile("rw") on <dir>/<id> and <dir>/<id>.tmp, renamed on completion ("Error renaming file ... for completion!"), with a 20 s busy-wait (wait(5) x 4000) and a 20 s SocketTimeoutException ceiling.
  • i (p333i) — AbstractC17126a is a dead stub: both methods (execSQL(SupportSQLiteDatabase,String,Object[]), isOpen) were reduced by R8 to throw new UnsupportedOperationException() / return false. Live SQLite work is in androidx.sqlite / Room elsewhere.
  • g2 (p300g2) — Glide android.resource:// / asset loaders; calls context.createPackageContext(authority, 0) on a URI authority taken from an image URL (Glide’s known behaviour), and Resources.getSystem().getIdentifier(name, type, "android").

3.3 Contacts / SMS / call log / telephony / IMEI / MAC / android_id / ad-ID / location / microphoneh4

Nothing in C6b. Exhaustive scan of all 472 files for ContactsContract|CallLog|TelephonyManager|getDeviceId|getImei|getMacAddress|ANDROID_ID|AdvertisingId|LocationManager|getLastKnownLocation|AudioRecord|MediaRecorder|ClipboardManager returned only the PackageManager and MessageDigest/Cipher/SharedPreferences hits tabulated in §4. (The contact-photo / location / GAID harvesting that is in the APK lives in v1, e1, ic etc., i.e. C6a / already-documented packages.)

3.4 Hardcoded string constants / URLs / key materialh4

Only two hits in the whole of C6b:

  • p347ih/C17303a.java — the interleaved Crashlytics URL + Google API key (§2.2).
  • p592v0/C20848c.java — a literal package-name reflection probe: "androidx.compose.ui.node.Owner" and "com.amplitude.android.internal.locators.ComposeViewTargetLocator", used only to decide whether to register Amplitude’s Compose target locator. No other URL, hostname, API key, AES/Blowfish key material, or endpoint path appears in any of the 193 packages.

3.5 Other notable behaviour (not security, but materially affects what the app does)h4

  • t8 (p557t8) — Meta/Facebook SDK 18.0.3 local-network probe. m37297c(String) registers an mDNS/NSD service "fbsdk_android-18.0.3_<md5>", type _fb._tcp., port 80, via NsdManager.registerService(..., TTL_DNS_SERVICE=1, listener), and unregisters it on the singleton’s teardown. This makes the device discoverable on the local Wi-Fi subnet while the Facebook SDK is alive. It also short-circuits entirely if C9075b0.m10750b(C9154o.m10979b()).f13446c.contains(EnumC9093k0.Enabled) is false (i.e. gated on the FB “Enabled” data-processing option).
  • w8 (p622w8) — Meta/Facebook ThreadGuru-style ANR + native-crash reporter. AbstractC21297a.m38472a() walks ActivityManager.getProcessesInErrorState(), filters condition == 2 && uid == Process.myUid(), then serialises the entire main-thread StackTraceElement[] into a JSONArray and ships {shortMsg, JSONArray-of-stacks} via AbstractC16137c.m31565m(...) to Facebook. AbstractC21299c.m38473a() walks the ANR trace directory, filters to traces newer than 5, takes the 5 most recent, and uploads them as "anr_reports" — deleting them only after Facebook returns {"success":true}. Full main-thread stacks + up to 5 ANR traces per crash window leave the device on every ANR.
  • lq (p412lq) — ShortcutBadger 1.1.22-style launcher badge adapters, four of them, and each one carries a hardcoded vendor launcher package name: com.oppo.launcher (ContentResolver.call(content://com.android.badge/badge, "setAppBadgeCount")), com.vivo.launcher (sendBroadcast("launcher.action.CHANGE_APPLICATION_NOTIFICATION_NUM")), ZUI/Lenovo com.zui.launcher (content://com.android.badge/badge), and an AOSP content://com.android.launcher3.cornermark.unreadbadge setAppUnreadCount variant whose getSupportedLaunchers() returns new ArrayList(0) (see §6 — unreachable). All four take com.eyecon.global.Others.MyApplication as the context parameter, so this is the one package where R8 narrowed a third-party API signature to MICO’s own Application subclass.
  • rn (p531rn) — RxJava2 main-thread Scheduler (HandlerScheduler-equivalent, C19964c/C19966e/RunnableC19965d), used broadly.
  • yn (p689yn) — RxJava2 Disposables (EmptyDisposable, DisposedDisposable), plus OnErrorNotImplementedException routing.
  • zn (p717zn) — RxJava2 CompositeDisposable-equivalent (C22291a extends AtomicInteger with a composite C21168c), Runnable/Callable Task wrappers, and Completable-esque sources.
  • rn/zn/j9/s9/k9/b9/on/d1-style switch(int) synthetic dispatchers — see §6.


4. Classification table — every package in the briefingh3

Legend — TP = third-party SDK/library code (owner named), FP = first-party MICO, DEAD = unreachable / no-op stub, MERGE = R8/ProGuard merge artifact.

#Real pkgDisk dirFilesClassOne-line responsibilityNotable classes
1ocp455oc1TP Mintegral/ExoPlayer forkAMR-NB / AMR-WB Extractor (magic #!AMR / #!AMR-WB, frame-size tables)C19118a
2fep290fe5TP Google MaterialAppBarLayout.BaseBehavior / ScrollingViewBehavior inner classes + a11y delegateC16533a, AbstractC16535c, AbstractC16536d
3vjp606vj3TP Gson 2.10.1ReflectAccessFilterHelper + java.lang.reflect Record support (isRecord/getRecordComponents)AbstractC21068c, C21067b
4klp390kl2TP Liftoff/VungleAdMob mediation BaseAdListener shims (interstitial / app-open / banner / rewarded)C17854a, C17855b
5g2p300g22TP GlideResourceUriLoader (android.resource://) + drawable→bitmap resource wrapperC16694a, C16695b
6ujp584uj1TP utilISO-8601 yyyy-MM-dd'T'HH:mm:ss[.SSS]X date parser (UTC fixed)AbstractC20748a
7ibp343ib6TP InMobiAdMob mediation adapters (banner / interstitial / native / rewarded) — setKeywords(""), load()C17199a…C17204f
8m1p415m17TP Google MeasurementAnalytics transport error payloads: BAD_REQUEST (events_with_invalid_fields, silenced_devices), TOO_MANY_REQUESTS (throttled_devices/users/events), PAYLOAD_TOO_LARGE, FAILEDC18474a, EnumC18476c
9a2p004a23MERGEOne Thread class with a 5-way switch: Glide executor (priority 9 + StrictMode.detectNetwork().penaltyDeath()), Coil, ExoPlayer:SimpleDecoder, Amplitude timeline, androidx.workC0026a, ExecutorServiceC0028c, ThreadFactoryC0027b
10lep402le8TP Google MaterialCarouselLayoutManager ItemDecoration + LinearSmoothScroller + card-size metricsC18342a…C18349h
11mip424mi2TP Firebase InstallationsPersistedInstallationEntry (FID, auth/refresh tokens, expiry) + GCM RSA pubkey→SHA-1 cert prefsC18592a, C18593b
12hep322he2TP Google MaterialSwipeDismissBehavior ViewDragHelper.Callback + AccessibilityViewCommandC16966a, C16967b
13yap677ya4TP Fyber FairBidSDK request wrapper for OfferWall ("ofw") and video-caching ("vcs") flowsAbstractC21893d, C21891b, EnumC21892c
14ipp352ip5TP Kotlin stdlibkotlin.random.Random bridge onto java.util.Random + writeReplace serialization proxyAbstractC17413a, C17416d
15ybp678yb6TP Google DataTransportCCT backend registry (BackendResponse{status,nextRequestWaitMillis}, CreationContext)C21894a, C21895b, C21898e
16vfp604vf4TP MintegralHTTP request builder (POST, 10 MB cap, Range: bytes */*, 5xx-retrying ServerCallback)C20993c, C20994d
17j9p362j94TP Fyber FairBidSynthetic switch dispatchers for Banner/Interstitial/Rewarded/OfferWall + Android-8 IAB intent fallbackC17542a…C17545d
18z3p699z32TP Digital Turbine IgniteAIDL IIgniteServiceCallback.Stub subclasses: resolve data.token; decrypt & persist one DT to prefs "odt"BinderC22067a, BinderC22068b
19wjp632wj3TP Gsonjava.sql.Date/Time/Timestamp TypeAdapter ("MMM d, yyyy", "hh:mm:ss a")C21435a, AbstractC21437c
20kdp386kd2TP ExoPlayer / MintegralAdPlaybackState + AdGroup (ad-loader bookkeeping)C17837a, C17838b
21pbp479pb5TP Liftoff/VungleVungle AdMob adapter init queue; sets VungleWrapperFramework.admob integration 7.7.0.1_7_7_0_1; COPPA mappingC19334c, C19336e, InterfaceC19333b
22w8p622w83TP FacebookgetProcessesInErrorState() main-thread stack-trace uploader + 5× ANR-trace uploader ("anr_reports")AbstractC21297a, AbstractC21299c
23drp251dr1TP OkHttpHttpUrl.canonicalize percent-encoding (%XX, +)AbstractC15999a
24e3p258e32TP Pangle/CSJ (ByteDance)Video preload manager — HTTP RANGE pre-warm of the ad video fileAbstractC16094a, C16095b
25v0p592v05TP Amplitude Session ReplayTargetLocator registry — reflective probe for androidx.compose.ui.node.OwnerC20846a, C20848c
26rgp524rg3TP Google Measurementzzlm interface adapter + GRANTED/DENIED and AD_STORAGE / ANALYTICS_STORAGE / AD_USER_DATA / AD_PERSONALIZATION enumsC19912c, EnumC19910a, EnumC19911b
27up567u7TP CoilSize/Dimension/SizeResolver (screen-size resolver)C20549a, C20551c, C20554f
28ksp393ks2TP MolocoAES/GCM/NoPadding + GZIP + Base64(NO_WRAP) request codec, 12-byte SecureRandom IVC18251a, C18252b
29ubp578ub7TP AmplitudeTyped Event{code,payload,priority,productData} + ProductData{productId} + Encoding{name} + Priority enumC20710a, EnumC20713d
30rp510r8TP CoilMemoryCache (LruCache subclass, Value(bitmap,extras), low-RAM % heuristics)C19805a, C19809e, InterfaceC19812h
31znp717zn4TP RxJava2CompositeDisposable (AtomicInteger) + Task/Runnable/Callable wrappers + no-op taskC22291a, C22292b, C22293c
32wrp639wr25TP Retrofit 2The complete @interface annotation set (see §2.5)InterfaceC21493a…InterfaceC21517y
33o1p445o14TP AmplitudeIdentity(userId,deviceId) + IdentityConfiguration(instanceName, apiKey, experimentApiKey, identityStorageProvider, storageDirectory, fileName)C19042a, C19043b
34h1p312h14TP Amplitude$identify event model + $set/$setOnce/$add/$append/$clearAll/$prepend/$unset/$preInsert/$postInsert/$removeC16908a, EnumC16910c
35mjp425mj5TP Firebase In-App MessagingFIAM Rollouts: RolloutAssignment{rolloutId,variantId,parameterKey,parameterValue,templateVersion}C18594a, C18596c, C18597d
36z1p697z16TP GlideArrayPool-style cache + disk-cache key digest (MessageDigest→uppercase hex, 1000-entry LRU) + low-RAM pool sizingC22059f, C22057d
37s3p539s37MERGE (TP)Quartz-style cron field value model (L, W, LW, a#b, ?) hoisted into a shared package; consumed by Google Measurement, Firebase, Facebook and WorkManagerAbstractC20038e, C20039f, C20037d
38lqp412lq9TP ShortcutBadger (+ FP types)Launcher app-badge adapters for OPPO / vivo / ZUI / AOSP cornermark, all parameterised on MyApplicationC18439d…C18444i
39dip247di1TP Google Materialcom.google.android.material.internal.ModalLayout (max width/height % from ModalLayout attrs)AbstractC15951a
40e9p264e93TP recyclerviewfastscrollSectionIndexer/AbstractSectionIndexer base + animator-end listenerAbstractC16147b, C16146a
41d0p231d05TP LottiePath keyframe Keyframes/KeyframeParser data objects (merged with a Facebook holder)C15791a…C15794d
42tip563ti4TP Firebase PerformancePerf component factory (reads metaData, Remote Config + SessionManager wiring)C20461b, C20463d
43c3p043c31TP Pangle/CSJMediaDataSource for CSJ media download-and-play (wait(5) ×4000 busy-wait)C3838a
44vrp611vr3TP Retrofit + Moshi + FyberRetrofit RequestBodyConverter / ResponseBodyConverter adapters (UTF-8 BOM "EFBBBF" skip)C21202a, C21203b, C21204c
45kkp389kk2TP ZXingGenericGF (GF(2^k) tables for 4 predefined polys) + GenericGFPoly (Reed–Solomon)C17852a, C17853b
46n1p427n14TP SingularEvent(eventType, eventProperties, userProperties, groups, groupProperties) + 512-slot ArrayBlockingQueue ingest bufferC18690a, C18691b
47t8p557t82TP FacebookmDNS/NSD registration of fbsdk_android-18.0.3_<hash> _fb._tcp. on port 80 (see §3.5)C20364a, C20365b
48d3p234d31TP Pangle/CSJVideo cache file handler (RandomAccessFile("rw"), .tmp → final rename, RANGE resume)C15799a
49anp016an2TP KtorRandomAccessFile-backed suspending writer (setLength(remaining))AbstractC0232a, C0233b
50opp465op2TP Android (android.graphics)Pair-equivalent value class with Externalizable proxy (UUID-like formatting)C19220a, C19221b
51xgp656xg3TP Firebase CrashlyticsCrashlytics component factory ("clx"/"crash" store lookups) + component lookup holderC21760a, C21761b
52ihp347ih6TP Firebase CrashlyticsInterleaved-string constants (Crashlytics batchlog URL + API key, §2.2) + internal metric throttle samplerC17303a, C17304b
53rnp531rn5TP RxJava2Main-thread Scheduler (Handler-backed) + DisposableC19964c, C19966e, RunnableC19965d
54nbp435nb5TP Pangle (ByteDance)Pangle AdMob adapter init; PAGConfig setAdxId("207"), userData=[{"name":"mediation","value":"google"},{"name":"adapter_version","value":"7.6.0.3.0"}]C18752c, C18753d
55h8p316h82TP FacebookApp-Events “deprecated event / deprecated_param” remote-config applierC16951b
56osp468os2TP MolocoAdError constants (0 “SDK is not initialized” … 5 “Internal error”) + impression-URL / request countersAbstractC19281a, AbstractC19282b
57k9p383k92TP Fyber FairBidBannerView load/show synthetic dispatchers (SettableFuture.Listener)C17795b, RunnableC17794a
58r8p519r82TP FacebookApp-Events “restrictive_param” remote-config applierC19894b
59s9p545s92TP Fyber FairBid30-arm synthetic dispatcher over C9501d + MediationManager accessorsC20083a, C20086d
60abp012ab2TP Fyber FairBidFairBid SDK config string accessor (C9901tb) + "Fyber"-tagged threaded loggerC0098a, C0099b
61hbp319hb4TP InMobiBidding AdMob adapters — pass getBidResponse().getBytes() straight to InMobi*.load()C16956a…C16959d
62u3p571u33MERGE (TP)Cron-field typed values (Integer / EnumC20575b) shared with s3C20574a, C20576c, EnumC20575b
63bkp037bk2TP libphonenumberMetadata resource reader — getResourceAsStream + ObjectInputStream.readExternal() on /com/google/i18n/phonenumbers/data/*.ProtoC3581a, C3582b
64p4p473p43TP Amazon APSDTBAdCallback → AdManagerAdRequest param injection; 320×50 / 300×250 slot tableC19293a, C19294b, AbstractC19295c
65gkp309gk2TP ZXingEncoder hint enum (ERROR_CORRECTION … GS1_FORMAT)EnumC16771a, InterfaceC16772b
66qp487q1TP GlideModel loaders for byte[], file://, resource-id Uri, android.resource://…, StringC19511a
67ynp689yn3TP RxJava2Disposables (EmptyDisposable, DisposedDisposable) + OnErrorNotImplementedException sinkAbstractC21960b, C21961c
68j2p356j23TP GlideByteBuffer → byte[] transcoder with ArrayBuffer fast pathC17479b, C17480c
69s2p538s28TP GlidePools.Pool / FactoryPools (ArrayPool pool of pools, ObjectPool release guard)C20029d, AbstractC20032g, C20033h
70ohp460oh6TP FIAM / protobufFieldDescriptor{name,properties} + the 6-method field-setter builder interfaceC19133b, InterfaceC19135d
71m2p416m24TP GlideDecode-options Registry data objects (ArrayMap keyed on 3-type tuple)C18481a…C18484d
73t0p550t02TP AmplitudeFragmentLifecycleCallbacks auto-track "[Amplitude] Fragment Viewed" (class, id, screen name, tag)C20305a
74hpp330hp3TP Kotlin stdlibObservableProperty / ReadWriteProperty (beforeChange/afterChange hooks)AbstractC17097a
75p0p470p02TP AmplitudeIdentity(userId, deviceId, userProperties) + identity storage singletonC19287b, C19286a
76t2p552t23TP Pangle/CSJVideo-config singleton ("v_config", 10 s timeouts) + download tracker interfacesAbstractC20311a, InterfaceC20312b
77edp266ed1TP ExoPlayerIcyInfo metadata parser ((.+?)='(.*?)';, streamurl/streamtitle)C16179a
78i4p337i41TP Digital Turbine IgniteBroadcastReceiver for com.dt.ignite.service.action.PROPERTY_CHANGED watching the DTID propertyC17150a
79y3p670y32TP Digital Turbine IgniteIDTAuthenticator wrapper — authenticate(), getOdt(), isAuthenticated(), isConnected()C21842a, C21843b
80m3p417m32TP Joda-TimeDayOfWeek arithmetic (7-day and 6-day variants)C18486b
81qip502qi1TP Koin/DIQualified Executor provider lookup (Qualified.qualifiea, Executor::class.java) | C19727a`
82xap650xa1TP Fyber FairBidInstall-reporter (InstallReporter) posting a C9284cv request in a new ThreadAbstractC21677a
83c4p044c43TP Digital Turbine IgniteIgniteEventDispatcherWrapper + error enums 1100-1106 (ENCRYPTION_EXCEPTION, ONE_DT_*)C3839a, EnumC3840b, EnumC3841c
84ymp688ym4TP OkHttpRequestBody/TextContent(text, contentType) with byte-length + charsetAbstractC21956b, C21958d
85ykp686yk1TP Moloco / AdMobOnPaidEventListener bridge — forwards AdValue CPM to Moloco with ca-app-pub / GAM source taggingC21941a
86z2p698z27TP Pangle/CSJVideo player control + download-manager interfacesInterfaceC22060a…InterfaceC22066g
87y6p673y61TP OkHttpBasicNameValuePair(name, value)C21872a
88fmfm1TP OkHttpHost/port resolution (http→80, https→443) + hex char parserC16655a
89hcp320hc1MERGE (TP)Priority→int mapper shared by androidx.transition, Facebook AppEvents and Google DataTransportAbstractC16960a
90z8p704z81TP Mintegralerror_log_<epochSeconds>.json writer ({"timestamp":…,"error_message":…})C22130a
91f2p280f21TP GlideDataFetcher factory for ByteBuffer / ParcelFileDescriptor (one arm is UnsupportedOperationException("Not implemented"))C16413a
92pip482pi1TP Firebase commonfirebase_data_collection_default_enabled resolver on device-protected storage (§3.2)C19354a
93wip631wi1TP Firebase (Dagger)Provider for the Firebase C19129g app context / trace / clock depsC21434a
94oep457oe1TP Google MaterialMaterialColors helpers (calculateLuminance > 0.5, compositeColors)AbstractC19121a
95i9p342i91TP Fyber + KtorAPSAdapter request builder + a Ktor HttpClient call on a background dispatcherC17198a
96wqp638wq1TP KtorWindowCounter(streamId,total,acknowledged,unacknowledged) flow-control windowC21492a
97u8p576u81TP AmplitudeGateKeeper(name, value) feature flag modelC20706a
98x8p649x81TP AmplitudeUncaughtExceptionHandler that walks the cause chain looking for a fatal marker and reports itC21676a
99ifp345if1TP Google Play Core / Install ReferrerPlay error-code→message maps (ERROR_APP_NOT_OWNED, ERROR_INSTALL_NOT_ALLOWED, ERROR_PLAY_STORE_NOT_FOUND)AbstractC17293a
100p9p478p91TP Fyber FairBidOnScreenAdTracker SettableFuture.Listener dispatcherC19330b
101v2p594v21TP AmplitudeMD5 hex digest helper (shared MessageDigest, uppercase hex) — weak hash, used for cache/idsAbstractC20864a
102ulp585ul1TP Firebase Perf (Dagger)Double-check scoped Provider (FirebasePerformanceRegistrar)C20750a
103z0p696z01TP Amplitude Session ReplayInternalOptions override: pack=true, transform=true, logPayloads=false, maxStorageBytes=10 MB (§2.6)C22053a
104enp273en1TP Kotlin reflectTypeInfo(KClass, KType?)C16301a
105g3p301g31TP MintegralProgress %, hh:mm:ss formatter, immersive setSystemUiVisibility(3846)AbstractC16696a
106ffp291ff1TP Google MaterialmaterialThemeOverlay / ContextThemeWrapper resolutionAbstractC16538a
107efp267ef1TP Google MaterialMaterialTextView textAppearanceLineHeight supportC16183a
108b0p027b01TP GlidePer-request resource cache (Callback+base-URL+extras → bitmap)C3399a
109xep654xe1TP Google MaterialElevationOverlayProvider state colour/alpha helpers (state_pressed, state_focused, …)AbstractC21755a
110i5p338i51FP MICOPaidInappProductsCache — premium SKU set in prefs SP_KEY_INAPP_PRODUCTS_CACHE (§2.1)C17151a
111q2p490q22TP GlideEmptySignature + ObjectKey{object} (MessageDigest cache key)C19564a, C19565b
112zbp706zb2TP Dagger/ComponentRuntimeDoubleCheck scoped ProviderC22133a
113d1p232d12TP GlideLog-level enum DEBUG/INFO/WARN/ERROR/OFF + Logger interfaceEnumC15796a, InterfaceC15797b
114v4p596v45TP MintegralError classes: code 8 "~App Id Missing~", 101, 3 "~No Fill~"AbstractC20871e, C20867a, C20870d
115yhp683yh1TP Dagger/ComponentRuntimeDoubleCheck scoped ProviderC21935a
116f1p279f11TP GlideDefaultLogLogger (writes to System.out, level INFO)C16412a
117g4p302g42TP Digital Turbine IgniteLogger facade (d/w/i/e) dispatching to InterfaceC16697aC16698b
118pp469p3TP GlideCache-key encoders — File path (+lastModified), Uri (+uiMode & 48)C19283a, C19285c
119b9p036b93TP FacebookAIDL stub for com.facebook.ppml.receiver.IReceiverService (transact(1, Bundle) -> int) (§2.7)AbstractBinderC3527b, C3526a
120xnp661xn8TP RxJava3/okioQueue / SpscLinkedArrayQueue / LockFreeLinkedQueue / Callable marker interfacesInterfaceC21796h, InterfaceC21793e
121onp463on3TP OpenTelemetryContextManager with reflective io.opentelemetry.opencensusshim.OpenTelemetryContextManager loadAbstractC19216a
122whp630wh2TP Firebase FIAM (Dagger)DoubleCheck scoped Provider for In-App Messaging componentsC21432a
123xqp664xq2TP Fyber FairBidPunycode/IDN encode-decode ("xn--", 36-ary digit alphabet, TypedValues.TransitionType.TYPE_DURATION reuse)AbstractC21811a, AbstractC21812b
124qep499qe3TP AndroidX/MaterialOutline.setConvexPath/setPath shims + Drawable → ColorStateList (SDK 29 gated)AbstractC19717a…AbstractC19719c
125rrp534rr2TP RxJava2SerializedExecutor / ExecutorWorkerC20003a, C20004b
126jnp371jn1TP FacebookApp-Events HTTP header provider — hardcodes X-Cloud-Trace-ContextC17668a
127ahp013ah2TP Firebase CrashlyticsNo-op session-signal reporter used before the Crashlytics component is readyC0137b
128hjp327hj1TP androidx.startupInitProvider base (wall-clock / elapsed / uptime clocks, AtomicBoolean)AbstractC17075a
129jgp366jg1MERGE (TP)RoundingMode.ordinal() switch-map (BigDecimal / Moshi shared)AbstractC17625a
130b4p031b41TP FacebookApp Links classification: "Applink" / "Unclassified"C3459a
131vp591v1TP GlideUnit-type singleton (Size.class-style token)C20845a
132u9p577u91TP Fyber FairBidMediationManager accessor dispatcherC20707a
133j4p357j42TP Digital Turbine / MintegralShared 1-2 thread pool (queue 5, 30 s keepalive) + main-looper Handler; RejectedExecutionHandler is a no-opAbstractC17482b, RejectedExecutionHandlerC17481a
134sfp546sf2TP google-api-clientArrayList batch-request holder + LoggerC20116b, InterfaceC20115a
135h9p317h92TP Fyber FairBidInterfaceC9469hw impls: (int,String) and (double,String,String,String,boolean) — bid/revenue recordsC16953a, C16954b
136y8p675y81TP FacebookSoft-exception collector: WeakHashMap guard set + report on first exception (f59044b toggle)AbstractC21888a
137o2p446o21TP GlideDrawableTransition base (SIZE_ORIGINAL sentinel, no-op lifecycle)AbstractC19046a
138x2p643x22TP Pangle/ByteDanceCacheDirFactory scan result (File[] files, int count)C21596a
139pdp481pd4TP ExoPlayer2 UIStyledPlayerView/SubtitleView helper constants (merged with a marker interface)C19345a, C19347c, C19348d
140hsp332hs1TP MolocoAd view state enum LOADING / DEFAULT / HIDDENEnumC17125a
141ip333i1DEADSupportSQLiteDatabase exec/isOpen stub — both bodies are throw new UnsupportedOperationException() / return false (§3.2)AbstractC17126a
142fpp295fp6TP Kotlin stdlibkotlin.collections.* marker interface hierarchy hoisted out of kotlin/ (consumed by androidx.collection, kotlinx, Ktor zm, Glide)InterfaceC16658a…InterfaceC16663f
143unp586un4TP Kotlin stdlibRunnable/Consumer/Function/Predicate functional interfaces hoisted out of kotlin.jvm.functionsInterfaceC20751a…InterfaceC20754d
144eip269ei1TP GlidePre-draw measured-height helper (handles ScrollView child height + padding)C16276a
145xrp665xr3TP MintegralResource-id constants (ad_attr, fsn_layout_template, countdown_seconds, banner_ad_size_txt, …) — R-class fragmentAbstractC21813a…AbstractC21815c
146app025ap1TP utilReflective Build$VERSION.SDK_INT holder (duplicate of zo)AbstractC3242a
147u2p570u23TP Pangle/CSJDownload-progress listener interfaces (psq/xoo with (AbstractC4358a,int[,String]))InterfaceC20571a, AbstractC20573c
148qgp500qg1TP Firebase (Dagger)Memoising Map<String, C19352b> factory over a Context + providerC19720a
149jqp372jq3TP kotlinx.serializationSerializersModuleBuilder — no-op contextual(KClass, List) registrationAbstractC17678b, C17677a
150oap453oa1TP Fyber FairBid test suiteEventBus Handler.Callback pumpC19104a
151n6p431n61TP MolocoActivity-scoped Closeable that closes the ad activity by string keyC18710a
152yop690yo1TP Kotlin stdlibReflective lookup of Throwable.addSuppressed(Throwable)AbstractC21962a
153l0p395l01DEAD (TP Amazon APS)All four ApsAdListener forwarders have empty bodiesAbstractC18269a
154r3p514r32TP Joda-TimeCron field/parser data holder ("CronFieldName must not be null"), merged with a Facebook context objectC19828a, C19829b
155i8p341i81TP FacebookApp-Events sampling gate: fires only when random <= 1e-4 and the event name contains "gps"C17197a
156akp015ak1TP libphonenumberMetadata bootstrap: /com/google/i18n/phonenumbers/data/{PhoneNumber,ShortNumber,PhoneNumberAlternateFormats}ProtoC0154a
157l8p400l82TP Firebase/GMSGpsTopicsManager (Google-Play-services topics, not GPS) + cached-thread-pool providerAbstractC18317b, C18316a
158f9p286f91TP Fyber FairBidAd-format enum OFFER_WALL("ofw") / UNKNOWN("unknown")EnumC16513a
159ypp691yp1TP utilDirect (same-thread) ExecutorExecutorC21963a
160d2p233d21DEAD (TP Glide)Pass-through ResourceEncoder — empty MessageDigest update + return interfaceC21595z unchangedC15798a
161kjp388kj1DEAD (TP FIAM)Synthetic Runnable whose two switch arms call the same method with the same argsRunnableC17851a
162e4p259e42TP Digital Turbine IgniteService/credential callback interfaces (onIgniteServiceAuthenticated, onOdtUnsupported, onCredentialsRequestSuccess)InterfaceC16096a, InterfaceC16097b
163mfp423mf1TP Google Play In-App ReviewError maps: "Call first requestReviewFlow to get the ReviewInfo.", PLAY_STORE_NOT_FOUND, INTERNAL_ERRORAbstractC18588a
164zop718zo1TP utilReflective Build$VERSION.SDK_INT holder (duplicate of ap)AbstractC22295a
165qmp504qm1TP (owner unresolved)"SSECapability" capability singleton; referenced only from p440nmC19733a
166zjp714zj1TP FacebookAccess-ordered LinkedHashMap LRU (removeEldestEntry on AppEvents cache limit)C22238a
167yfp681yf1TP google-api-clientSingleton placeholder for the API-client credential factoryAbstractC21930a
168f4p281f42TP Fyber Inneractive / Ignite2-method lifecycle marker (mo14085a(), mo14086a(String))InterfaceC16414a
169oop464oo1MERGE (TP)Dagger Provider<T> { Object get(); } hoisted to its own package (21 consumers)InterfaceC19219a
170lhp405lh1DEAD (TP Firebase)Empty marker interface referenced only by FirebaseCrashlyticsKtxRegistrarC18377a
171bp026b1TP Amplitude Session ReplayView→windowAttachCount shim for replay target discoveryAbstractC3398a
172gcp307gc1MERGE (TP)Clock (long mo32569c()) shared by Glide’s Timer/SystemClock and Google DataTransportInterfaceC16728a
173nsp442ns1DEAD (TP)Empty marker interface; consumers are Moloco AdDataNative and mbridge thInterfaceC18919a
174fp277f1DEADabstract /* synthetic */ class with a single f5…c field and no methods, no importers anywhere in the APKAbstractC16400a
175o3p447o31DEAD (TP)Empty marker, single importer (p511r0, the Crashlytics component registry)AbstractC19047a
176nhp437nh1DEAD (TP Pangle)Empty marker, single importer (vg)InterfaceC18806a
177dpp250dp1TP Kotlin reflectKotlinReflectionNotFoundError ("kotlin-reflect.jar is not in the classpath")C15998a
178uhp582uh1DEAD (TP FCM)Empty marker, single importer (FirebaseMessagingRegistrar)InterfaceC20741a
179iip348ii1DEAD (TP FIAM)Empty class, importers are FirebaseInAppMessagingRegistrar, fi, gi, p326hiC17305a
180tbp558tb1DEAD (TP)Empty abstract class; single importer be (an AIDL Binder base)AbstractC20366a
181n9p434n91DEAD (TP Fyber)m35090a(BannerWrapper) with an empty bodyAbstractC18747a
182ljp406lj1DEAD (TP Firebase)Empty marker; importers CrashlyticsRegistrar, RemoteConfigRegistrar, p349ijInterfaceC18388a
183zgp711zg2TP Firebase CrashlyticsSessionCallbacks (onEvent(String, Bundle)) + session-signals providerInterfaceC22216a, InterfaceC22217b
184fcp288fc2TP utilCallable-like execute() marker + empty interfaceInterfaceC16528a, InterfaceC16529b
185d4p235d41TP Digital Turbine IgniteIgnite service-API triple (auth, BinderC22068b callback, IIgniteServiceAPI)C15800a
186rfp523rf1DEAD (TP google-api-client)GoogleTokenResponse base with no bodyC19909a
187x3p644x31TP Firebase CrashlyticsCron field + clock + thread triple holder for session signalsC21598a
188ofp458of1DEAD (TP google-api-client)Empty AbstractC19122a extends com.google.api.client.util.Data — OIDC IdTokenResponse baseAbstractC19122a
189lmp408lm1DEAD (TP Joda-Time)Empty marker; single importer omInterfaceC18400a
190y2p669y21DEAD (TP Pangle)Empty marker; importers are bytedance.sdk.openadsdk.component.reward.*InterfaceC21841a
191rlp529rl1DEAD (TP uCrop)Empty 3-method interface implemented by yalantis.ucrop.view.widget.HorizontalProgressWheelViewInterfaceC19952a
192uep581ue2TP AndroidXEmpty FrameLayout base + abstract view baseAbstractC20735a, AbstractC20736b
193p2p471p22DEAD (TP Glide)Empty singleton pair (C19288a/C19289b); importers bumptech.glide.C4204a, C4206cC19288a, C19289b

(§2.6 and §2.7 are referenced from the table in §4 and are written up here, immediately after it, for readability.)


2 (continued) — additional NEW security-relevant findingsh3

2.6 MEDIUM — Amplitude Session Replay is enabled with a 10 MB local bufferh4

p696z0/C22053a.java (real pkg z0) — Amplitude Session Replay

public C22053a() {
super(true, /*debug*/ false, true, 10485760L);
this.f60152a = true; // getPack() == true
this.f60153b = false; // getLogPayloads() == false
this.f60154c = true; // getTransform() == true
this.f60155d = 10485760L; // getMaxStorageBytes() == 10 MB
}

Session Replay is a UI-session recording feature: Amplitude captures a text/structural representation of what the user sees and does, batches it locally, and uploads it. Here the masking transform() pipeline is on and payload logging is off, but the recording itself is enabled and a 10 MB rolling buffer is kept on-device. MICO is a caller-ID app whose screens are, by construction, lists of the user’s contacts and their caller identities — so the replay stream is a third-party copy of the contact graph. Supporting classes in the same cluster: v0 (TargetLocator for Compose views), b (windowAttachCount), oh (field descriptors), t0 ([Amplitude] Fragment Viewed), o1/p0 (Identity with userId/deviceId), h1 ($identify user-property ops), ub (typed events), n1 (Singular-style event queue), hc (priority mapping), u8 (GateKeeper), x8 (uncaught-exception reporter), v2 (MD5 helper).

Severity note: this is a configuration fact, not a vulnerability — but combined with the already-documented cleartext remote-config channel (which can flip SDK behaviour remotely) it means a large amount of on-screen personal data can be exported to a third party without any user-visible control in MICO itself.

2.7 LOW-MEDIUM — App ships a client for Meta’s com.facebook.ppml.receiver.IReceiverServiceh4

p036b9/AbstractBinderC3527b.java + C3526a.java + InterfaceC3528c.java (real pkg b9)

A hand-transacted AIDL proxy for a bindable service named com.facebook.ppml.receiver.IReceiverService:

parcel.writeInterfaceToken("com.facebook.ppml.receiver.IReceiverService");
parcel.writeInt(1); // TRANSACTION_...
bundle.writeToParcel(parcel, 0);
f2963a.transact(1, parcel, reply, 0); // flags = 0 -> two-way Binder

“PPML” is Meta’s privacy-preserving-ML measurement pipeline (the receiver used for measurement/attribution hand-offs). Its presence means MICO can bind to — and therefore deliver a Bundle of attribution/measurement payload into — a Meta-owned component inside the same app process. The Bundle contents are not built here (the proxy only carries them), so the payload shape must be read from the caller, which lives outside this cluster.


5. Packages that could NOT be classified with confidenceh3

Only three packages in the 193 could not be attributed to a specific SDK/library, and none of them contain logic, I/O, or data access:

Real pkgDiskFilesWhy unresolvedRisk of being wrong
qmp504qm1C19733a is a unit-singleton whose only identity is toString() == "SSECapability"; the single importer is p440nm/C18876e.java, itself an unlabelled utility package. "SSE" almost certainly means Server-Side Encryption, but no SSE call, key, or endpoint appears anywhere in the file or in p440nm.Very low — the class is a 40-byte marker with no members beyond equals/hashCode/toString.
sfp546sf2Attributed to google-api-client because the only two importers are com\google\api\client\googleapis\services\AbstractC11800b/C11802d. The classes themselves (ArrayList holder + empty interface + Logger.getLogger(...) in a static block) have no distinguishing content, so “google-api-client BatchRequest item” is an inference from the importers rather than from the code.Very low.
tbp558tb1AbstractC20366a is an empty abstract class whose only importer is be, and be is itself an AIDL Binder base (AbstractBinderC3539b extends Binder implements IInterface). The pair is clearly some SDK’s IPC layer, but the SDK identity cannot be pinned from a single empty file.Very low — the file has one synthetic int field and no behaviour.

Two further packages are deliberately over-classified in §4 and are called out here so the reader does not over-trust them:

  • s3 / u3 — I have labelled these Quartz-style cron field value models (L, W, LW, a#b, ?, -). That the semantics are cron is certain ("value missing for a#b cron expression"), but the owning SDK is not: the consumers are bm, p029b2 (Google Measurement errors), p035b8, p472p3, p511r0, p595v3, p617w3 and com\facebook\C9067e, i.e. R8 collapsed several SDKs’ cron models into one shared package. Treat the “MERGE” label as the reliable part.
  • hc — the Priority→int mapper is genuinely shared by androidx.transition, Facebook AppEvents and Google DataTransport (EnumC20713d is Amplitude’s DEFAULT/VERY_LOW/HIGHEST). The single package is an R8 merge of at least three consumers.

Everything else in the 190 remaining packages is attributed to a named SDK/library on the evidence of an import of that library’s own readable package (e.g. com.fyber.fairbid.*, com.bytedance.sdk.openadsdk.*, tech.crackle.*, com.google.android.gms.ads.*, com.facebook.*, com.amazon.device.ads.*, com.bumptech.glide.*, io.ktor.*, com.google.api.client.*, bumptech.glide.*) or of its distinctive class/constant strings.


6. Dead code, stubs and R8 merge artifacts — how much of C6b is actually live?h3

6.1 Hard dead code (bodies emptied or methods throw)h4

PackageWhat is deadEvidence
i (p333i)Both methods of AbstractC17126a — execSQL throws UnsupportedOperationException(), isOpen returns falseclass declared abstract /* synthetic */; this is androidx.sqlite’s synthetic accessor that R8 failed to inline
f (p277f)AbstractC16400a — one synthetic int field, no methods, no importers in the entire 38,575-file APKgrep for import p277f. returns zero hits
d2 (p233d2)Glide ResourceEncoder whose MessageDigest update is empty and whose getData() returns the input unchangedempty mo3591b(MessageDigest)
kj (p388kj)RunnableC17851a — both switch arms call the identical method with identical argscase 0: and default: are byte-identical
p2 (p471p2)Empty singleton pair2 zero-body classes
l0 (p395l0)All four ApsAdListener forwarders (Amazon APS)four empty method bodies
n9 (p434n9)m35090a(BannerWrapper) (Fyber)empty body
lh, ns, o3, nh, uh, ii, tb, rf, of, lm, y2, rl, ue, p2empty class / empty interface with only marker role13 packages are literally public interface X {} / public final class X {} with no fields and no methods; fp contributes 6 more such files
o3, p2, tb, of, rf, lm, y2marker classes whose only purpose was to be a type token—
a2 (p004a2)ONE Thread subclass carries a 5-way switch over five unrelated captured objects (Glide / Coil / ExoPlayer / Amplitude / androidx.work)each arm is reachable from exactly one library, so the class is only “alive” as a shared container
k9, j9, s9, i9, yh/zb/wh/ul, on, wr, b9…synthetic switch(int) dispatchers created by R8 to de-sugar when/lambdas — dead in the sense that no single instance is reachable from more than one call site, but they are all genuinely executed—

6.2 Dead / no-op inside live classesh4

  • lq/C18443h.mo34378a() returns new ArrayList(0) — the AOSP cornermark badge adapter’s “supported launchers” list is empty, so this adapter can never be selected by ShortcutBadger and its ContentResolver.call(content://com.android.launcher3.cornermark.unreadbadge, …) is unreachable. The other three adapters (OPPO / vivo / ZUI) are live.
  • f2/C16413a.mo31905b() has case 1: throw new UnsupportedOperationException("Not implemented").
  • a2/C0026a case 2 and case 0 contain empty try {} blocks (GlideExecutor, ExoPlayer:SimpleDecoder).
  • j4/RejectedExecutionHandlerC17481a.rejectedExecution() — silently drops every rejected task.
  • wr (Retrofit annotations) and b9/InterfaceC3528c are interface-only packages: zero executable code.
  • gg-style hoisting produced 30+ packages whose entire content is equals/hashCode/toString on a 2-3 field record (C21842a, C21843b, C21941a, C19043b, C18690a, …). These are live but carry zero behaviour.

6.3 R8/ProGuard merge artifacts (code exists, but its home is a lie)h4

These packages are real, reachable code whose package name is meaningless because R8 merged several libraries’ identically-named inner classes into one destination:

PackageMerged from
a2Glide GlideExecutor, Coil, ExoPlayer SimpleDecoder, Amplitude timeline, androidx.work
s3, u3≥4 SDKs’ cron/period models (Google Measurement, Firebase, Facebook, WorkManager)
hcAmplitude + androidx.transition + Facebook AppEvents + DataTransport priority enum
ooDagger Provider (21 consumers across the whole APK)
gcGlide Timer/SystemClock + Google DataTransport Clock
fp, unKotlin stdlib kotlin.collections.* / kotlin.jvm.functions.* (imported by androidx.*, kotlinx.*, Ktor)
jgRoundingMode.ordinal() switch-map shared by BigDecimal and Moshi code
lqShortcutBadger, with MICO’s MyApplication substituted for the library’s ShortcutBadgeApp parameter
d0Lottie path keyframes + a Facebook holder in one class
ap, zoDuplicate reflective Build$VERSION.SDK_INT holders

6.4 Bottom line on livenessh4

Of the 193 packages / 472 files in C6b:

  • ~1 % (2 packages, 10 files) is first-party MICO logic (i5, and the MICO-typed half of lq).
  • ~7 % (12–13 packages) is hard dead code.
  • ~10 % (19–20 packages) is empty marker/interface/R-class fragments with no behaviour.
  • ~5 % (9 packages) are R8 merge artifacts whose package attribution is meaningless.
  • The remaining ~70 % is genuinely-live third-party SDK glue — but it is ad-stack glue, not product logic. Combined with C6a, the practical conclusion is that a very large fraction of this APK’s obfuscated code is monetisation and telemetry infrastructure, not MICO’s caller-ID feature set. That matters when judging how much of the app’s 38,575 files is actually shipped logic: the ad/analytics surface area dwarfs the product surface area by roughly an order of magnitude.

7. Cross-checks against the existing findings listh3

Existing findingConfirmed / extended in C6b?
Blowfish/ECB contact-photo crypto, cipher_key in prefs, ObjectInputStream.readObject() over decryptable file bytesNot in C6b. The only ObjectInputStream here is bk/C3582b (libphonenumber metadata reader, §3.2) — a different sink, first-party-relevant content is absent.
https://api.eyecon-app.com/app/ base, e-auth-* headers, rc_key = "PgdtSBeR0MumR7fO"Not in C6b. Zero MICO URLs/keys here; the only URL + key in the cluster belong to Google Crashlytics (§2.2).
~400 A/B flags + remote config over cleartext HTTP, shouldSendNameToServerNot in C6b. No remote-config fetch code here. The only remotely-driven behaviour is SDK-side: Facebook’s deprecated_param / restrictive_param maps (h8, r8) and Meta’s “Enabled” gate on the mDNS probe (t8) — both fetched by the FB SDK over HTTPS.
GET cantalk.jsp?recvcli=…&message=<SMS body>; first-party spam report as GET query stringNot in C6b. No SMS/call-log code at all (§3.3).
Premium entitlement 100 % client-side, no Google purchase verificationExtended: i5 (§2.1) shows the exact client-side store (SP_KEY_INAPP_PRODUCTS_CACHE) and that it is written from first-party a7 code on purchase rows. No Play Billing verification class exists in C6b.
f5 = Google Drive backup of raw contactdb; n4 = generated R-class; zd = ExoPlayerExtended: n4-style R-class fragments continue in xr (Mintegral resource ids) and pd (ExoPlayer2 UI constants). zd-family ExoPlayer code continues in oc, kd, ed, vf, vf-adjacent Mintegral HTTP.
Ads: Mintegral, Moloco, Liftoff/Vungle, Fyber/DT-Exchange, InMobi, Pangle, Amazon APS, AdMob mediationConfirmed and extended: this cluster adds the inner implementation packages of every one of those SDKs (ks Moloco crypto, os/hs/yk/n6 Moloco, kl/pb Vungle, ab/xa/h9/n9/p9/s9/f9/u9/oa/xq/f4 Fyber FairBid + Inneractive, i9/xq/f4/c4/i4/y3/e4/d4/z3/j4/g4 Digital Turbine Ignite, ib/hb InMobi, c3/d3/e3/nb/t2/u2/z2/x2/y2/nh Pangle, p4/l0 Amazon APS).

Corrections to earlier claims on this workh2

Where an earlier claim of mine turned out to be wrong, the corrected version is what appears above and the superseded version is not. Two of them are worth naming because both were wrong in ways that would have mattered. There is no call recording anywhere in the app. And the cleartext remote-config channel does not rotate the rc_key: it carries feature flags, not key material.