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.
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.
| part | covers | source |
|---|---|---|
| I | scope, target identity, obfuscation map, method, tooling state | workspace README |
| II | manifest, components, permissions, network config | MANIFEST-AND-SECURITY.md |
| III | e-auth, endpoints, HTTP stack, device fingerprint, secrets | BACKEND-API.md |
| IV | databases, tables, preferences, the clientId chain | DATA-LAYER.md |
| V | 29 feature modules and nine reconstructed flows | FEATURE-AND-FLOWS.md |
| VI | native libraries, ARM binaries, Pangle provenance | three native documents |
| VII | every first-party package, cluster by cluster | seven 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
| Path | Size | Contents |
|---|---|---|
src/sources/ | 38,575 .java files | Complete decompiled source tree (JADX) |
src/resources/ | 6,800 files | Decoded resources.arsc, layouts, manifests, assets |
docs/BACKEND-API.md | 83 KB, 1049 lines | Every outbound HTTP call, headers, params, auth algorithms |
docs/DATA-LAYER.md | 65 KB, 1110 lines | 8 databases, 21 tables, ~250 preference keys, storage layout |
docs/MANIFEST-AND-SECURITY.md | 72 KB, 741 lines | 96 activities, 27 services, 27 receivers, 9 providers, 78 permissions, security surface |
docs/FEATURE-AND-FLOWS.md | 164 KB, 2008 lines | 29 modules catalogued, 9 end-to-end flows reconstructed, obfuscation→identity map |
docs/cluster-C1-core-platform.md | 172 KB, 2354 lines | 16 pkgs / 350 files — foundation: constants, HTTP core, prefs, remote config |
docs/cluster-C2-registration-billing.md | 155 KB, 2574 lines | 50 pkgs / 962 files — registration, CLI auth, Play Billing, JS bridges |
docs/cluster-C3-dialer-call-sms.md | 118 KB, 1121 lines | 42 pkgs / 843 files — dialer, spam, AI SMS assistant, TTM |
docs/cluster-C4-main-contacts.md | 120 KB, 1464 lines | 27 pkgs / 489 files — contact sync, photos, main screen, reminders |
docs/cluster-C5-media-social-extra.md | 204 KB, 2186 lines | 14 pkgs / 229 files — Toki voice, backup, photos, social, ads |
docs/cluster-C6a-unclassified.md | 59 KB, 706 lines | 193 pkgs / 2426 files — 177 third-party, 9 first-party, 16 deep entries |
docs/cluster-C6b-unclassified.md | 60 KB, 637 lines | 193 pkgs / 472 files — tail-end stubs and dead code |
Total footprint: 44,575 files / 314 MB.
Target identityh4
package com.eyecon.globalversionCode 749versionName 4.2026.04.23.1145minSdk 23 targetSdk 35 compileSdk 36size 65,604,949 bytesDEX 6 files (classes.dex … classes6.dex) + assets/audience_network.dex (5,017,908 B)classes 54,878 across 8,291 packagesABIs arm64-v8a, armeabi-v7a, x86, x86_64processes main, :notification_process, :restart_appCorrections to BACKEND-FINDINGS.mdh4
The first pass left open questions that the deeper pass resolved:
- CID origin — RESOLVED.
clientId(thee-authcredential) is thejoin.jspresponse body. It is server-issued, not a device identifier. SeeFEATURE-AND-FLOWS.md§A. - The AI SMS assistant does NOT use voice2text. It posts to
numberlookup.instabridge.comover cleartext HTTP.voice2text.eyecon-app.comis the separate Talk-To-Me voicemail-to-text service. The first pass conflated them. e-auth-kis rotatable.PgdtSBeR0MumR7fOis the compiled-in default, but Remote Config keycircus_keycan reflectively overwrite thestatic finalfield at runtime (p032b5.AbstractC3464e.m3668s()), and that Remote Config is fetched over cleartext HTTP.EyeconRoomDBis dead code. The entity class is an image utility, the DAO is empty, it is never instantiated. MICO uses raw SQL exclusively.- App SharedPreferences is not Android SharedPreferences — it is a custom implementation
backed by a
shared_preference_v2table insidecontactdb. - Native libraries are not app protection.
libpglarmor.so/libtobEmbedPagEncrypt.so/libnms.so/libbuffer_pgl.soare 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:
| Obfuscated | Identity |
|---|---|
a7 | constants + SQLiteOpenHelper |
w6 | HTTP core (NetworkHelper), Server facade, API work queue |
f7 | custom SharedPreferences, emulator detector, ServerMySettings |
q7 | registration callbacks, validation, error enums |
x7, w9 | spam model + spam DB |
x5, wo | AI SMS assistant (Retrofit + Moshi) |
e7 | AsyncCallbackBase |
jd | join.jsp client |
r5 | cli_auth / checkcode |
x6 | durable request queue |
yl, wl, as.u2 | Toki UDP transport / session / TalkiServer |
f5 | Google Drive backup |
g5, j5 | Play Billing + PaidPremium/FreePremium |
k7 | dead 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, andfct= the pair asapp_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_scriptfetched over plain HTTP fromcloudfront-cdn-all.eyecon-app.com. Enables JS injection, cards-host repointing (leakingcli+cid), and audio redirection. (This replaces the retracted “rc_key rotation” claim.)
Highh5
- AI SMS assistant is unauthenticated and cleartext —
x5.p.m38847dPOSTs up to 50 messages from inbox and sent, plus contact name, number and display name, tonumberlookup.instabridge.com/respond-smsover 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
mysettingsPOST. Purchase tokens and a page-chosenofferTokenare handed to remote web content. StoreActivityleaks the bearer token — remote-configstore_urlwith no scheme, host or pinning validation, loaded into a JS WebView exposingclientId(thee-authbearer), GAID, MSISDN and an unfiltered SharedPreferences reader.WebviewPaywallActivityimplements exactly the missing validation, so the fix is known internally and applied in one place only.MyPhoneReceiveris exported with no permission and honoursCALL_SCREENING_NEW_CALLandINTENT_ACTION_START_CALL_WHATSAPP, takingstateandincoming_numberverbatim 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 viaACTION_DOWN/ACTION_UP, except the notification-answer path starts the mic with no release. - Unencrypted
contactdbto Google Drive (full SMS bodies, notification text, voicemail transcripts), restored by arenameToswap. PhotosTrackerWorkersilently 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.EyeconAnalyticsWorkercan ZIP and upload the entire call log; disabled by default but remotely flippable, with afrom_daterewind for re-upload.- Contact-photo “encryption”: Blowfish/ECB keyed
"Ewu" + nextInt(999) + "sTQHGK"→ 999 possible keys, key stored plaintext in prefs ascipher_key, withObjectInputStream.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 onjoin.jsp,cli_auth/gettransport,addcontacts.jsp. - Singular (
e1) harvests GAID via reflection and discardsisLimitAdTrackingEnabled()— 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.m31717ecallsclearApplicationUserData()on DB corruption andSystem.exit(0)on any prefs read error, destroyingclientIdand forcing re-registration.- Moloco banner WebView: JS enabled +
evaluateJavascript("beginVideo(" + <VAST data> …), with creative URLs passed to an unschematised externalACTION_VIEWintent. PhoneAccountis a dead stub —getAuthTokenthrowsUnsupportedOperationException.- 401 +
E-Marktriggers a destructive client-side wipe of auth state, reachable via the same cleartext path.eyecon://browser?url=intent redirection; exportedPhoneNumberValidationActivity.onNewIntentextras injection.
Explicitly NOT findings / NOT presenth5
- No call recording exists. No
MediaRecorder, noVOICE_CALL/DOWNLINK/UPLINK, noMediaProjection. The onlyAudioRecordapp-wide is an AMR voice-note. TheDocuments/Eyecon/RecordedCallsdirectory is index/scan/migrate/promote/ACTION_SENDonly. - 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
pglnative 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
| Tool | State |
|---|---|
| JADX MCP | Live. Used for interactive reads and naming of the CLI export. |
| JADX CLI 1.5.6 | Used 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.2 | Installed 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>8080> answers. |
| Ghidra bridge | Configured in ~\.config\opencode\opencode.jsonc → ghidramcp\bridge_mcp_ghidra.py, target http://127.0.0.1:8080/. Currently refused. |
| IDA Pro 9.4 | Installed 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 registration | Written to ~\.config\opencode\opencode.json as type: remote, http://127.0.0.1:13337/mcp. |
| REA MCP | Registered 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.
- Response schemas for every endpoint — only request shapes are recoverable statically.
- Whether
e-auth-kis validated server-side at all. E-Mark/CID401 semantics — needs runtime or server-side evidence.- 392 classes (1.2%) failed decompilation; contents unknown.
- The phone-call audio capture call site was not located; only Toki’s
AudioRecordexists in app code. PhoneAccount.SyncService.onPerformSyncis empty — the contacts account is registered and unused.- The GhidraMCP extension declares
ghidraVersion=11.3.2against 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
| Field | Value |
|---|---|
| Package | com.eyecon.global |
| versionCode / versionName | 749 / 4.2026.04.23.1145 |
| minSdk / targetSdk / compileSdk | 23 / 35 / 36 |
| installLocation | auto (SD-card install permitted) |
| Application class | com.eyecon.global.Others.MyApplication |
| Manifest | src/resources/AndroidManifest.xml (1669 lines) |
| Component totals | 96 activities, 4 activity-aliases, 27 services, 27 receivers, 9 providers |
| Permission entries | 78 (77 uses-permission + 1 uses-permission-sdk-23), plus 3 self-declared <permission> |
| Processes | 3 total - com.eyecon.global (main) + 2 remote (:notification_process, :restart_app) |
uses-feature | 3 (all required="false") |
<queries> | 54 <package> + 26 <intent> = 80 visibility declarations |
meta-data | 58 total (22 on <application>, 36 on components) |
| Backup | android:allowBackup="false" and android:fullBackupContent="false" - no dataExtractionRules |
| Network | usesCleartextTraffic="true" + @xml/network_security_config |
1. Manifest componentsh4
1.1 Processesh5
| Process | Declared on | Purpose |
|---|---|---|
com.eyecon.global (main) | default | Everything 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_process | IdPlus.NotificationReader.NotificationReaderService | NotificationListenerService - isolated so the WhatsApp/Telegram notification scraper cannot crash the caller-ID process. |
:restart_app | Others.Activities.RestartAppActivity | Trampoline 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
| Activity | exported | launchMode | taskAffinity / process | Intent filters (action -> data) | Notes |
|---|---|---|---|---|---|
MainScreen.MainActivity | true | singleTop | - | 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.BackupActivity | default | - | - | - | |
Call.HistoryLogActivity | default | - | - | - | |
Others.Activities.RestartAppActivity | default | - | :restart_app | - | separate process |
Registration.RegistrationActivity | default | singleTask | - | - | 4354 lines; sends verification SMS |
Registration.PhoneNumberValidationActivity | true | singleTask | - | VIEW/eyecon_number_validation (BROWSABLE) | onNewIntent trusts external extras - see F-02 (M) |
MoreMenuAndSettings.MyProfileActivity | default | - | - | - | |
Contacts.NewContactActivity | true | - | - | INSERT_OR_EDIT/APP_CONTACTS + mime contact/vcard; INSERT + mime cursor.dir/person|contact|raw_contact | Contacts-app integration |
Photos.PhotoPicker.PhotoPickerActivity | default | - | - | - | camera capture + crop; FileProvider output URI |
CanTalk.CanTalkActivity | default | singleInstance | .CanTalkActivity, excludeFromRecents, noHistory | - | |
Others.Activities.DummyActivity | true | - | .eyecon, excludeFromRecents | VIEW/eyecon_request (BROWSABLE) | Also reachable by explicit intent with arbitrary action + extras - see M-04 |
MoreMenuAndSettings.SettingActivity | true | singleTask | - | VIEW/eyecon_settings (BROWSABLE) | |
MoreMenuAndSettings.AboutActivity | default | - | .AboutActivity | - | |
MoreMenuAndSettings.LicensesActivity | default | - | .AboutActivity | - | |
Others.Activities.TestActivity | default | singleTask | - | - | Reads cli extra |
AfterCall.CompactAfterCallActivity | default | singleInstance | .AfterCallActivity, excludeFromRecents | - | |
AfterCall.BaseAfterCallActivity | default | singleInstance | .AfterCallActivity, excludeFromRecents, noHistory, showWhenLocked + turnScreenOn | - | registers 3 dynamic receivers |
AfterCall.AfterCallWithNoAutoFinish | default | singleInstance | .AfterCallActivity, excludeFromRecents, showWhenLocked + turnScreenOn | - | |
AfterCall.DefaultAfterCallActivity | default | singleInstance | .AfterCallActivity, excludeFromRecents | - | |
Others.Activities.FragmentsActivity | default | - | - | - | |
Billing.Store.StoreActivity | true | - | - | VIEW/eyecon_store (no category) | JS-enabled WebView + 2 JS bridges, remote-config URL - see H-01 |
Billing.Premium.PremiumPurchasingDefaultActivity | default | singleTask | - | - | |
Billing.Premium.PremiumPurchasingPaywallUiActivity | default | singleTask | - | - | |
Billing.Premium.GoldPremiumPurchasingActivity | default | singleTask | - | - | |
DefaultDialer.CallActivity | default | - | .CallActivityTask, showWhenLocked + turnScreenOn + showForAllUsers + autoRemoveFromRecents | - | in-call UI |
MainScreen.Communication.EyeSearchActivity | default | - | - | - | |
Contacts.ContactsChooserActivity | default | - | - | - | |
Billing.Premium.GetFreePremiumActivity | default | singleTask | - | - | |
Billing.Premium.FreePremiumUserActivity | true | singleTask | - | EYECON_PREMIUM_UPDATE (BROWSABLE) | |
Billing.Premium.PremiumUserActivity | default | singleTask | - | - | |
Sms.SmsWindowActivity | default | - | .SmsWindowActivity | - | |
ContactReminder.ContactReminderActivity | default | - | .ContactReminderActivity | - | |
IdPlus.NotificationReader.NotificationReaderWindowActivity | default | - | .NotificationReaderActivity | - | renders scraped notification text |
IdPlus.Telegram.TelegramWindowActivity | default | - | .TelegramWindowActivity, excludeFromRecents | - | |
Call.TalkToMe.p204UI.TtmWindowActivity | false | singleInstance | .NotificationReaderActivity, excludeFromRecents, noHistory | - | |
Toki.TokiActivity | default | singleTop | .toki | - | |
Toki.TokiContactsChooserActivity | default | singleTop | .toki | - | |
Permissions.ExtendedPermissionPromptActivity | default | - | - | - | |
Themes.CustomThemePreviewActivity | default | - | - | - | |
Call.TalkToMe.p204UI.TtmTutorialActivity | default | - | - | - | |
AfterCall.SplashInterstitialActivity | false | - | .AfterCallActivity | - | |
Registration.Onboarding.OnboardingActivity | default | - | - | - | |
Registration.Onboarding.FakeCallActivity | default | - | - | - | |
Ads.Google.FullscreenAds.FullscreenNativeAdsActivity | default | - | - | - | |
DefaultDialer.assistant.OnboardingActivity | default | - | - | - | |
Billing.Premium.WebViewPaywall.WebviewPaywallActivity | default | - | - | - | JS bridge Eyecon - see M-06 |
com.yalantis.ucrop.UCropActivity | default | - | - | - | image crop |
com.facebook.FacebookActivity | default | - | - | - | |
com.facebook.CustomTabActivity | true | - | - | VIEW/@string/fb_login_protocol_scheme = fb1716660955257637; VIEW/fbconnect host cct.com.eyecon.global | FB SDK OAuth callback |
com.facebook.CustomTabMainActivity | default | - | - | - |
Third-party / SDK activitiesh6
| Activity | exported | Notes |
|---|---|---|
tech.crackle.cracklertbsdk.vast.ShowFullScreenVideo | true | no filter -> explicit intent only. Ad-SDK “show me an ad” surface (L-05) |
tech.crackle.cracklertbsdk.ShowFullScreenStatic | true | same |
tech.crackle.core_sdk.ads.CrackleFullScreenActivity | true | same |
com.amazon.aps.ads.activity.ApsInterstitialActivity | true | same |
com.amazon.device.ads.DTBInterstitialActivity | true | same |
com.google.android.gms.ads.AdActivity | false | multiprocess="true" |
com.google.android.gms.ads.NotificationHandlerActivity | false | singleTask |
com.bytedance.sdk.openadsdk.activity.* (13 activities) | default (false) | no filters |
com.moloco.sdk.xenoss...* (4 activities) | false | |
com.inmobi.ads.rendering.InMobiAdActivity | default (false) | |
com.vungle.ads.internal.p226ui.VungleActivity | default (false) | |
com.fyber.* (7 activities) | default (false) | |
com.mbridge.msdk.* (4 activities) | default/false | |
com.facebook.ads.AudienceNetworkActivity | false | |
com.android.billingclient.api.ProxyBillingActivity(V2) | false | |
androidx.credentials.playservices.HiddenActivity | false | |
com.google.android.gms.auth.api.signin.internal.SignInHubActivity | false | |
com.google.android.gms.common.api.GoogleApiActivity | false | |
com.google.android.play.core.common.PlayCoreDialogWrapperActivity | false | |
com.fyber.fairbid.sdk.testsuite.views.TestSuiteActivity | default (false) | debug surface of the ad SDK |
activity-aliases (4) - all exported="true" but enabled="false"h6
| Alias | target | Icon |
|---|---|---|
com.eyecon.global.pink | MainScreen.MainActivity | ic_launcher_white_icon_pink_bg |
com.eyecon.global.blue | MainScreen.MainActivity | ic_launcher_white_icon_blue_bg |
com.eyecon.global.green | MainScreen.MainActivity | ic_launcher_white_icon_green_bg |
com.eyecon.global.purple | Others.Activities.TestActivity | ic_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
| Service | exported | permission guard | process / FGS type | Intent filter | Notes |
|---|---|---|---|---|---|
Call.CallService | default(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.NotificationReaderService | true | BIND_NOTIFICATION_LISTENER_SERVICE | :notification_process | android.service.notification.NotificationListenerService | meta-data disabled_filter_types=silent. Reads WhatsApp/Telegram notification title+text |
Others.Tasks.ForegroundLauncherService | default(false) | - | main / specialUse | - | Subtype: “A workaround service that prevent crashes while trying to start other services” |
Toki.TokiService | default(false) | - | main / specialUse | - | |
DefaultDialer.CallStateService | true | BIND_INCALL_SERVICE | main / phoneCall | android.telecom.InCallService | meta-data IN_CALL_SERVICE_UI=true, INCLUDE_SELF_MANAGED_CALLS=true, IN_CALL_SERVICE_RINGING=false |
DefaultDialer.EyeconCallScreeningService | true | BIND_SCREENING_SERVICE | main | android.telecom.CallScreeningService | Reaches MyPhoneReceiver via RunnableC17334k case 7 |
Others.Tasks.JobsService | true | BIND_JOB_SERVICE | main | - | only JobScheduler can bind |
Push.PushService | false | - | main | com.huawei.push.action.MESSAGING_EVENT, com.google.firebase.MESSAGING_EVENT | |
PhoneAccount.SyncService | true | none | main | android.content.SyncAdapter | Standard pattern. supportsUploading=false, userVisible=true, contentAuthority=com.android.contacts |
PhoneAccount.EyeconAccountAuthService | true | none | main | android.accounts.AccountAuthenticator | Standard pattern (system binds) |
Sms.SmsJobService | true | BIND_JOB_SERVICE | main | - | |
SocialMediaShare.ShareMediaJob | true | BIND_JOB_SERVICE | main | - | |
com.google.firebase.components.ComponentDiscoveryService | false | - | directBootAware | - | 20 ComponentRegistrar meta-data entries (Messaging, Crashlytics, Perf, RemoteConfig, Sessions, Installations, ABT, DataTransport, InAppMessaging, AnalyticsConnector) |
com.google.firebase.messaging.FirebaseMessagingService | false | - | directBootAware | com.google.firebase.MESSAGING_EVENT (priority -500) | |
com.google.android.gms.auth.api.signin.RevocationBoundService | true | ...signin.permission.REVOCATION_NOTIFICATION | - | - | visibleToInstantApps |
com.google.android.gms.measurement.AppMeasurementService / AppMeasurementJobService / AppMeasurementReceiver | false | (JobService) | - | - | |
com.google.android.gms.ads.AdService | false | - | - | - | |
com.google.firebase.sessions.SessionLifecycleService | false | - | - | - | |
com.google.android.datatransport.runtime.backends.TransportBackendDiscovery | false | - | - | - | meta-data backend:...CctBackendFactory |
...jobscheduling.JobInfoSchedulerService | false | BIND_JOB_SERVICE | - | - | |
androidx.work.impl.background.systemjob.SystemJobService | true | BIND_JOB_SERVICE | - | - | |
androidx.work.impl.background.systemalarm.SystemAlarmService | false | - | - | - | enabled=@bool/enable_system_alarm_service_default |
androidx.work.impl.foreground.SystemForegroundService | false | - | - | - | |
androidx.room.MultiInstanceInvalidationService | false | - | directBootAware | - | |
androidx.credentials.playservices.CredentialProviderMetadataHolder | false | - | - | - | |
com.bytedance.sdk.openadsdk.multipro.aidl.BinderPoolService | default(false) | - | - | - |
1.4 Receivers (27)h5
| Receiver | exported | permission guard | priority | Intent filter |
|---|---|---|---|---|
IdPlus.NotificationReader.NotificationBroadcastReceiver | false | - | - | eyecon.action_notification_posted |
Others.Receivers.BootStartReceive | true | - | 2147483647 | BOOT_COMPLETED |
Others.Receivers.OnPackageReplaceReceiver | true | - | 2147483647 | ACTION_PACKAGE_REPLACED |
Others.Receivers.OnDataClearedReceiver | true | - | 2147483647 | ACTION_PACKAGE_DATA_CLEARED |
Call.MyPhoneReceiver | true | none | 2147483647 | com.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.MySmsReceiver | true | BROADCAST_SMS | 2147483647 | android.provider.Telephony.SMS_RECEIVED - system-only sender; no abortBroadcast() |
DefaultDialer.assistant.MessageAssistantBroadcastReceiver | true | BROADCAST_SMS | - | SMS_RECEIVED - extracts body + sender, serialises to JSON, enqueues MessageAssistantReplyWorker (network required) |
Call.CallServiceRestarterReceiver | false | - | - | eyecon.CallServiceRestart (label RestartServiceWhenStopped) |
Others.Receivers.MainProcessReceiver | false | - | - | EYECON_ACTION_GET_CS, ...RECORDING_SUCCESS, ...EXCEPTION, ...REPORT_EVENT |
Registration.InstallReferrerReceiver | true | android.permission.INSTALL_PACKAGES (sender must be Play Store / system-signed) | - | com.android.vending.INSTALL_REFERRER |
Analytics.RemoteConfig.RemoteConfigDebugTestReceiver | true | none | - | eyecon_remote_config_test - body is a no-op (result of equalsIgnoreCase is discarded) |
Registration.SMSRetrieverBroadcast | true | none | - | 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.CurrentAccessTokenExpirationBroadcastReceiver | false | - | - | com.facebook.sdk.ACTION_CURRENT_ACCESS_TOKEN_CHANGED |
com.facebook.C5849x7a527f5c | false | - | - | ...ACTION_CURRENT_AUTHENTICATION_TOKEN_CHANGED |
com.google.firebase.iid.FirebaseInstanceIdReceiver | true | com.google.android.c2dm.permission.SEND | - | com.google.android.c2dm.intent.RECEIVE; FINISHED_AFTER_HANDLED=true |
com.google.android.gms.measurement.AppMeasurementReceiver | false | - | - | - |
androidx.work.* (7 receivers) | false | DiagnosticsReceiver has DUMP | - | battery/storage/network constraint proxies, RescheduleReceiver, ForceStopRunnable.BroadcastReceiver |
androidx.work.impl.diagnostics.DiagnosticsReceiver | true | android.permission.DUMP | - | androidx.work.diagnostics.REQUEST_DIAGNOSTICS |
androidx.profileinstaller.ProfileInstallReceiver | true | android.permission.DUMP | - | 4 androidx.profileinstaller.action.* |
com.google.android.datatransport...AlarmManagerSchedulerBroadcastReceiver | false | - | - | - |
com.mbridge.msdk.foundation.same.broadcast.NetWorkChangeReceiver | true | - | - | android.net.conn.CONNECTIVITY_CHANGE (protected broadcast - system-only) |
1.5 Providers (9)h5
| Provider | exported | authority | grantUriPermissions | initOrder | meta-data |
|---|---|---|---|---|---|
com.eyecon.global.Others.Objects.EyeconInitProvider | false | com.eyecon.global.EyeconInitProvider | - | 101 | init-order provider that initialises the Facebook SDK (m10352a) |
androidx.core.content.FileProvider | false | @string/file_provider = com.eyecon.global.fileprovider | true | - | android.support.FILE_PROVIDER_PATHS -> @xml/file_paths |
com.facebook.FacebookContentProvider | true | com.facebook.app.FacebookContentProvider1716660955257637 | - | - | Required by the FB SDK because this app uses multiple processes |
com.google.android.gms.ads.MobileAdsInitProvider | false | com.eyecon.global.mobileadsinitprovider | - | 100 | - |
androidx.startup.InitializationProvider | false | com.eyecon.global.androidx-startup | - | - | WorkManager, EmojiCompat, ProcessLifecycle, ProfileInstaller, OkHttp Platform initializers |
com.fyber.fairbid.internal.UncaughtExceptionHandlerContentProvider | false | com.eyecon.global | false | 101 | authority == the app’s own package name |
com.vungle.ads.VungleProvider | false | com.eyecon.global.vungle-provider | - | 102 | - |
com.facebook.ads.AudienceNetworkContentProvider | false | com.eyecon.global.AudienceNetworkContentProvider | - | - | - |
com.squareup.picasso.PicassoProvider | false | com.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
| Name | Value | Note |
|---|---|---|
com.google.android.gms.ads.APPLICATION_ID | @string/admob_app_id -> ca-app-pub-2959190743815944~6092014645 | AdMob app id (public) |
com.google.android.gms.ads.flag.OPTIMIZE_INITIALIZATION | true | |
com.google.android.gms.ads.flag.OPTIMIZE_AD_LOADING | true | |
com.google.android.gms.ads.flag.rtb_enabled | true | Real-time bidding on |
com.google.android.gms.ads.INTEGRATION_MANAGER | webview | |
com.google.android.geo.API_KEY | AIzaSyD1LLixZGTwP6i3Ts8AzX4jxdb9zuaikKo | Google 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_tracker | ga_logLevel=verbose, ga_dryRun=false, ga_dispatchPeriod=1800, ga_autoActivityTracking=true |
com.facebook.sdk.ClientToken | @string/facebook_client_token -> afa3f38c55f24c73b177afc962969b15 | FB app id 1716660955257637 |
com.facebook.sdk.AutoLogAppEventsEnabled | false | FB auto-app-events off |
com.facebook.sdk.AdvertiserIDCollectionEnabled | false | FB advertiser-ID collection off |
com.facebook.sdk.AutoInitEnabled | false | manual 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=234363 | Ad-SDK keys incl. a second Google API key |
firebase_performance_logcat_enabled | false | |
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.version | 7.1.1 | |
com.bytedance.sdk.pangle.version | 7.6.0.3 | Pangle ad SDK |
com.android.dynamic.apk.fused.modules | base | |
com.android.stamp.source | https://play.google.com/store | |
com.android.stamp.type | STAMP_TYPE_STANDALONE_APK | |
com.android.vending.splits | @xml/splits0 |
On components (36)h6
| Owner | meta-data |
|---|---|
NotificationReaderService | android.service.notification.disabled_filter_types = silent |
CallStateService | android.telecom.IN_CALL_SERVICE_UI=true, android.telecom.INCLUDE_SELF_MANAGED_CALLS=true, android.telecom.IN_CALL_SERVICE_RINGING=false |
FileProvider | android.support.FILE_PROVIDER_PATHS -> @xml/file_paths |
SyncService | android.content.SyncAdapter -> @xml/eyecon_account_sync_adapter; android.provider.CONTACTS_STRUCTURE -> @xml/eyecon_contacts_account |
EyeconAccountAuthService | android.accounts.AccountAuthenticator -> @xml/eyecon_contacts_account_auth |
ComponentDiscoveryService | 20 x com.google.firebase.components:<registrar> |
TransportBackendDiscovery | backend:com.google.android.datatransport.cct.CctBackendFactory = cct |
CredentialProviderMetadataHolder | androidx.credentials.CREDENTIAL_PROVIDER_KEY |
InitializationProvider | 5 x androidx.startup initializers |
FirebaseInstanceIdReceiver | com.google.android.gms.cloudmessaging.FINISHED_AFTER_HANDLED=true |
<property> (foreground-service subtypes)h6
| Component | PROPERTY_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
| Name | protectionLevel | Notes |
|---|---|---|
android.permission.INTERACT_ACROSS_USERS | signature | Re-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_MESSAGE | signature | Used by PushService’s FCM intent filter. Correctly signature-guarded. |
com.eyecon.global.DYNAMIC_RECEIVER_NOT_EXPORTED_PERMISSION | signature | Standard 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.
| Category | Packages | Consumer |
|---|---|---|
| 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.bbm | p516r5.EnumC19861h0 (social-action enum; ctor line 108 calls m334C1("com.whatsapp") / ("com.whatsapp.w4b")); p511r0.C19813a; p034b7.C3502i; p516r5.C19867k0 |
| Social networks | com.instagram.android, com.facebook.katana, com.linkedin.android, com.vkontakte.android | same enum; social-profile scraping in C3502i / C19867k0 |
| Competitor caller-ID apps | com.webascender.callerid (Truecaller), com.truecaller, com.callapp.contacts, mobi.drupe.app (Druper), com.contapps.android, com.bsb.hike, com.sgiggle.production | RegistrationActivity.java:2872; migration/offers |
| Browsers / maps | com.google.android.browser, com.waze, com.google.android.apps.maps, com.google.android.apps.mapslite, com.sygic.aura | default-dialer / map hand-off |
| Telecom | com.android.server.telecom | CallScreeningService / default-dialer role checks |
| OEM security / autostart managers | com.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.g3 | p720zq.AbstractC22312g - 21 setClassName(...) calls to OEM autostart/permission screens (lines 162-210, 1274-1322) |
| Emulators | com.google.android.launcher.layouts.genymotion, com.bluestacks, com.bignox.app | Facebook SDK (p284f7.C16489q, lines 60-62) - not app code. Same class holds IMEI/serial blacklist arrays and goldfish/genyd checks |
| MIUI / iQOO deep-link targets | intents miui.intent.action.APP_PERM_EDITOR, com.iqoo.secure.PERMISSION_MANAGER, com.iqoo.secure.BGSTARTUPMANAGER | AbstractC22312g |
<intent> queries (26)h6
| Query | Consumer / purpose |
|---|---|
SEND + message/rfc822; SENDTO + mailto | Compose email (AbstractC0062l0.m336E1) |
INSERT + content scheme | Contacts insertion |
INSERT + vnd.android.cursor.dir/event | Calendar event creation |
VIEW + geo | Navigation hand-off |
VIEW + sms; DIAL + tel: | Messaging / dialer hand-off |
telecom.action.CHANGE_DEFAULT_DIALER | Default-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 + market | Play Store rating |
androidx.browser.customtabs.CustomTabsService, android.support.customtabs.action.CustomTabsService | Custom Tabs |
com.android.vending.billing.InAppBillingService.BIND; ...play.billingtestcompanion.BillingOverrideService.BIND | Play Billing / licence testing |
com.singular.preinstall.READ_PERMISSION_SINGULAR | Singular pre-install attribution |
com.digitalturbine.ignite.cl.IgniteRemoteService | Digital 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 byandroid:usesCleartextTraffic="true"on<application>. Nothing is scoped down to a single legacy host. - No certificate pinning anywhere.
debug-overridestrusts 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-configfor127.0.0.1is 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
| Permission | Level | Consumer (file | Data actually touched |
|---|---|---|---|
READ_SMS | dangerous | MainActivity:500, MainActivity:2499, SmsJobService:59, SmsFragment:117/135/180, RegistrationActivity:2002 | SMS inbox for the in-app SMS window |
RECEIVE_SMS | dangerous | SmsFragment:137 | - |
SEND_SMS | dangerous | RegistrationActivity:816/2002, PhoneNumberValidationActivity:523/848, MainActivity:1671/2495 | sends the verification SMS |
READ_CONTACTS / WRITE_CONTACTS | dangerous | DBContacts:2033; AccountWithDataSet:37 (ContactsContract.RawContacts.CONTENT_URI); SyncAdapter writes into com.android.contacts | full contact graph read + write |
READ_CALL_LOG | dangerous | DBContacts:280/1964/2033, EyeconAnalyticsWorker:250, RegistrationActivity:3923, PhoneNumberValidationActivity:1242 | call history |
WRITE_CALL_LOG | dangerous | RegistrationActivity:3923, PhoneNumberValidationActivity:1242 | writes call-log entries |
READ_PHONE_STATE / READ_PHONE_NUMBERS | dangerous | fingerprint builder p620w6.C21280h | IMEI, phone number, SIM count |
GET_ACCOUNTS | dangerous (deprecated) | - (no direct reference found) | declared, apparently unused |
AUTHENTICATE_ACCOUNTS maxSdkVersion=22 | deprecated | - | inert on minSdk 23 |
CALL_PHONE | dangerous | dialer actions (C3502i, C19867k0) | places calls |
ANSWER_PHONE_CALLS | dangerous | CallStateService / CallActivity | answers incoming calls |
PROCESS_OUTGOING_CALLS | deprecated | MyPhoneReceiver NEW_OUTGOING_CALL | monitors outgoing dialling |
MANAGE_OWN_CALLS (+ uses-permission-sdk-23) | normal | CallStateService, EyeconCallScreeningService | effective only when the app is the default dialer |
RECORD_AUDIO | dangerous | RecordingsFragment:180/205/347/450 (user-facing recordings list); p599v7.RunnableC20908f:508 -> new AudioRecord(1 /* VOICE_CALL */, 8000, 16, 2, 16000) + AmrEncoder | Talk-To-Me records the call audio stream, AMR-encodes it and uploads to voice2text.eyecon-app.com. Companion playback loop in p633wl.RunnableC21441d |
CAMERA | dangerous | PhotoPickerActivity:427/1008, CallActivity:2684/2701 | contact-photo capture |
ACCESS_FINE_LOCATION / ACCESS_COARSE_LOCATION | dangerous | no LocationManager / FusedLocationProvider reference found in com.eyecon | likely ad-SDK only (InMobi / Pangle / Fyber location targeting) |
POST_NOTIFICATIONS | runtime | notification channels caller_id_status, caller_id_v2 (CallService:134/153) | |
ACTIVITY_RECOGNITION | dangerous | no reference found in com.eyecon | ad-SDK |
READ_APP_BADGE + 17 vendor badge permissions | vendor | me.leolin.shortcutbadger.impl.* | launcher badge counts |
WRITE_EXTERNAL_STORAGE / READ_EXTERNAL_STORAGE | deprecated | RecordingsFragment:185/206/437 | see 3.4 / M-05 |
BIND_NOTIFICATION_LISTENER_SERVICE (service guard) | signature | NotificationReaderService | notification content of all apps |
SYSTEM_ALERT_WINDOW | appop | AbstractC22312g:612 -> android.settings.action.MANAGE_OVERLAY_PERMISSION | ”draw over notifications” caller-ID popup |
DISABLE_KEYGUARD | deprecated/normal | AbstractC0046d0.m191J (KeyguardManager.requestDismissKeyguard) |
Headline privacy items
NotificationListenerServicescrapes WhatsApp/Telegram notification title (= phone number) and text (= message body) -p252ds/RunnableC16004e.java:442-518. Gated by user-granted Notification Access plus thereadNotificationTelegramEnableremote flag /IS_TELEGRAM_WINDOW_ENABLE_BY_USERpref.MessageAssistantBroadcastReceiverforwards full incoming SMS bodies toMessageAssistantReplyWorker, which requires network connectivity.AudioRecord(VOICE_CALL, ...)records live call audio for Talk-To-Me and uploads it tovoice2text.eyecon-app.com.- 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
| Component | What an external app can do |
|---|---|
MainScreen.MainActivity | 20 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>625>), parsed for tiktok.com / facebook.com / instagram.com URLs. |
Others.Activities.DummyActivity | eyecon_request:// (host must be caller_id_role for the deeplink path) or an explicit intent with any action - see M-04. |
MoreMenuAndSettings.SettingActivity | eyecon_settings:// |
Registration.PhoneNumberValidationActivity | eyecon_number_validation://; onNewIntent reads state / phoneNumber / smscode from any sender - see M-02 |
Billing.Store.StoreActivity | eyecon_store://?source=...[&product_id=..&item_id=..] -> JS bridge - see H-01 |
Billing.Premium.FreePremiumUserActivity | EYECON_PREMIUM_UPDATE |
Contacts.NewContactActivity | Contacts-app INSERT / INSERT_OR_EDIT intents |
com.facebook.CustomTabActivity | fb1716660955257637:// and fbconnect://cct.com.eyecon.global - the only legitimate purpose is the FB OAuth redirect |
Others.Receivers.MyPhoneReceiver | Unprotected custom actions -> H-02 |
Analytics.RemoteConfig.RemoteConfigDebugTestReceiver | Broadcast eyecon_remote_config_test - no-op body |
Registration.SMSRetrieverBroadcast | Broadcast either SMS_RETRIEVED action - forwards the Status extras onward |
Registration.InstallReferrerReceiver | Guarded by sender-side INSTALL_PACKAGES -> Play Store only |
Sms.MySmsReceiver, ...assistant.MessageAssistantBroadcastReceiver | Guarded by BROADCAST_SMS -> system only |
tech.crackle.*ShowFullScreen*, tech.crackle.core_sdk.ads.CrackleFullScreenActivity, com.amazon.aps.ads.activity.ApsInterstitialActivity, com.amazon.device.ads.DTBInterstitialActivity | exported="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.FacebookContentProvider | exported="true", no permission -> any app can query() its installreferrer / bridge tables |
| Exported services with guards | NotificationReaderService, CallStateService, EyeconCallScreeningService (BIND_* signature/system), JobsService, SmsJobService, ShareMediaJob, SystemJobService (BIND_JOB_SERVICE), RevocationBoundService (GMS perm), DiagnosticsReceiver / ProfileInstallReceiver (DUMP) - all safe |
PhoneAccount.SyncService / EyeconAccountAuthService | exported="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.
| Site | Filter | Flags | Assessment |
|---|---|---|---|
AfterCall/BaseAfterCallActivity:984 | android.intent.action.SCREEN_OFF | 2 | Protected broadcast (system-only) -> not exploitable |
AfterCall/BaseAfterCallActivity:1000 | android.intent.action.CLOSE_SYSTEM_DIALOGS | 2 | Protected broadcast -> not exploitable |
DefaultDialer/CallActivity:2483 | android.bluetooth.devicepicker.action.DEVICE_SELECTED | 2 | Unprotected. Any app can spoof a “BT device selected” event into the dialer’s device picker (L-04) |
Registration/RegistrationActivity:837-838 | ACTION_SMS_SENT_EYECON, ACTION_SMS_DELIVERED_EYECON | 2 | Unprotected app-defined actions (L-03) |
Registration/PhoneNumberValidationActivity:541-542 | same two | 2 | same |
CallStateService:973, CallStateService:1833, RegistrationActivity:4174, PhoneNumberValidationActivity:1344, TelegramWindowActivity:301, SpamAndBlockFragment:149, SmsFragment:247, BaseAfterCallActivity:556 | various EYECON_*, INTENT_*, EYECON_ACTION_CALL_TO_FLASH_FOUND | 4 (NOT_EXPORTED) | Correct |
Others.MyApplication:508 | overrides 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
| Site | JS | JS bridges | URL source | Origin enforcement |
|---|---|---|---|---|
Billing/Store/StoreActivity:217-226 | on | addJavascriptInterface(new C16132d(), "Eyecon") and addJavascriptInterface(C17757i, "EyeconStore"); setAcceptThirdPartyCookies(true); dom storage on | AbstractC15804d.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-273 | on | "Eyecon" (C16132d) and "Android" (C5838e) | paywall model from backend | yes - 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, viaMyApplication.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*incom.eyecon.globalroutes throughAbstractC0046d0.m185D(flags)(p008a7/AbstractC0046d0.java:70-72), which ORs in a constant on API >= 31. Callers pass134217728(FLAG_UPDATE_CURRENT) or1073741824(FLAG_ONE_SHOT); no caller passesFLAG_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 asAccessibilityEventCompat.TYPE_VIEW_TARGETED_BY_SCROLL(0x40000000) rather thanPendingIntent.FLAG_IMMUTABLE(0x04000000).FLAG_UPDATE_CURRENT | FLAG_ONE_SHOTwith neither mutability flag would throwIllegalArgumentExceptionon 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:156andForegroundLauncherService:43usePendingIntent.getActivity(ctx, 777, new Intent(), ...)as the notificationcontentIntent- tapping does nothing. Harmless but sloppy (L-11). - Intent redirection:
MainActivity.java:836- thehost.equals("browser")branch buildsnew Intent(ACTION_VIEW, Uri.parse(URLDecoder.decode(data2.getQueryParameter("url"), "UTF-8")))and thenintent5.putExtras(bundleM32028w)(the whole incoming extras bundle), thenstartActivity(intent5). No scheme allow-list. See M-03. - Implicit
sendBroadcasthelperAbstractC0062l0.m343L1is package-scoped when the boolean istrue; all call sites examined passtrue.
3.6 Logging of sensitive datah5
- All app logging funnels through
p410lo.C18416a.m34588y(tag, msg, args...), which is gated onC18416a.f46357c, initialisedfalseand set from the remote-config flagprint_logs_in_releaseatp008a7/RunnableC0074r0.java:264andp672y5/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 togetFilesDir()/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.Logimports remain incom.eyecon, and both areLog.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,118dumps every key and value of the install-referrer bundle plus the raw referrer string;Others/Activities/RestartAppActivity.java:28prints the pid. Also severalSystem.out.printlninp005a4/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:
-
Bail-outs: already in
RegistrationActivity, real phone number known, orSP_unAuthorizedUser_isClearedalready true (one-shot latch). -
Sets
SP_unAuthorizedUser_isCleared = true,clear_data_by_eyecon = true. -
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_SOCIALdb.delete("fresh_pics", null, null);db.delete("block_list", null, null);db.delete("recorded_notes", null, null); -
Stops the foreground notification (
EYECON, STOP_FOREGROUND_NOTIFICATION->CallService). -
Logs analytics
("User unauthorized"). -
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"/>plusandroidx.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. MyApplicationinstalls itself as the process-wideUncaughtExceptionHandler(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:272usesCleartextTraffic="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 byp005a4/RunnableC0031c.java:463viaC21273a.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:213readsstore_urlfrom that config;:218-226enables JS, setssetAcceptThirdPartyCookies(true)and attaches theEyeconandEyeconStorebridges.C5846m(the WebViewClient) does not overrideshouldOverrideUrlLoading, so the WebView will navigate anywhere while keeping the bridges.p262e7/C16132d.getStringSP(String, String)->p358j5/AbstractC17503r.m33607k->MyApplication.m10348j()-> the app’s defaultSharedPreferences. Arbitrary key read from any page loaded in that WebView, plusgetAdvertisingId(),getPublicId(),getCid(),getMyName(),reportEvent(), andEyeconStore.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-287shows 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 -
CallServicestart, caller-ID DB writes, reporting calls, and the WhatsApp-call path) - Evidence:
AndroidManifest.xml:391-401-com.eyecon.global.Call.MyPhoneReceiverisandroid:exported="true"with noandroid:permission, priority2147483647, and listens forcom.eyecon.global.action.INTENT_ACTION_START_CALL_WHATSAPPandcom.eyecon.global.action.CALL_SCREENING_NEW_CALL- two unprotected, app-defined actions alongside the protectedPHONE_STATE/NEW_OUTGOING_CALL.com/eyecon/global/Call/MyPhoneReceiver.java:48-53accepts any of the four and forwards the rawBundletoRunnableC17334k case 7.im/RunnableC17334k.java:868-924- action is coerced toPHONE_STATE,state+incoming_numberare read straight from the broadcast extras,incoming_numberis normalised andCallServiceis started.- The identical payload shape is what the trusted
IdPlus/NotificationReader/NotificationReaderService.m10223auses (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) ifstateis omitted whileincoming_numberis 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(wipeshistory,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 setssetInstanceFollowRedirects(true)(C21273a:191). The remote-config fetch athttp://cloudfront-cdn-all.eyecon-app.com/configs/ab-test.txtuses exactly this code path. - Impact: a network MITM who can answer that cleartext request with
HTTP/1.1 401+E-Mark: 1forces 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-1282readsstate,phoneNumberandsmscodefrom any sender and callsm10473L0(phone, code)(:429-459), which (a) fills and locks the four code fields with attacker-chosen digits, (b) setssendSmsEnded = trueso the genuine SMS flow is suppressed, and (c) submits the code to the backend viaC21280h.m38435h(..., EnumC19662h1.SO_SMS, true). - Preconditions: the activity must be on the code-entry step (
ViewFlipper.getDisplayedChild() == 1);singleTaskmeans an externalstartActivityreuses the existing task and routes throughonNewIntent. - Also a crash primitive: a
smscodeshorter than 4 characters (or absent) hitsstr2.charAt(0..3)unguarded -> uncaught NPE /StringIndexOutOfBoundsExceptionon 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 BROWSABLEeyeconscheme 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 forwardedstartActivity(i5);}No scheme allow-list, and the incoming extras are copied verbatim. Classic intent-redirection: an
intent://...#Intent;...;endpayload 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-194switches ongetIntent().getAction()and, for actionEYECON.INTENT_ACTION_SHOW_NOTE, parses the caller-suppliednoteextra as JSON (C18416a.m34586w(string6).m14606i(), line 150) and displays it in the app’s dialog with caller-suppliedcli/name/source;EYECON.INTENT_ACTION_THANK_YOU_DIALOGcallsAbstractC21601b.m38812e(...)(an API call) with caller-suppliedcis/cli/src;EYECON.INTENT_ACTION_SHOW_WARNING_FACEBOOKshows a Facebook warning for a caller-suppliedfid;EYECON.INTENT_ACTION_UNLOCK_KEYGUARD*reachesKeyguardManager.requestDismissKeyguard. - The custom-scheme path itself is tightly restricted (scheme must be
eyecon_request, and onlyhost=caller_id_roledoes anything), butexported="true"means a local app can bypass the filter entirely with an explicit intent. requestDismissKeyguardon 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:152also usesEnvironment.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();, withProcess.setThreadPriority(-19). Streamed AMR frames are POSTed tovoice2text.eyecon-app.com(seeBACKEND-FINDINGS.md).p633wl/RunnableC21441dis 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.MySmsReceiverandDefaultDialer.assistant.MessageAssistantBroadcastReceiver- both guarded byandroid.permission.BROADCAST_SMS(system-only sender) and neither callsabortBroadcast(). No SMS interception.NotificationReaderService,CallStateService,EyeconCallScreeningService,JobsService,SmsJobService,ShareMediaJob,SystemJobService,DiagnosticsReceiver,ProfileInstallReceiver,FirebaseInstanceIdReceiver,RevocationBoundService- all exported but protected bysignature/system permissions. Standard Android patterns, not exploitable.PhoneAccount.SyncService/EyeconAccountAuthServiceexported with no permission - only the systemSyncManager/AccountManagercan bind them. Standard.com.mbridge...NetWorkChangeReceiverexported forCONNECTIVITY_CHANGE- protected broadcast, system-only sender.SCREEN_OFF/CLOSE_SYSTEM_DIALOGSregistered withRECEIVER_EXPORTED- both protected broadcasts.AndroidManifest.xml:224-226- declaringandroid.permission.INTERACT_ACROSS_USERSdoes not let the app use it; platform permissions cannot be redefined by third-party apps. No escalation.debug-overrideswith<certificates src="user"/>- correct Android default, inert in a release build.onReceivedSslErroris not overridden anywhere in app code, so the platform default (cancel) applies; there is nohandler.proceed().android:allowBackup="false"+android:fullBackupContent="false"- adb / cloud / D2D backup disabled.- No
FLAG_MUTABLEPendingIntent, noQUERY_ALL_PACKAGES, no root/emulator detection in app code (the Genymotion/BlueStacks queries come from the Facebook SDK). <queries>does not includeQUERY_ALL_PACKAGES; all package-visibility is declared per-package.
5. Permissions, grouped (78 entries)h4
Dangerous / runtime (personal data)h5
| Permission | maxSdk | Note |
|---|---|---|
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_ACCOUNTS | 22 | inert (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
| Feature | required |
|---|---|
android.hardware.telephony | false |
android.hardware.camera | false |
android.hardware.camera.front | false |
<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,
exportedflag,process,launchMode,taskAffinity, provider authority andmeta-datavalue in sections 1 and 5 - all extracted programmatically fromAndroidManifest.xmlvia 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, theMyApplicationprocess gate, the JS-bridge classesC16132d/C17757i, the 401 /E-Markchain, the DB wipe table list,RemoteConfigDebugTestReceiverbeing a no-op,MySmsReceivernot aborting the broadcast,m343L1package-scoping, theprint_logs_in_releasegating, and theAudioRecord(VOICE_CALL)call.BACKEND-FINDINGS.mdclaims about thee-authscheme, 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.m185Dis the single choke point for everyPendingIntentin app code, and no caller passesFLAG_MUTABLE. But JADX printed the ORed constant asAccessibilityEventCompat.TYPE_VIEW_TARGETED_BY_SCROLL(0x40000000) rather thanPendingIntent.FLAG_IMMUTABLE(0x04000000). The immutability conclusion is inferred:FLAG_UPDATE_CURRENT | FLAG_ONE_SHOTwith neither mutability flag would throwIllegalArgumentExceptionon API 31+, and this is a working targetSdk-35 app. A DEX-level read of the constant pool would confirm it. eyecon://browserreachability.MainActivity’s intent router is heavily decompiled and JADX inverted several conditions aroundMainActivity.java:694-712. Thehost-dispatch chain (showdialog…browser…menifa…url…tel) is unambiguous, and every branch that matters is guarded only byhost.equals(...). I am confident the branch is reachable from aneyecon://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:104renders the"social"table name asNotificationCompat.CATEGORY_SOCIAL;C5846m.f11594d = 4is 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
store_urlreachability 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 remotestore_urlis empty,StoreActivitywould throw onUri.parse("").getHost().replace(...)(StoreActivity.java:215) before the WebView is ever configured. The bridge exposure in H-01 is real iffstore_urlis populated, which the codebase strongly implies (theme/ringtone purchase flows,premium_products_ids_list,EyeconStore.getSelectedItems).E-Marksemantics. The server-side meaning is unknown (seeBACKEND-FINDINGS.mdsection 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.FragmentTransaction.TRANSIT_FRAGMENT_MATCH_ACTIVITY_OPENand similar appear as enum-constant substitutes for integer IDs inEnumC19861h0(e.g.SMS(1048576, ...),CALL(4101, ...)). The values look right but were not cross-checked against the DEX.- 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.
- No dynamic analysis. Nothing here observes execution. No traffic was captured, no intent was actually broadcast, no WebView was loaded. All conclusions are static.
- Which
uses-permissionentries are actually exercised was determined by greppingcom.eyeconfor permission strings. Permissions consumed only inside SDK code (e.g.ACCESS_FINE_LOCATIONfor ad targeting) are marked “no app-code consumer found”, not “unused”. - 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= originala7,p620w6=w6,p284f7=f7,p494q7=q7,p647x6=x6,p420m7=m7,p673y6=y6,p466oq=oq,p315h7=h7.com.facebook.appevents.*andandroidx.fragment.app.RunnableC2598care application code deliberately packed into third-party-looking package names.ir.AbstractC17448bis reallykotlin.jvm.internal.Intrinsics.p420m7.C18533l/p466oq.C19231e0are OkHttp.bm.C3645j1is OkHttpRequest.Builder.
0. Executive summaryh4
| Item | Value |
|---|---|
| Primary API base URL | https://api.eyecon-app.com/app/ (const p008a7.AbstractC0039a.f168L1, hard-coded) |
| HTTP core | p620w6.C21273a (orig. w6.a, TAG NetworkHelper) |
| Server facade | p620w6.C21280h (orig. w6.h) - caches base URL + uploadpic.jsp + join.jsp |
| Queued-request store | SQLite table table_api_requests (p647x6.*), drained by com.eyecon.global.Network.ApiRequests.ApiRequestWorker |
| Auth scheme | 4 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 key | PgdtSBeR0MumR7fO (C21273a.rc_key), remotely overridable via Remote Config key circus_key using reflection |
| Distinct Eyecon API paths | 50 (43 on api.eyecon-app.com/app/, 7 on voice2text.eyecon-app.com) |
| Distinct non-Eyecon app-initiated destinations | 12 |
| Distinct hostnames | 36 |
| Cleartext functional transmissions | 3 (cloudfront-cdn-all.eyecon-app.com config fetch, numberlookup.instabridge.com SMS assistant, join.eyecon-app.com Singular branded domain) |
| Cleartext allowed globally | Yes - 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
- CRITICAL - hard-coded, remotely overridable auth secret.
rc_key = "PgdtSBeR0MumR7fO"ships in the APK and is the compiled-in default of Remote Config keycircus_key.p032b5.AbstractC3464e.m3668s()reads the remote value and reflectively overwrites theprivate static finalfieldC21273a.rc_key. Control of the Remote Config project therefore changes thee-auth-kcredential presented by every installed client, with no app update. - 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}. - HIGH - full call-log exfiltration endpoint.
EyeconAnalyticsWorkerzipsdata.json({cli, fake_cli, call_logs:[{phone, date, duration_in_seconds, type, name}]}) and multipart-POSTs it to the Remote-Config-controlledanalytics_worker_url. Off by default ("disabled_by_remote") but one remote flip away; it also carries afrom_daterewind that can re-upload history. - MEDIUM - device fingerprint including IMEI and Wi-Fi MAC address sent on
join.jsp,cli_auth/gettransport,addcontacts.jspand others (section 7). - MEDIUM - contact names sent to Google Translate (
GET translate.google.com/translate_a/single?...&q=MY NAME IS <name>). - 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 holdscircus_keyand every endpoint URL. - 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).
- LOW/MEDIUM - numerous hard-coded third-party credentials, including a usable PaperQuotes bearer token and a LocationIQ key (section 8).
1. Host inventoryh4
| Host | TLS | Purpose | Source |
|---|---|---|---|
api.eyecon-app.com | https | Primary API (/app/*.jsp, /app/<rest>) + caller-ID photo endpoint /app/pic | AbstractC0039a.f168L1 |
voice2text.eyecon-app.com | https | ”Talk-To-Me” (TTM) / voice-to-text service | p620w6.RunnableC21274b, RunnableC21276d, p572u4.RunnableC20597o, as.RunnableC3316o |
ptt.eyecon-app.com | plain UDP, no TLS | Toki / push-to-talk signalling and media (C3349u2 / C21952k DatagramSocket) | p703z7.C22122v0:419, p633wl.C21450m |
eyecon-app.com | https | Viral invite landing pages https://eyecon-app.com/vrs/evid-<id> | p358j5.C17485b:79, C17487c:64 |
www.eyecon-app.com | https | Marketing site / ToS / privacy | many UI classes |
faq.eyecon-app.com | https | Per-manufacturer troubleshooting WebViews | p008a7.AbstractC0042b0.m181d0 |
apps.eyecon-app.com | https | call-contest/a/report-calls (remote-config value, no call site found) | AbstractC15804d:220 |
cards.eyecon-app.com | https | ”Dynamic area”/cards host (remote config dynamic_area_url; no direct call site found) | AbstractC15804d:122 |
cloudfront-cdn-all.eyecon-app.com | http (cleartext) | Eyecon A/B + remote-config JSON (/configs/ab-test.txt) | AbstractC15804d:300 |
join.eyecon-app.com | http (cleartext) | Singular SDK branded domain / attribution deep-link domain | p671y4.C21845b:72 |
api.paperquotes.com | https | After-call quotes (/apiv1/quotes/) | p284f7.C16476j0 |
us1.locationiq.com / eu1.locationiq.com | https | Reverse geocode of caller area | p379k5.RunnableC17750b:583 |
numberlookup.instabridge.com | http (cleartext) | AI SMS-assistant backend (/respond-sms) | p636wo.C21474f:295, p646x5.InterfaceC21626a |
workers.degoo.com | https | IP -> lat/long feature gate (/location) | p008a7.RunnableC0064m0:112 |
translate.google.com | https | Contact-name normalisation | p344ic.RunnableC17219d1:590 |
www.google.com, google.com, google.co.in | https | Connectivity 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.com | https | Profile/photo scraping, Facebook media download | p344ic, p303g5, p541s5, PhotosTrackerWorker |
www.instagram.com, instagram.com, www.tiktok.com | https | Profile/media scraping, TikTok oEmbed | p599v7.C20917o, p621w7.C21295o:216 |
www.linkedin.com, mobile.twitter.com, new.vk.com | https | Share-sheet text only | UI |
the-news-desk.com, s3.amazonaws.com (embed.footylight.com) | https | Ad-tag <script> injected into WebViews | AbstractC15804d:195-197 |
hb.crackle.co.in | https | Crackle / Mobitech mediation (/user-module/v4/getAppIdAdUnitsMap) | p614w0.AbstractC21232a:370 |
docs.google.com | https | Welcome-back survey form, ToS / privacy | AbstractC15804d:208, UI |
api2.amplitude.com, api.eu.amplitude.com, static.amplitude.com | https | Amplitude SDK | p284f7.C16440x:639 |
firebase-settings.crashlytics.com, firebaseinappmessaging.googleapis.com, firebaseremoteconfigrealtime.googleapis.com, www.googleapis.com, console.firebase.google.com | https | Firebase Crashlytics / FCM / Remote Config | Firebase SDK |
<analytics_worker_url>, <server_report_url>, <store_download_url> | remote-config controlled | Dynamic 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) | Transport | Simplified 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(...) | HttpURLConnection | thin wrapper over m38423g with rid = "" |
m38424h(ArrayList, String url, int[] codeOut, String label, String rid) | HttpURLConnection + GZIPOutputStream | gzip POST |
m38425i(int[] codeOut, boolean addAuth, String body, String dataName, String url, String label, String contentType, String method, String rid) | HttpURLConnection | raw-body POST/GET |
p420m7.C18540p.m34732a / m34736e | HttpURLConnection + BitmapFactory | photo/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.C18533lb.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
m38423gends up in the URL query string, regardless of HTTP method; such POSTs carry an empty body. - A
nullvalue is emitted as a barenamewith no=. - Parameter order equals insertion order into the
ArrayList.
2.3 Headers set by m38423gh5
Observation (C21273a.m38423g, lines 171-198):
| Header | Value | Condition |
|---|---|---|
User-Agent | Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/87.0.4280.88 Safari/537.36 | always |
accept | application/json when jsonAccept == true, else */* | always |
accept-charset | UTF-8 | always |
content-type | application/x-www-form-urlencoded; charset=utf-8 | always, even for GET |
Accept-Encoding | gzip | only when gzip == true |
rid | per-request UUID | only when the rid argument is non-empty (section 2.6) |
e-auth-v, e-auth, e-auth-c, e-auth-k | see section 3 | only when addAutoHeaders == true and the URL starts with https://api.eyecon-app.com/app/ or contains eyecon-app.com |
| caller-supplied | from the headerValues HashMap | when non-null |
HttpURLConnection.setInstanceFollowRedirects(true) is also set here.
2.4 Timeoutsh5
| Primitive | Connect | Read | Write | Call |
|---|---|---|---|---|
m38423g / m38422f | 60 000 ms | 60 000 ms | n/a | n/a |
m38424h (gzip POST) | 60 000 ms (set twice) | 60 000 ms | n/a | n/a |
m38425i (raw body) | 60 000 ms | 60 000 ms | n/a | n/a |
m38417a (OkHttp, C19225b0 builder defaults) | 10 000 ms | 10 000 ms | 10 000 ms | 60 000 ms |
C21280h.m38441n (uploadpic.jsp) | none set (platform default) | none | none | none |
EyeconAnalyticsWorker (OkHttp) | client defaults | client defaults | n/a | n/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-8String.- Gzip is unwrapped only when
gzip == trueand theContent-Encodingresponse header equals"gzip". - All response headers are copied into the caller’s
HashMap headerResultthroughm38418b(first value per name). This is howjoin.jspreadspi,sd,is_new, and howgetupdatedcontactsinfo.jspreadssms. - The photo loader (
C18540p.m34738g) setssetInstanceFollowRedirects(false)and follows 3xx manually: maximum 5 hops, scheme must behttp/https, otherwiseSecurityException("illegal URL redirect"). It special-cases Eyecon-hosted photos redirecting offeyecon-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);| Header | Value | How computed |
|---|---|---|
e-auth-v | constant "e1" (auth protocol version) | literal |
e-auth | the account CID | AbstractC9039h.m10662s() -> SharedPreferences key "clientId", default "" |
e-auth-c | checksum of the CID | C21273a.m38421e(cid) - see algorithm below |
e-auth-k | static key | private 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:
- Host gate:
url.startsWith("https://api.eyecon-app.com/app/") || url.contains("eyecon-app.com"). Consequence: the auth headers are also sent tovoice2text.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 containseyecon-app.com. - Per-call gate:
addAutoHeaders, the 8th positional argument ofm38423g(and theaddAutoHeadersJSON field of a queued request). TheC21653f/C21652e/C21651dconstructors force it totrue. Several call sites deliberately passfalse: LocationIQ, degoo,getfreshpicsjson.jsp, allvirality/*,eyecon_remote_config_url,social_auth/use_cli_validation, andjoin.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 valueField 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-kvalue 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 astatic finalfield 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 byC15803c.m31073a(...). Whethercircus_keyis also settable over cleartext depends on a server-side allow-list that static analysis cannot see.
3.4 Other credential headersh5
| Header | Value | Where |
|---|---|---|
Authorization: Token <quotes_api_id> | Token e2eeb1aa9f32eb07fa04595a0c457ecb6fadb772 | p284f7.C16476j0:102 |
Content-Type: application/json | - | same |
Content-Type: multipart/form-data;boundary=***** | hand-built body, field msisdn | C21280h.m38441n |
Content-Type: application/getStoreItem | store item download | p379k5.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 == 700if (!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:
- bails out if
RegistrationActivity.f12898k1 != null(a join is already in flight) or if!m10665v()(not an Eyecon user); - bails out if the
cidheader equals the staticAbstractC9039h.f13178e; - bails out if SP flag
SP_unAuthorizedUser_isClearedis already true (one-shot); - otherwise sets
SP_unAuthorizedUser_isCleared = trueandclear_data_by_eyecon = true, postsEYECON, STOP_FOREGROUND_NOTIFICATIONto stopCallService, 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.RunnableC17614zcase 1. URL cached inC21280h.f56467c. - Auth headers:
addAutoHeaders = false- the client has no CID yet. - Callback: yes,
p262e7.AbstractC16130b;C21280h.m38437j(...)maps the server result ontop494q7.EnumC19653e1.
| Param | Value source |
|---|---|
cli | p399l7.C18312b.m34437h().m34444e(rawCli) - E.164-normalised CLI |
username | literal "New_User_Eyecon" |
devicename | p008a7.AbstractC0062l0.m357l1() -> "<MANUFACTURER> <MODEL>" (de-duplicated via m347b1) |
user_lang | AbstractC0062l0.m356k1() - device language, iw->he, in->id, ji->yi, default "en" |
os | "Android " + Build.VERSION.RELEASE + " ; " + Build.VERSION.SDK_INT |
public_id | AbstractC9039h.m10625E() - SP SP_UUID (previous public id, may be empty) |
adv_id | p448o4.InterfaceC19056i.f48663a.m35361a() - Google Advertising ID (cached in Eyecon.SP_KEY_ADVID) |
mc | p008a7.AbstractC0042b0.m175X() - uppercase hex MAC of wlan0 |
viral_id | SP SP_KEY_INVITER_ID |
cli_cc | caller-supplied country code of the CLI |
svr | only when re-joining: C19644b1.f50539o.toString() |
transport | only if non-null: mo_sms / so_sms / so_call / so_flash / huawei / fb / social_login |
generated_cli | SP SP_REJOIN_OLD_GENERATED_CLI, only if non-null |
imei | p418m5.C18502p.m34691f() - up to 3 IMEIs across slots, de-duplicated, comma-joined |
os_id | p008a7.AbstractC0042b0.m172U() - Settings.Secure "android_id" |
cv | build stamp |
- Response consumption: headers, not body.
map.get("pi")becomes the newSP_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 top268eh.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.RunnableC17614zcasedefault. Auth:addAutoHeaders = z14(is_already_social_auth), hence false on a first-time validation. Callback: yes,C19647c1.
| Param | Value source |
|---|---|
cli | normalised CLI |
reg_id | Firebase Cloud Messaging registration token |
is_already_social_auth | boolean string |
entire fingerprint set from C21280h.m38430c (section 7) | |
transport | optional EnumC19662h1 name |
last_error | optional 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, anddetails{}withsend_to,timeout,body,code(forMO_SMS) orpattern(forSO_FLASH). - Notable: the
SERVER_BANNED_BY_TRAFFIC_ANALYZERcode 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.RunnableC19875scase 1. Callback: yes,AbstractC16130b.
| Param | Value source |
|---|---|
cli | normalised CLI |
code | code entered by the user |
transport | EnumC19662h1 name |
is_already_social_auth | boolean string |
cv | build stamp |
- Response: plain integer text.
0+ HTTP 200 -> success -> continues into thep308gj.C16770djoin flow. Anything else ->C21280h.m38437j. - Retry: attempted twice - on any
Throwablethe app sleeps 5000 ms (AbstractC0077t.m439X0(5000L)) then retries once (RunnableC19875s:137-139). The same pattern is used bysocial_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.RunnableC21082e0case 23. - Observation: the internal label passed is
"join.jsp"although the path issocial_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, thenAbstractC16257t1.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.RunnableC21093icase 25. Callback: yes (C15802b).
| Param | Value source |
|---|---|
device_id | android_id |
sim_cc | AbstractC0062l0.m369x1() - TelephonyManager.getSimCountryIso() upper-cased |
user_cc | AbstractC0062l0.m371z1() - SP userCountryISO, defaults to the SIM country |
adv_id | Google Advertising ID |
provider | e.g. facebook, google |
token | provider access token |
cv | build stamp |
- Response: JSON that must contain a non-empty
cli; otherwisep284f7.C16479l("validatetoken api return with missing info, json = ..."). Arealkey 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.RunnableC20874acasedefault, fromAboutActivity’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 intotable_api_requests-> executed byApiRequestWorker. - Transport:
C21273a.m38424h. The query string produced byC18416a.m34580q(params)is UTF-8 encoded, GZIP-compressed, and written as the request body, withContent-Type: application/x-www-form-urlencodedandContent-Encoding: gzip. The URL itself carries no query string.
| Param | Value source |
|---|---|
contacts | cli@z@name records; @a@ separates fields, @z@ separates records - the same encoding as removecontacts.jsp (p541s5.RunnableC20065s:92-96) |
cli_cc | TelephonyManager.getSimCountryIso().toLowerCase() |
init | "1" / "0" first-upload flag |
possible_langs | AbstractC0062l0.m356k1().toLowerCase() |
cv | build stamp |
- Response: read then discarded (the
StringBuilderinm38424his never returned) - fire-and-forget at payload level; only the HTTP status feeds the queue bookkeeping. - On success:
DBContacts.m10115r(); ifisCameFromReg,DBContacts.m10143p(false, "updatePostRequest", true, false, null)re-syncs. - Follow-up: may also issue
getcontactslang.jsp. - Caveat:
C21280h.m38428ahas no call site in the decompiled tree (section 11). The payload format is inferred from the matchingremovecontacts.jspbuilder (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.RunnableC20065scasedefault->new C21653f(params, url, "POST", "removecontacts.jsp")-> queued. - Params:
contacts=cli@a@name@z@cli@a@name...built from each contact’scli_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.RunnableC21093icase 26 (privatem38227a), driven fromDBContacts. - Params:
cvonly. - Response header consumed:
sms.C21280h.m38432echecksInteger.parseInt(map.get("sms")) > 0; on a positive value it re-enqueuesRunnableC21276d(..., 5)(fetch new external SMS ids), sets SPsp_new_external_sms_in_server = trueand calls back.NumberFormatExceptionis 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 pairsthen repeated(social_type, social_id)pairs, thenis_sub, thenis_can_talk_enable, thenvn. Values are written into thesocialtable (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 fromDBContacts.m10143p(...)andp541s5.RunnableC3686q0. - Callback: a
Runnable(RunnableC20055i) plus an outputArrayList.
| Param | Value |
|---|---|
init | "1" / "0" |
cv | build stamp |
trigger | caller-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:
RunnableC2598ccase 6 (caller-ID path,jsonAccept = true) andp599v7.RunnableC20908fcase 5 (photo-picker path,source = PhotoPicker). - Callback: yes -
p620w6.InterfaceC21279g.mo34917g(String[] names, C21657d result, int errorCode).
| Param | Value source (caller-ID path) |
|---|---|
cli | C18312b.m34437h().m34444e(cli) (normalised) |
lang | "<contactLang or 'en'>" + "," + "<deviceLang>" when the device language is not already contained; otherwise just contactLang |
is_callerid | "1" |
is_ic | boolean string of the caller (is_ic flag) |
cv | build stamp |
requestApi | "okHttp" or "URLconnection" - chosen by C21280h.f56469e, which is new Random().nextBoolean() evaluated once per process start |
source | caller-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.m38429bmaps the name to""when it is blank /"spam"/"maybe spam". - Client-side caching: before hitting the network the app consults
p648x7.C21670qandp554t5.C20337gcaches; if a cached name exists it is returned witherrorCode = 0and 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.RunnableC3316ocasedefault. Fire-and-forget: result arrayint[]is initialised to{0}and never read; the call is wrapped in atry/catchthat only prints a stack trace. - Guard: only when
notifycliis non-empty andAbstractC9039h.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 SPcontactLang, clearscontactLangNeedUpdate, 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.RunnableC20597ocase 5, viaC21280h.m38431d(...). - Params:
| Param | Value source |
|---|---|
piccli | comma-joined list of CLIs from the caller’s ArrayList |
tag | the photo tag, with &access_token=<FB token> stripped from it (str.replace("&access_token=" + PhotosTrackerWorker.m10433e(), "")) |
isapproved | "yes" / "no" |
cv | build stamp |
- Transport difference (observation): when
isapproved == "yes"the call is executed immediately and synchronously (m38423g, result returned throughstrArr[0]); when"no"it is queued intotable_api_requestsviaC21649b.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.RunnableC21082e0case 22, fromPhotoPickerActivity. - 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 areis_survey_pic(AbstractC0039a.f243o),survey_pic_url(f246p) andsurvey_pic_tag(f249q) - inference fromDBContacts.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 param | Value |
|---|---|
cli | the raw CLI passed by the caller |
is_callerid | literal true |
size | Constants.SMALL = "small" when size == 1, "big" when size == 2, otherwise the literal string "null" |
type | literal 0 |
src | the src argument (feature tag) |
cancelfresh | 0, then &cv=vc_749_vn_4.2026.04.23.1145_a appended |
cv | build stamp |
- Guard:
C18312b.m34437h().m34452q("+" + cli)must be true (a dialable number), else the callback fires with error812and a null bitmap. - Response: raw image bytes ->
BitmapFactory.decodeStream. Additional caching behaviour: the downloaded bitmap is PNG-compressed and stored asphoto_bytes(rejected above 2048 KiB, or down-scaled to at most 1080x1920 first); aredirectand aphoto_foundboolean are stored inp554t5.C20337g. - “Balwan” filter:
C18540p.m34733bparses remote-config keybalwan_recognizerand throwsFileNotFoundException(surfacing as HTTP code-404) when the URL matches a blockedurls[]+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 inC21280h.f56468d; driven bycom.eyecon.global.MoreMenuAndSettings.UploadProfileWorker. - Guards: SP
SP_KEY_NEED_UPDATE_PROFILE_PICmust be true;AbstractC9039h.m10665v(); the file<filesDir>/.Eyecon/.nomedia/temp_photo.jpgmust decode to a bitmap. Returns-1if any guard fails. - Headers:
Content-Type: multipart/form-data;boundary=*****; the foure-auth*headers viaC21273a.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
1is returned;0on 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.RunnableC21275ccases 2/3/4,p607vk.RunnableC21082e0case 24,p516r5.RunnableC19874rcase 12,p475p6.RunnableC19310jcasedefault. - Response: ignored. Only the HTTP status gates a local SP write.
| Variant | Param | Value source | SP write on HTTP 200 |
|---|---|---|---|
adv_id | adv_id | Google Advertising ID, only sent when it differs from SP SP_KEY_SERVER_UPDATED_WITH_ADVERTISING_ID | stores the new adv_id |
mc | mc | Wi-Fi MAC, only when SP is_mac_updated is false | is_mac_updated = true |
| version | cv only | build stamp | appVersion = "vc_749_vn_4.2026.04.23.1145_a" |
| FCM | g_reg_id | the new FCM token, only when it differs from SP SP_FCM_TOKEN_EYECON | stores SP_FCM_TOKEN_EYECON |
| IMEI | imei | C18502p.m34691f(), only when it differs from SP imeis_updated | stores imeis_updated |
| username | username | SP username (the local profile name), only when it differs from SP usernameserver | stores usernameserver |
| queued variant | username + cv | via C21653f -> table_api_requests | none |
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.RunnableC21275ccase 1 - fired fromC21280h’s constructor, i.e. once per process start when both SP values exist. - Params:
token= SPfacebookToken,fbid= SPfacebookId,cv. - Response: ignored. Fully wrapped in
try/catch(stack trace only).
mysettings - read / write the server-side user settings blobh6
Two shapes:
| GET | POST | |
|---|---|---|
| URL | GET https://api.eyecon-app.com/app/mysettings | POST https://api.eyecon-app.com/app/mysettings?type=..&key=..&value=.. |
| gzip | yes (Accept-Encoding: gzip) | no |
| Issuer | p620w6.RunnableC21276d case 8 | p572u4.RunnableC20597o case 7 |
| Params | none | type, key, value (obj.toString()) |
| Callback | yes - HTTP != 200 -> "Error code N", empty -> "Result is empty" | yes - always calls back; error code 1 on exception |
| Response | raw body forwarded to the callback | ignored |
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.RunnableC21082e0case 21, fromcom.eyecon.global.Others.Tasks.DailyWorker. - Params:
list_version= the client’s last-known list version string; nocv. - Callback: yes (
C16937a); HTTP != 200 ->"Error code N", empty ->"Result is empty", otherwise the raw body. - Note: the remote flag
spam_list_sync_intervaldefaults to-1(disabled) and is read in a sibling class.
share - social-media share of a video/posth6
| GET | POST | |
|---|---|---|
| URL | GET https://api.eyecon-app.com/app/share (gzip, addAutoHeaders = true) | POST https://api.eyecon-app.com/app/share with a JSON body |
| Issuer | p598v6.RunnableC20880f case default | p607vk.RunnableC21093i case 24 |
| Guards | Eyecon user, share_video_list_sync_interval != -1 | Eyecon user |
| Request | no params | {cli_list: [phone_number_in_server...], type: <provider>, content: <serialised media>, provider: <provider enum>} |
| Content-Type | - | application/json (via m38425i) |
| Callback | yes (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/spamreportwith a JSON body - Issuer:
p599v7.RunnableC20908fcase 4, throughnew 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-Typeheader is set toapplication/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>(nocv) - Issuer:
p544s8.RunnableC20081fcase 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)wherestris the raw number andC21872a("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.
social-mngr - report a social-media identity link/unlinkh6
- URL / method:
POST https://api.eyecon-app.com/app/social-mngrwith anapplication/jsonbody - Issuer:
p541s5.RunnableC20060ncasedefault->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 typetext/plain - Issuer:
p516r5.RunnableC19874rcase 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(...)))), thenC20369c.m37306n(...). - Guards: SP
SP_KEY_DID_UPDATE_V1453not yet set; Eyecon user; remotesig_report_config.is_enable(defaultfalse);cooldown_days_after_join(default 1) elapsed sincesp_join_time. - Response: ignored. SP flag set immediately after enqueue.
- Related key:
R.string.pk_enc(RSA-2048 public key) is used byp284f7.AbstractC16468f0:362for 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.RunnableC21082e0case 26, built as an OkHttpFormBody. - 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
urlvalue:https://api.eyecon-app.com/app/pub/redirectme.jsp?goto=geo:<lat>,<lon>?q=<lat>,<lon>(built inp308gj.C16770d:1166withURLEncoder.encode(..., "UTF-8")). Observation:pub/redirectme.jspitself is never requested directly - it is only ever embedded intoshorten-url.
upgrade_pic2eyecon.jsp - legacy photo-migration triggerh6
- URL / method:
GET https://api.eyecon-app.com/app/upgrade_pic2eyecon.jspwith no parameters at all (new ArrayList(2)is empty, so not evencv). - Issuer:
p620w6.RunnableC21275ccase 6. - Response: ignored.
resetupdatedstatus.jsp - acknowledge the contact-update pollh6
- Intended URL / method:
GET https://api.eyecon-app.com/app/resetupdatedstatus.jsp?cv=..., issued fromp607vk.RunnableC21082e0case 25, labelled"resetupdatedstatus.jsp". - Observation / probable bug: the URL passed to
m38423gisC21280h.f56466b, i.e. the base URLhttps://api.eyecon-app.com/app/with no endpoint path. Either a genuine defect or a stale build; static analysis cannot tell. The success flag iscode == 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()andp474p5.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 withThread.sleep(1000)untilf49586eflips. - 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 identicalcantalksaw.jsp. - Issuer: both via
p451o7.AbstractC19089d.m35398e->AbstractC0062l0.m354i1(new AsyncTaskC19296a(), "", 34, cli, "cantalkreceived.jsp"), andcom.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(seeAbstractC19089d.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.RunnableC21088g0casedefault. - 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"(som38425iprefixes the body withdata=). - 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 asaddAutoHeaders, 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.
| # | Endpoint | Method | Params / body | Issuer | Notes |
|---|---|---|---|---|---|
| 1 | /init-ttm | POST application/json | {mycli, purchases: <JSONArray>, freeTrialCode: <remote "ttm_free_trial_code">} | p620w6.RunnableC21274b case 0 | Body 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 | /ack | POST application/json | the acknowledgement object | p620w6.RunnableC21274b case default | Response logged only; timing measurement is broken (elapsedRealtime() - elapsedRealtime()). |
| 3 | /start-ttm | POST application/json | {mycli, acli, calltag, purchases} | p572u4.RunnableC20597o case 6 | Accepts any 2xx. Response forwarded to the callback. |
| 4 | /no-ttm | POST application/json | {acli, mycli} | as.RunnableC3316o case 9 | Response logged. mycli = SP authenticatedCli. |
| 5 | /getText | GET | mycli = SP authenticatedCli | p620w6.RunnableC21276d case 1 (checkMessagesTtm) | Accepts any 2xx and a non-empty body; raw text handed to C15802b. |
| 6 | /ttm-get-tokens-amount | GET, jsonAccept = true | mycli | p620w6.RunnableC21276d case 7 | Response parsed as JSON into C3502i. |
| 7 | /ttm-call-forwarding-result | GET | mycli | p620w6.RunnableC21276d case 3 | Response parsed as JSON; .m14606i() (int) extracted. |
4.7 Virality / referralh5
| Endpoint | Method | Params | Issuer | Response |
|---|---|---|---|---|
virality/viral_id | GET | none | p620w6.RunnableC21276d case 9 | raw string; the app builds https://eyecon-app.com/vrs/evid-<id> and stores SP SP_KEY_MY_VIRAL_ID |
virality/viral_id_check | GET | none | p620w6.RunnableC21276d case 4 | raw string forwarded to the callback |
virality/premium_status | GET | none | p620w6.RunnableC21276d case 2, from InstallReferrerReceiver | a bare long = premium expiry epoch seconds; multiplied by 1000 client-side |
4.8 Messages / pushh5
| Endpoint | Method | Body / params | Issuer | Notes |
|---|---|---|---|---|
messages/get | GET, gzip | none | p620w6.RunnableC21276d case 5 | Requires the sms response header from getupdatedcontactsinfo.jsp to be > 0; raw body forwarded |
messages/received | POST application/json | JSONArray of external SMS message ids | p620w6.RunnableC21278f case default -> C21652e -> queued | SP 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 keytoki_ports=[5620, 50000-50010], fallback5620(p720zq.AbstractC22312g.m39737C). The server address can be overridden per-intent byINTENT_KEY_SERVER_ADDRESS. - Issuer:
p703z7.C22122v0->p633wl.C21450m.m38608b(host, port)->p340i7.RunnableC17180l0case 6 ->as.C3349u2(InetAddress, port, session). - Control messages are JSON, built by
C21450m.m38606c(cli, cid, checksum)and always carrying:
| Field | Value |
|---|---|
cli | C18312b.m34437h().m34444e(authenticatedCli) |
vn | "4.2026.04.23.1145" |
vc | 749 |
e-auth-v | "e1" |
e-auth | the CID |
e-auth-c | the 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
authenticatedCliis null the app callsAbstractC9039h.m10641U("", "Toki - connect canceled not eyecon user")- the same destructive 401 handler.
4.10 Non-Eyecon destinations initiated by the apph5
| Endpoint | Method | Params / body | Issuer | Notes |
|---|---|---|---|---|
http://numberlookup.instabridge.com/respond-sms | POST (Retrofit + Moshi) | {contactName, number, messages:[{role: "User"|"System", message}], locale, messagesInSession, userName} | p636wo.C21474f, interface p646x5.InterfaceC21626a | CLEARTEXT 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=1 | GET | lang (iw->he), order=-likes, limit=<StatisticData.ERROR_CODE_NOT_FOUND>, maxlength (96, or 117 for hi), curated=1; only on the first page | p284f7.C16476j0.m32047b | Authorization: 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.php | POST | q (query/phone), format=json, limit=1, countrycodes=<user country lower-case>, key=pk.f8b1a66cf0f04c66264e7e72a00fac7f | p379k5.RunnableC17750b case 24 | Response keys lat, lon. HTTP 429 logs “exceeded the per-X rate-limits”. addAutoHeaders = false - no Eyecon auth headers. |
https://workers.degoo.com/location | GET | none | p008a7.RunnableC0064m0 case 0 | Response 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=... | GET | q = "MY NAME IS " + <contact name> (prefix only when the target language is en) | p344ic.RunnableC17219d1 case 24 | Response: 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.m34065a | store_download_url defaults to "" (disabled). Non-200 -> StoreException. Response is a binary file. |
| Instagram / Facebook / TikTok profile & media | GET | see p599v7.C20917o.m38009h, p303g5.RunnableC3316o, p541s5.RunnableC20056j, p607vk.RunnableC21093i case default | Photo picker / video sharing | Full 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:216 | Used to enrich shared TikTok posts. |
| Google reverse-image search | WebView | search_url = https://www.google.com/search?hl=en&udm=2&q=<name>+<phone>, max_links=75, JS injected via remote google_photos_link_loader | p420m7.C18533l | Up 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:72 | Registered 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
| Endpoint | Method | Payload | Issuer | Guard |
|---|---|---|---|---|
eventreport.jsp | POST application/x-www-form-urlencoded (data=<json>) | {name, type, attributes, timestamp} | C21280h.m38436i | always (headers only if authenticated) |
<analytics_worker_url> | POST multipart/form-data | ZIP 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_stream | com.eyecon.global.Analytics.EyeconAnalyticsWorker.m9989c | remote 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 body | form field data = <JSON> | p620w6.RunnableC21278f case 0 (sendCallReport) | remote server_report_url defaults to "" |
eyecon_remote_config_url | GET | none | p005a4.RunnableC0031c case 17 | none; cleartext default URL |
EyeconAnalyticsWorker details:
- Scheduled as unique periodic WorkManager work
"EyeconAnalyticsWorker"with intervalanalytics_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 theidchanges the window start is replaced with the earlier of the stored time and the parsedfrom_date- i.e. the server can instruct the client to re-upload historical call logs. - Because
cliandfake_cliare 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_jsonper record:phone,date(UTCyyyy-MM-dd HH:mm:ss),duration_in_seconds,type(C19856f.m36609d), and optionallyname.- Result handling:
1= success,-1= failure (->Result.retry()),0= nothing to do (->success()).
5.2 Embedded analytics / attribution SDKs (vendor traffic)h5
| SDK | Evidence |
|---|---|
| Firebase Analytics / Crashlytics / Performance / Remote Config / FCM / In-App Messaging | MyApplication, FirebasePerfUrlConnection.instrument(...) on every HttpURLConnection, firebase-settings.crashlytics.com, firebaseremoteconfigrealtime.googleapis.com |
| Amplitude | api2.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 |
| Singular | app key + secret in p671y4.C21845b:72; singular_events_json frequency caps |
| Adjust | R.string.adjust_app_token = m23t9uwrtvy8 |
| Meta (Facebook) App Events | com.facebook.appevents.* |
| AppLovin, Google Ads/AdMob, InMobi, Mintegral, Vungle, Pangle/ByteDance, Fyber FairBid, Inneractive, AdColony, Digital Turbine, Crackle/Mobitech, Amazon Ads | vendor 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
| Code | Meaning in the app | Where |
|---|---|---|
200 | success everywhere; body read | all helpers |
2xx (range) | treated as success by the TTM and virality paths (code >= 200 && code < 300) | p620w6.RunnableC21276d cases 1/3/7 |
-10 | synthetic “no error” marker used as the initial value of several int[] holders; mapped to CLIENT_RETURN_WITH_NO_ERROR | C21273a.m38423g, C21280h.m38437j |
-1 | generic failure marker passed through HttpURLConnection exception paths | C21651d/52e/53f catch blocks |
-404 | synthetic “photo blocked / not found” | p420m7.C18540p.m34732a |
-2, 0, 1, 2, -404, 812 | callback-specific error codes (2 = “wrong number / no CLI”, 812 = “not a dialable number”, 0 = cache hit) | InterfaceC21279g implementors |
401 | see section 3.5 | C21273a.m38420d |
700 | synthetic code returned when a 401 came from a non-Eyecon host or without an E-Mark header (androidx ... TypedValues.TransitionType.TYPE_DURATION) | C21273a.m38420d |
404 | photo loader caches the URL as “not found” (photo_found = false); getnames.jsp invalidates the cached name | C18540p, RunnableC2598c |
6.2 Retry / persistence of failed requestsh5
table_api_requests (SQLite) is the durable outbox. Flow:
C21649b.m38854c(request)->RunnableC21301bcase 6 inserts a row(url, json, server_retry_count, uuid)and immediately enqueuesRunnableC21275c(12)on theApiRequestDBexecutor.RunnableC21275ccase 12 (ApiRequestWorker.doWork) reads all rows (SELECT * FROM table_api_requests) and rehydrates each into the same request class viaC21649b.m38853b(...).- 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.
- If at least one row was retried, re-enqueue
ApiRequestWorkerwithsetInitialDelay(apiWorkRepeatIntervalMinutes)(remote default 60 min),BackoffPolicy.LINEAR1 hour,NetworkType.CONNECTED. addcontacts.jspgets a special side effect on success.
Additional ad-hoc retries:
cli_auth/checkcodeandsocial_auth/validatetoken: exactly one retry after a 5 000 ms sleep onThrowable.PaperQuotes: none.- OkHttp (
m38417a): defaultretryOnConnectionFailure = true. getnames.jsp: no retry, but a local cache + HTTP 404 invalidation.
6.3 Exception handlingh5
- All
HttpURLConnectionhelpers catchIOException(andOutOfMemoryErrorin thex6request wrappers) and return-1. C21273a.m38423greturns""on any non-200;m38422f/m38424h/m38425ipropagate the code throughint[].- Unhandled exceptions inside lambdas are routed to
com.facebook.appevents.AbstractC9039h.m10631K(Throwable)->m10630J-> in the main process,RunnableC0031cappends the stack trace to SPexceptions_history(capped at 60 000 chars) and prefixesVn: 4.2026.04.23.1145; in other processes it is forwarded toMainProcessReceiverasEYECON_ACTION_EXCEPTION. - Firebase Performance is auto-instrumented on every connection (
FirebasePerfUrlConnection.instrument(...)), so network timings are reported to Google.
6.4 Redirect behaviourh5
| Path | Behaviour |
|---|---|
m38423g (query-string API) | setInstanceFollowRedirects(true) - the platform follows redirects automatically, including cross-scheme |
m38425i, m38424h | platform default (true for HttpURLConnection) |
m38417a (OkHttp) | OkHttp default: follow, up to 20, but not across http <-> https |
C18540p photo loader | manual, max 5 hops, http/https only, else SecurityException("illegal URL redirect"); Eyecon-hosted photos redirecting off-domain are recorded and blocked from caching |
uploadpic.jsp | setUseCaches(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
| Param | Value | Source (observation) |
|---|---|---|
cv | "vc_749_vn_4.2026.04.23.1145_a" | constant |
installer_name | SP Eyecon-InstallerName, else the installer package’s label; "Google Play" normalised; else "manually or unknown source" | p008a7.AbstractC0042b0.m174W() |
n_sims | number of SIM subscriptions found | p418m5.C18502p.m34694i().size() (TelemetryManager + SubscriptionManager) |
time | System.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) |
mc | Wi-Fi MAC address, uppercase hex, :-separated, from NetworkInterface "wlan0" | AbstractC0042b0.m175X() |
device_id | Settings.Secure "android_id" | AbstractC0042b0.m172U() |
adv_id | Google Advertising ID | p448o4.InterfaceC19056i.f48663a.m35361a() (cached in Eyecon.SP_KEY_ADVID) |
imei | up to 3 IMEIs (getDeviceId, getDeviceIdGemini, getSimSerialNumberGemini, getDeviceIdDs), de-duplicated, comma-joined | p418m5.C18502p.m34691f() via reflection (AbstractC0085x.m463n0) |
client_cc | Locale.getDefault().getCountry() | direct |
time_zone | TimeZone.getDefault().getID() | direct |
device_manu | Build.MANUFACTURER (only when non-empty) | direct |
device_model | Build.MODEL | direct |
Additional identifiers sent on specific endpoints but outside m38430c:
| Param | Endpoint | Source |
|---|---|---|
os | join.jsp, cli_auth/gettransport | "Android " + Build.VERSION.RELEASE + " ; " + Build.VERSION.SDK_INT |
devicename | join.jsp, cli_auth/gettransport | AbstractC0062l0.m357l1() |
public_id | join.jsp | SP SP_UUID |
cli_cc | join.jsp, addcontacts.jsp | caller-supplied / getSimCountryIso().toLowerCase() |
user_lang / possible_langs | join.jsp / addcontacts.jsp | AbstractC0062l0.m356k1() |
sim_cc / user_cc | social_auth/validatetoken | SIM country / SP userCountryISO |
country / language | store getItem | Locale display language/country |
countrycodes | LocationIQ | AbstractC0062l0.m371z1().toLowerCase() |
random | grps/sgn | Math.random() |
7.2 Persistent local identifiersh5
| SP key | Meaning | Sent as |
|---|---|---|
clientId | the CID - the primary account credential | e-auth header |
authenticatedCli | the validated phone number | mycli on all TTM calls; cli on updateSgn/Toki |
SP_UUID | public id / analytics user id | public_id, pi response header, Amplitude/Singular/FB user id |
SP_REJOIN_OLD_GENERATED_CLI | previously server-generated CLI | generated_cli |
SP_KEY_INVITER_ID | referrer | viral_id |
SP_KEY_FAKE_AUTHENTICATED_CLI | pre-validation “fake” number | fake_cli in the analytics ZIP |
SP_KEY_MY_VIRAL_ID | own referral id | used to build eyecon-app.com/vrs/evid-<id> |
SP_UUID / Eyecon.SP_KEY_ADVID | analytics / ads | as above |
8. Secrets, tokens and key constantsh4
8.1 In Java source (compiled into the DEX)h5
| Secret | Value | Where | Role |
|---|---|---|---|
rc_key | PgdtSBeR0MumR7fO | p620w6.C21273a:47 | e-auth-k auth header value; remotely overridable (section 3.3) |
circus_key (remote default) | PgdtSBeR0MumR7fO | p236d5.AbstractC15804d:103 | the remote-config twin of rc_key |
| LocationIQ key | pk.f8b1a66cf0f04c66264e7e72a00fac7f | p379k5.RunnableC17750b:580 | LocationIQ API key, sent as key= |
| Singular app secret | ddddafd404a9a4d918bae58070b136d7 (app key instabridge_ffcbcb0b) | p671y4.C21845b:72 | attribution |
cid/token for social providers | read from SP at runtime | p620w6.RunnableC21275c:104-110 | Facebook token forwarded to addfbtoken.jsp |
pk_enc_2 | 2048-bit RSA public key | res/values/strings.xml, used at p516r5.RunnableC19874r:191 | encrypts the grps/sgn payload |
pk_enc | 2048-bit RSA public key | res/values/strings.xml, used at p284f7.AbstractC16468f0:362 | local APK-signature verification |
cipher_key | "Ewu" | res/values/strings.xml, used at p379k5.RunnableC17750b:401 | Blowfish 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 key | AIzaSyCD1Lmwj_ln-h4RMLHApAZjuPZNWAzJi3M | R.string.google_api_key / google_crash_reporting_api_key | Google services |
| Google app / project ids | 1:820458390093:android:93b50d6644b00155, gcm_defaultSenderId=820458390093, project_id=eyecon-app, default_web_client_id, an extra OAuth client id 820458390093-5bi3hdnbtrj6lugkjm0t1ot6sftk7cgq.apps.googleusercontent.com | res/values/strings.xml, p614w0.AbstractC21232a:344 | Firebase / Sign-In |
| Facebook app id / client token | 1716660955257637 / afa3f38c55f24c73b177afc962969b15 | res/values/strings.xml | Facebook Login |
| Adjust token | m23t9uwrtvy8 | R.string.adjust_app_token | attribution |
| AppVestor | 980d4e3f-d057-4e5b-9034-1587edadf5a4 / 8c830f45-9f86-48d3-bcca-4e14fe2c1129 | R.string.app_vestor_* | attribution |
| AppLovin SDK key | UZvHsQBqsDCf0qv_yVDKn0UeTmRknnQSgTT3HP7HFikqs8IxdD-7uWdloUQVGTn09CQ0LThxLm6tfMnKDFS9xa | R.string.applovin_key | mediation |
| Crackle/Mobitech key blob | fai=1:917722272265:android:af67d7515574ed8c689ed8~~~fak=AIzaSyBxXQfYa0khnAZ2-NiK3WQV-4JbMWCw6ls~~~fpi=eyecon---eyecon-caller-id... | R.string.crackle_key | mediation (contains another Firebase key) |
| AdColony app id | app17ccb9995f3f46c682 | R.string.adcolony_app_id | ads |
| Amazon Ads key | 905c0238-13dd-4cf9-af3d-90f64672a4a9 | R.string.amazon_app_key | ads |
| AdMob app id | ca-app-pub-2959190743815944~6092014645 plus ~30 ad-unit ids | R.string.*, AbstractC15804d | ads |
| The Trade Desk UID2 | subscription_id TSqMwCWMkK, public_id UID2-X-P-MFkwEwYHKoZIzj0CAQ... | AbstractC15804d:362 (uid2_config), consumed by p516r5.C19845p:75 | identity |
| Mobitech publisher keys | EC170523APP, EC180623O | AbstractC15804d | ad publisher keys |
8.2 In res/values/strings.xml (secret-bearing string resources)h5
| Key | Value | Used by |
|---|---|---|
quotes_api_id | e2eeb1aa9f32eb07fa04595a0c457ecb6fadb772 | p284f7.C16476j0:102 as Authorization: Token ... against api.paperquotes.com |
cipher_key | Ewu | Blowfish key prefix for local TTM message encryption |
pk_enc, pk_enc_2 | RSA public keys (see above) | grps/sgn and local signature verification |
google_api_key, google_crash_reporting_api_key | AIzaSyCD1Lmwj_ln-h4RMLHApAZjuPZNWAzJi3M | |
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_id | see 8.1 | attribution / ads |
default_web_client_id, google_app_id, gcm_defaultSenderId, project_id | see 8.1 | Firebase |
com.google.firebase.crashlytics.mapping_file_id | 7c6f28717cc9432e872d3e1b88cf99ea | Crashlytics |
8.3 In remote config (p236d5.AbstractC15804d, the compiled-in defaults)h5
| Key | Default | Notes |
|---|---|---|
circus_key | PgdtSBeR0MumR7fO | the auth key |
uid2_config | TTD subscription + public id | identity token |
analytics_worker_url | "disabled_by_remote" | call-log exfiltration endpoint |
server_report_url | "" | call-report endpoint |
eyecon_remote_config_url | http://cloudfront-cdn-all.eyecon-app.com/configs/ab-test.txt | cleartext config channel that also carries circus_key |
store_download_url / store_url | "" | store endpoints |
dynamic_area_url | cards.eyecon-app.com | cards host (no call site found) |
call_contest_report_calls_url | https://apps.eyecon-app.com/call-contest/a/report-calls | (no call site found) |
ac_ads_test_internet_connection_url | https://www.google.com | connectivity probe |
user_agent | Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 ... Chrome/123.0.0.0 Safari/537.36 | WebView UA for Google image search |
balwan_recognizer | JSON blocklist of fbcdn.net URL fragments | photo-cache poisoning defence |
google_photos_link_loader | JSON with the Google search URL + injected JS | photo search |
mobitech_* | publisher keys and injected <script> HTML | ads |
toki_ports | [5620,50000-50010] | Toki UDP ports |
welcome_back_notification_url | Google Form URL | survey |
9. Cleartext (non-TLS) transmission - consolidatedh4
| # | Destination | What is sent | Severity |
|---|---|---|---|
| 1 | http://cloudfront-cdn-all.eyecon-app.com/configs/ab-test.txt | nothing (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 key | HIGH |
| 2 | http://numberlookup.instabridge.com/respond-sms | contact name, phone number, full SMS conversation, locale, username | HIGH |
| 3 | http://join.eyecon-app.com/ | configured as the Singular branded domain (deep-link / attribution) | MEDIUM |
| 4 | ptt.eyecon-app.com over plain UDP | Toki control JSON (incl. e-auth, e-auth-c, CLI) and raw voice media | MEDIUM |
| 5 | Global cleartext permission | android:usesCleartextTraffic="true" + <base-config cleartextTrafficPermitted="true">, and <debug-overrides> trusts user-installed CAs | HIGH (enables interception of any endpoint if a future/remote value uses http) |
10. Quick endpoint indexh4
| # | Method | URL | Callback | Auth | Issue |
|---|---|---|---|---|---|
| 1 | GET | /app/join.jsp | yes | no (no CID yet) | p365jd.RunnableC17614z |
| 2 | GET | /app/cli_auth/gettransport | yes | conditional | p365jd.RunnableC17614z |
| 3 | GET | /app/cli_auth/checkcode | yes | conditional | p516r5.RunnableC19875s |
| 4 | GET | /app/social_auth/use_cli_validation | yes | yes | p607vk.RunnableC21082e0 |
| 5 | GET | /app/social_auth/validatetoken | yes | yes | p607vk.RunnableC21093i |
| 6 | GET | /app/deleteme.jsp | yes | yes | p598v6.RunnableC20874a |
| 7 | POST (gzip body) | /app/addcontacts.jsp | queue | yes | p620w6.C21280h |
| 8 | POST | /app/removecontacts.jsp | queue | yes | p541s5.RunnableC20065s |
| 9 | GET | /app/getupdatedcontactsinfo.jsp | yes | yes | p607vk.RunnableC21093i |
| 10 | GET | /app/resetupdatedstatus.jsp (URL actually the base path) | yes | yes | p607vk.RunnableC21082e0 |
| 11 | GET | /app/getfreshpicsjson.jsp | yes | yes | androidx.fragment.app.RunnableC2598c |
| 12 | GET | /app/getnames.jsp | yes | yes | RunnableC2598c, p599v7.RunnableC20908f |
| 13 | GET | /app/notifyaddcontact.jsp | no | yes | as.RunnableC3316o |
| 14 | GET | /app/getcontactslang.jsp | no | yes | p620w6.C21280h |
| 15 | GET | /app/picapproval.jsp | yes / queue | yes | p572u4.RunnableC20597o |
| 16 | GET | /app/getoptionalpics.jsp | yes | yes | p607vk.RunnableC21082e0 |
| 17 | GET (image) | /app/pic?cli=... | yes | yes | p620w6.C21280h.m38434g |
| 18 | POST (multipart) | /app/uploadpic.jsp | yes | yes | p620w6.C21280h |
| 19 | GET x6 | /app/updateuserdetails.jsp | no | yes | p620w6.RunnableC21275c et al. |
| 20 | GET | /app/addfbtoken.jsp | no | yes | p620w6.RunnableC21275c |
| 21 | GET (gzip) | /app/mysettings | yes | yes | p620w6.RunnableC21276d |
| 22 | POST | /app/mysettings | yes | yes | p572u4.RunnableC20597o |
| 23 | GET (gzip) | /app/spam | yes | yes | p607vk.RunnableC21082e0 |
| 24 | GET (gzip) | /app/share | yes | yes | p598v6.RunnableC20880f |
| 25 | POST (JSON) | /app/share | no | yes | p607vk.RunnableC21093i |
| 26 | POST | /app/spamreport | queue | yes | p599v7.RunnableC20908f |
| 27 | GET | /app/name_report | no | yes | p544s8.RunnableC20081f |
| 28 | POST (JSON) | /app/social-mngr | queue | yes | p541s5.RunnableC20060n |
| 29 | POST (RSA, text/plain) | /app/grps/sgn | queue | yes | p516r5.RunnableC19874r |
| 30 | POST (form) | /app/shorten-url | yes | yes | p607vk.RunnableC21082e0 |
| 31 | GET | /app/upgrade_pic2eyecon.jsp | no | yes | p620w6.RunnableC21275c |
| 32 | GET | /app/cantalk.jsp | poll | yes | p474p5.* |
| 33 | GET | /app/getcantalkstatus.jsp | poll | yes | p474p5.RunnableC19300e |
| 34 | GET | /app/cantalkreceived.jsp | no | yes | p474p5.AsyncTaskC19296a |
| 35 | GET | /app/cantalksaw.jsp | no | yes | CanTalkActivity |
| 36 | GET | /app/sethistory.jsp | no | yes | p607vk.RunnableC21088g0 |
| 37 | POST | /app/eventreport.jsp | no | conditional | p607vk.RunnableC21129z |
| 38 | GET | /app/virality/viral_id | yes | yes | p620w6.RunnableC21276d |
| 39 | GET | /app/virality/viral_id_check | yes | yes | p620w6.RunnableC21276d |
| 40 | GET | /app/virality/premium_status | yes | yes | p620w6.RunnableC21276d |
| 41 | GET (gzip) | /app/messages/get | yes | yes | p620w6.RunnableC21276d |
| 42 | POST (JSON) | /app/messages/received | queue | yes | p620w6.RunnableC21278f |
| 43 | GET (gzip) | /app/get_store_products | yes | yes | p620w6.RunnableC21276d |
| 44 | POST (JSON) | voice2text/init-ttm | yes | yes | p620w6.RunnableC21274b |
| 45 | POST (JSON) | voice2text/ack | no | yes | p620w6.RunnableC21274b |
| 46 | POST (JSON) | voice2text/start-ttm | yes | yes | p572u4.RunnableC20597o |
| 47 | POST (JSON) | voice2text/no-ttm | no | yes | as.RunnableC3316o |
| 48 | GET | voice2text/getText | yes | yes | p620w6.RunnableC21276d |
| 49 | GET | voice2text/ttm-get-tokens-amount | yes | yes | p620w6.RunnableC21276d |
| 50 | GET | voice2text/ttm-call-forwarding-result | yes | yes | p620w6.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
- No server code. Every schema, validation rule, rate limit, and business rule on
api.eyecon-app.com,voice2text.eyecon-app.comandptt.eyecon-app.comis unknown. Nothing here proves what the server checks - only what the client sends. E-Marksemantics. The app treats a 401 +E-Markheader as “this CID has been revoked”. Whether Eyecon also usesE-Markfor softer states (rate limiting, abuse detection, forced re-validation) is unknown. Thecidresponse header on a 401 is read but its role is not documented anywhere in the client.- Remote Config trust model. Whether
circus_key(and thereforee-auth-k) can be changed through the cleartexteyecon_remote_config_urldepends 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. - 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/gettransportclearly definesdetails{}variants, but whether other fields are always present is unknown.
Schemas not recovered
getoptionalpics.jspresponse - the consuming lambda was not decompiled; the JSON keys are unknown (only the DB columns it must populate are known).spam(list_version) response andmysettingsGET response - the bodies are forwarded to callbacks whose parsers were not recovered.messages/getandmessages/receivedpayload schemas - only “a JSONArray of ids” and “a raw array” could be determined.get_store_productsresponse and thegetItembinary format.analytics_worker_urlingestion format beyonddata.json; and whether the server also stores the multipartfilename(the CLI).- 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
addcontacts.jspcaller.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. Thecontactspayload format is therefore inferred from the structurally identicalremovecontacts.jspbuilder (p541s5.RunnableC20065s:92-96) - medium-high confidence, not certain.name_reportargument order.p544s8.RunnableC20081f:52-53sends("name", str)and("cli", buttonText), but at the call sitestris the raw number andbuttonTextis the displayed name. Either the call site is inverted or the server’s parameter names differ from the client’s intent. Unresolved.resetupdatedstatus.jspURL. The app requests the bare base URLhttps://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.dynamic_area_url/cards.eyecon-app.comandcall_contest_report_calls_url/apps.eyecon-app.comare 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 readdynamic_area_url-shaped values indirectly, so the cards host is probably contacted, but the exact endpoint path could not be determined.- OkHttp
callTimeoutexact 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. - Whether the reflective write to
static final rc_keysucceeds at runtime. This is ART-version dependent and cannot be confirmed statically. - 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. - Which of
ServerRetryCount,share_video_list_sync_intervaletc. are country-overridden. The remote-config helper (AbstractC15804d.m31082c) supports country-specific values, but the effective per-country overrides live server-side. - Server-side validation of the
grps/sgnRSA payload,cidissuance rules,join.jspre-join rules (svr,sd=0), and theSERVER_BANNED_BY_TRAFFIC_ANALYZERheuristics. All unknown. - 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
| Claim | Status |
|---|---|
Base URL https://api.eyecon-app.com/app/ and all 43 paths under it | Observation (literals + call sites) |
7 voice2text paths | Observation |
Auth header names, the e-auth-c algorithm, the host gate | Observation |
rc_key remote override via reflection | Observation (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 origins | Observation |
addcontacts.jsp contacts payload format | Inference from removecontacts.jsp (medium-high confidence) |
getoptionalpics.jsp response schema | Unknown |
| Number of retries / retry interval | Observation (ApiRequestServerRetryCount = 3, apiWorkRepeatIntervalMinutes = 60) |
| Endpoint hostnames | Observation, 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 documentgetnames.jsp/getfreshpicsjson.jsp). com.facebook.appevents.*is application code, not the Facebook SDK (it imports both, but the classAbstractC9039his Eyecon’s ownAuthenticator/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
| Item | Count |
|---|---|
| Distinct SQLite database files (all) | 8 |
| App-owned databases | 2 (contactdb + transient eyecon_restore_temp_database) |
| Tables in app-owned DBs | 21 in contactdb |
| Room databases | 1 declared (EyeconRoomDB) but dead code / not wired — see §3.1 |
Room DAOs (@Dao) with real SQL | 0 in app code (1 in Moloco SDK) |
SQLiteOpenHelper subclasses (app) | 1 (p008a7.C0041b) + 1 overload used for restore |
| Schema migrations recovered | 1 → 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 DataStore | 0 (all DataStore usage is Firebase / DataTransport SDK) |
| On-device personal-data classes | contacts, 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 name | Owner | Kind | Version | Location | Tables |
|---|---|---|---|---|---|---|
| 1 | contactdb | MICO app (p008a7.C0041b, original a7.b) | SQLiteOpenHelper (raw SQL) | 57 | /data/data/com.eyecon.global/databases/contactdb | 21 (§2) |
| 2 | eyecon_restore_temp_database | MICO app (backup/restore staging copy of contactdb) | SQLiteOpenHelper (same class, name overridden) | 57 | databases/eyecon_restore_temp_database | same 21 (mirror) |
| 3 | eyecon_temp_db | MICO app (rename target during restore) | plain file copy | n/a | databases/eyecon_temp_db | — |
| 4 | androidx.work.workdb | AndroidX WorkManager | Room (v23, autoMigrations) | 23 | databases/androidx.work.workdb | WorkSpec, WorkName, WorkTag, SystemIdInfo, WorkProgress, Dependency, Preference, room_master_table |
| 5 | com.google.android.datatransport.events | Google DataTransport (FCM) | SQLiteOpenHelper (ec.C16178l, original ec.l) | 7 | databases/com.google.android.datatransport.events | events, event_metadata, event_payloads, transport_contexts, log_event_dropped, global_log_event_state |
| 6 | metrics-db | Moloco SDK | Room (com.moloco.sdk.acm.p221db.MetricsDb) | 1 | databases/metrics-db | events |
| 7 | com.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) |
| 8 | com.im_10.8.7.db | InMobi SDK | SQLiteOpenHelper | 1 | databases/com.im_10.8.7.db | InMobi media cache |
| — | download_record (Mbridge) | Mintegral SDK | SQL constant, helper stripped | — | Mbridge-managed | download_record |
Evidence of the restore DB (app uses a 4-arg constructor of the same helper):
// p282f5/C16434r.java:267C0041b 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 -> revert2. 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
| # | Table | Purpose | Sensitive? |
|---|---|---|---|
| 1 | contacts | Master address book mirror + per-contact flags/scores/photos | YES |
| 2 | history | Call / event history (per contact) | YES |
| 3 | social | Social-network (Facebook) ids linked to contacts | YES |
| 4 | fresh_pics | Server-delivered “fresh photo” records + URLs | Medium |
| 5 | names_photos_cache | Cached contact photo BLOBs | YES |
| 6 | recorded_notes | Voice-note recordings index | YES |
| 7 | sms_table | External SMS messages (body, sender) | YES |
| 8 | ttm | ”Talk To Me” voicemail text-to-speech transcript messages | YES |
| 9 | block_list | User-blocked numbers | YES |
| 10 | spam_list | Spam numbers reported by user | YES |
| 11 | db_table_notification_reader | Captured notification contents (message, sender, photo) | YES |
| 12 | db_table_reverse_lookup_id | Reverse number-lookup results | Medium |
| 13 | table_share_media | Shared-media (audio/video “cards”) playback state | Low |
| 14 | table_api_requests | Persistent outbound HTTP API request queue | Medium |
| 15 | dynamic_area_tickets | Dynamic-Area (“cards”) server ticket state | Low |
| 16 | dynamic_area_subjects | Dynamic-Area subject definitions + cron schedule | Low |
| 17 | statistics_questions | Survey/statistics question log | Low |
| 18 | shared_preference_v2 | Current app SharedPreferences backing store | YES |
| 19 | shared_preference | Legacy app SharedPreferences (Java-serialised HashMap BLOB) | YES |
| 20 | merge_contacts | Contact-merge candidate JSON blobs | Medium |
| 21 | merge_history | Contact-merge history JSON blobs | Medium |
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_idwas declaredINTEGER PRIMARY KEY(the table was then rebuilt at v14-19 back to plainINTEGER). - Declared-but-unused constant:
has_thumbnail_photo(AbstractC0039a.f208c0) is not in the table. ROWID(f192V0) androwid(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 rawcontactdbfile (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 referencesEyeconRoomDB— the class is never instantiated (dead code). - The declared “entity”
p420m7.AbstractC18543s(originalm7.s) is not an entity — it is a bitmap/Exif/ImageIO utility class with no@Entityannotation and no table fields. - The declared “DAO”
p420m7.InterfaceC18541qis an empty interface with no@Daoannotation 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
| Class | Original | Tables touched | Style |
|---|---|---|---|
com.eyecon.global.Contacts.Database.DBContacts | DBContacts | contacts, history, fresh_pics | ContentValues + insert/update/delete/replace on a7.b |
p008a7.C0041b | a7.b | all 21 | DDL, migrations, rawQuery, transactions |
p274eo.C16303b | eo.b | shared_preference_v2, shared_preference | SP persistence engine |
p620w6.RunnableC21275c (ApiRequestDB) | w6.c | table_api_requests | raw SELECT/UPDATE + WorkManager retry |
p575u7.AbstractC20698s (SmsDB) | u7.s | sms_table | query() + content-provider merge |
p575u7.RunnableC20680b (ExternalSmsDB) | u7.b | sms_table | query() + insertWithOnConflict(..., 5 /*CONFLICT_REPLACE*/) |
p648x7.C21670q (SpamDB) | x7.q | spam_list | raw SELECT |
p623w9.RunnableC21301b | w9.b | spam_list | raw SELECT |
p260e5.C16102d | e5.d | recorded_notes | raw SELECT |
p672y5.C21851f | y5.f | dynamic_area_tickets, dynamic_area_subjects | raw SELECT/delete |
p672y5.C21856k | y5.k | dynamic_area_tickets, statistics_questions | raw SELECT |
p284f7.RunnableC16473i (DynamicAreaDataLoader) | f7.i | dynamic_area_tickets | raw SELECT + UPDATE |
p379k5.RunnableC17750b (TtmDatabase) | k5.b | ttm | rawQuery("SELECT * FROM ttm") |
3.3 Complete raw-SQL inventory (app code)h5
| SQL | Location | Returns / effect |
|---|---|---|
PRAGMA table_info(<table>) | a7.b.m147Q | Column-existence probe |
select ROWID as ROWID, cli_in_server, cli, name from contacts ORDER BY cli , has_photo ASC , cli , name COLLATE NOCASE DESC | a7.b.m148S | Re-numbers contact_id from ROWID (migration v36) |
SELECT * FROM table_api_requests | w6.c:270 | Pending API queue -> ArrayList<C21650c> |
UPDATE table_api_requests SET server_retry_count = server_retry_count + 1 WHERE db_column_uuid = ? | w6.c:321/375/430 | int rows affected |
DELETE FROM table_api_requests WHERE db_column_uuid = ? | w6.c:347/401/456 | int rows deleted |
DELETE FROM history | a7.b.onCreate:515 | fresh-install wipe |
UPDATE contacts SET score = call_count + sms_count + whatsapp_count + can_talk_count | a7.b.onUpgrade v5 | migration |
INSERT INTO contacts SELECT * FROM tmp | a7.b.onUpgrade v<=13 / v<=19 | table-rebuild data copy |
UPDATE contacts SET has_photo = 1 WHERE has_photo > 1 | v<16 | migration |
UPDATE contacts SET has_fresh_pic_init = 1 WHERE fresh_pic_init_url IS NOT NULL | v<18 | migration |
UPDATE contacts SET <col> = '' / 0 / -1 WHERE <col> IS NULL (x7 columns) | v<28 | null backfill |
UPDATE contacts SET contact_action_date_mili = last_action_date_long | v<36 | migration |
UPDATE contacts SET fav_pin_pos = 2147483647 WHERE fav_pin_pos = 0 | v<41 | migration |
SELECT 1 FROM shared_preference_v2 LIMIT 1 | eo.b:235 | post-delete sanity check |
SELECT * FROM shared_preference_v2 ORDER BY key, data_part ASC | eo.b:326 | full SP map (HashMap<String,Object>) |
SELECT substr(data, N, 1000000) AS data FROM shared_preference | eo.b:510 | legacy SP HashMap (deserialised) |
query("sms_table", null, "message_id = ?", {id}, ...) | u7.b:64 | Cursor |
query("sms_table", null, null, ..., orderBy "date_ms DESC") | u7.s:298 | ArrayList<C20678a> |
insertWithOnConflict("sms_table", ..., CONFLICT_REPLACE) | u7.b:84 | row id |
select * from spam_list WHERE cli_in_server = '<cli>' | x7.q:26 | C21657d or null |
select * from spam_list order by cli_in_server | w9.b:281 | ArrayList<C21657d> |
SELECT * FROM spam_list WHERE spam_type = 3 ORDER BY cli_in_server | w9.b:342 | Cursor |
select * from recorded_notes order by create_timestamp DESC | e5.d:274 | ArrayList<C16104f> |
SELECT DISTINCT subject FROM dynamic_area_subjects | y5.f:171 | HashSet<String> |
SELECT DISTINCT subject FROM dynamic_area_tickets | y5.f:203 | HashSet<String> |
SELECT * FROM dynamic_area_tickets | y5.f:984 | Cursor |
SELECT * FROM dynamic_area_subjects | y5.f:1077 | Cursor |
SELECT COUNT(*) FROM dynamic_area_tickets LIMIT 1 | y5.k:185 | int |
SELECT rowid, * FROM statistics_questions ORDER BY last_seen [LIMIT n OFFSET m] | y5.k:1469/2304, e6.d:507 | ArrayList<C16445c> |
SELECT <col> FROM dynamic_area_tickets WHERE <col> != -1 AND <col> = '<ticket>' LIMIT 1 | f7.i:149 | Cursor (count) |
UPDATE dynamic_area_tickets SET <col> = abs(random() % 1000000) WHERE <col> = '<ticket>' | f7.i:176 | randomise appearance bucket |
SELECT * FROM ttm | k5.b:435 | ArrayList<C18707f> (voicemail transcripts) |
select 1 | a7.p0:204 | DB 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, data1ContactsContract.Data.CONTENT_URI->photo_uri, photo_thumb_uri; WhatsApp profile mimetypevnd.android.cursor.item/vnd.com.whatsapp.profileContactsContract.RawContacts.CONTENT_URI->account_name, account_typeCallLog.Calls.CONTENT_URI-> full call log (Calls-Contest counters +call_logsbackup 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 -> To | Operation |
|---|---|
| 1 -> 2 | ALTER TABLE contacts ADD COLUMN favorite_timestamp DATETIME DEFAULT NULL |
| 2 -> 3 | ALTER TABLE contacts ADD COLUMN can_talk_status INTEGER DEFAULT 0; ADD can_talk_timestamp DATETIME DEFAULT NULL |
| 3 -> 4 | CREATE TABLE social |
| 4 -> 5 | ALTER TABLE contacts ADD COLUMN contact_photo_uri TEXT DEFAULT NULL |
| 5 -> 6 | ALTER TABLE contacts ADD COLUMN score INTEGER DEFAULT 0; UPDATE contacts SET score = call_count + sms_count + whatsapp_count + can_talk_count |
| 6 -> 7 | DELETE FROM social; set SP SP_REQUIRED_SERVER_CONTACTS_REFRESH = true |
| 7 -> 8 | CREATE TABLE fresh_pics; ALTER TABLE contacts ADD COLUMN is_hash_updated INTEGER DEFAULT 0 |
| 8 -> 9 | ALTER TABLE social ADD COLUMN social_source INTEGER DEFAULT 0 |
| 9 -> 10 | ALTER TABLE contacts ADD COLUMN fresh_pic_init_url TEXT DEFAULT NULL; ADD fresh_pic_init_tag TEXT DEFAULT NULL |
| <=13 -> 14 | Rebuild contacts: RENAME TO tmp -> recreate with ab_raw_id INTEGER PRIMARY KEY -> INSERT INTO contacts SELECT * FROM tmp -> DROP TABLE tmp |
| <16 -> 15 | ALTER TABLE contacts ADD COLUMN default_sim_id TEXT DEFAULT ''; UPDATE contacts SET has_photo = 1 WHERE has_photo > 1 |
| 16 -> 17 | ALTER TABLE contacts ADD COLUMN pinned_position INTEGER DEFAULT 0 |
| 17 -> 18 | ALTER TABLE contacts ADD COLUMN has_fresh_pic_init INTEGER DEFAULT 0; UPDATE ... SET = 1 WHERE fresh_pic_init_url IS NOT NULL |
| <=19 -> 18 | Rebuild contacts again (same 32-column shape, PK removed) |
| 19 -> 20 | ALTER TABLE contacts ADD COLUMN custom_phone_number_label TEXT NOT NULL DEFAULT '' |
| 20 -> 21 | CREATE TABLE block_list |
| 21 -> 22 | ALTER 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 -> 28 | CREATE TABLE shared_preference ( data BLOB DEFAULT NULL ) + 7 UPDATE ... WHERE col IS NULL backfills |
| 28 -> 29 | ALTER TABLE history ADD COLUMN account_id TEXT NOT NULL DEFAULT '' |
| 29 -> 30 | CREATE TABLE recorded_notes |
| 30 -> 31 | ALTER TABLE recorded_notes ADD COLUMN is_saved INTEGER NOT NULL DEFAULT 0 |
| <32 -> 31 | ALTER 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 -> 37 | CREATE TABLE merge_history, CREATE TABLE merge_contacts |
| 37 -> 38 | ALTER TABLE history ADD COLUMN participants TEXT NOT NULL DEFAULT '' |
| 38 -> 39 | ALTER TABLE contacts ADD COLUMN vn INTEGER NOT NULL DEFAULT 0 |
| 39 -> 40 | CREATE TABLE names_photos_cache; + fav_pin_pos INTEGER NOT NULL DEFAULT 2147483647 |
| 40 -> 41 | UPDATE contacts SET fav_pin_pos = 2147483647 WHERE fav_pin_pos = 0 |
| 41 -> 42 | ALTER TABLE recorded_notes ADD COLUMN call_direction INTEGER NOT NULL DEFAULT 2147483647 |
| 42 -> 43 | CREATE TABLE spam_list; ALTER TABLE history ADD COLUMN is_suspiciousSpam INTEGER NOT NULL DEFAULT 0 |
| <45 / ==45 -> 44 | CREATE 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 -> 47 | ALTER TABLE dynamic_area_tickets ADD COLUMN order_value INTEGER NOT NULL DEFAULT 0 |
| <50 -> 49 | ALTER 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 -> 51 | CREATE TABLE table_share_media |
| 51 -> 52 | dynamic_area_tickets + screens TEXT NOT NULL DEFAULT '["all"]'; statistics_questions + main_cli; contacts + extra_values_json TEXT NOT NULL DEFAULT '{}' |
| 52 -> 53 | CREATE TABLE db_table_notification_reader |
| 53 -> 54 | CREATE TABLE db_table_reverse_lookup_id |
| 54 -> 55 | CREATE TABLE shared_preference_v2, CREATE TABLE table_api_requests, CREATE TABLE ttm |
| 55 -> 56 | ALTER TABLE db_table_notification_reader ADD COLUMN source INTEGER DEFAULT 0 |
| 56 -> 57 | CREATE 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 inec.C16178l.f39901e:- create
events/event_metadata/transport_contexts+events_backend_id+contexts_backend_priorityindices transport_contexts + extras BLOB; new unique index; drop old indexevents + payload_encoding TEXTevents + inline BOOLEAN NOT NULL DEFAULT 1; recreateevent_payloads- create
log_event_dropped,global_log_event_state(+ seed row) events + product_id INTEGERevents + pseudonymous_id TEXT,experiment_ids_clear_blob BLOB,experiment_ids_encrypted_blob BLOB
- create
- WorkManager
androidx.work.workdb= version 23, migrations 1->2, 2->3, 3->4 … 21->22,autoMigrations13->14, 14->15, 16->17, 17->18, 18->19, 19->20, 20->21, 22->23, plusfallbackToDestructiveMigration().
5. SharedPreferencesh4
5.1 Architecture — MICO does not use Android SharedPreferences for its own stateh5
| Class | Original | Role |
|---|---|---|
p284f7.SharedPreferencesC16497y | f7.y | Custom 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.SharedPreferencesEditorC16496x | f7.x | Custom 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 unlessf41010asays the load completed; otherwise it triggers a background load.- On
SQLiteDatabaseCorruptException/SQLiteFullExceptionthe app callsActivityManager.clearApplicationUserData()(full wipe) andSystem.exit(0). SharedPreferencesC16497yregisters a broadcast receiver forEYECON.INTENT_ACTION_DATA_WAS_UPDATED.
5.2 Native Android SharedPreferences files owned by the apph5
| File name (MODE_PRIVATE) | Where | Keys |
|---|---|---|
spam_prefs | p672y5.C21867v.m39107r() | reportSpam, shareSpam — JSON dialog-throttle state (written by m39108u(type, key, text, buttonText)) |
after_call_feedback_prefs | af.RunnableC0127c | after_call_<state-lowercase> -> StringSet (per after-call feedback state) |
<pkg>_preferences (PreferenceManager.getDefaultSharedPreferences) | gf.AbstractC16733c | EYECON.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 incontactdb.shared_preference_v2.
5.3.1 Lifecycle, install, registration, server synch6
| Key | Type | Inferred meaning |
|---|---|---|
sp_install_time | long | First install epoch-ms |
SP_KEY_APP_FIRST_OPEN_TIME | long | First app-open timestamp |
SP_KEY_ORIGNALY_INSTALL_VERSION | String | First installed versionCode |
SP_KEY_INSTALL_REFERRER | String | Install referrer |
SP_KEY_app_instance_id | String | Per-install instance uuid |
SP_UUID | String | Client UUID sent to api.eyecon-app.com |
last_app_v_code_v5 | int | Last launched versionCode |
current_eyecon_version | String | Current known client version |
SP_KEY_IS_REJOIN | bool | User re-joined a contest after leaving |
SP_REJOIN_OLD_GENERATED_CLI | String | Old generated CLI kept for rejoin |
SP_JOIN_VERSION / SP_JOIN_VERSION_CODE | String/int | Version at contest join |
sp_join_time | long | Contest join timestamp |
SP_KEY_NOT_REGISTERED_WITH_REAL_CLI_V2 | bool | Registration done only with a generated number |
SP_KEY_INIT_ADD_CONTACTS_COUNT | int | Contacts added during onboarding |
SP_REQUIRED_SERVER_CONTACTS_REFRESH | bool | Force server-side contacts refresh |
SP_KEY_DID_UPDATE_V1453 | bool | One-off migration for build 1453 |
SP_KEY_STARTING_RESTORE_PROCESS | bool | Restore in progress |
SP_KEY_LAST_TIME_BACKUP | long | Last Google-Drive backup timestamp |
SP_KEY_IS_AFTER_RESTORE | bool | Post-restore flag |
SP_KEY_IS_AFTER_RESTORE_DA / SP_KEY_IS_AFTER_RESTORE_DA_STATS | bool | Post-restore Dynamic-Area flags |
SP_KEY_SHOW_BACKUP_RESTORE_SUCCESS_DIALOG | bool | Show restore success dialog |
SP_KEY_FIXED_RC_TIMESTAMP | long | Remote-config timestamp fix |
SP_KEY_LAST_EXIST_EVENT_TIME | long | Last “exist” event time |
SP_KEY_SYSTEM_EVENT_SENT | bool | System event already sent |
Eyecon-InstallerName | String | Detected installer (store) |
sp_isCalenderAvailable | bool | Calendar-app availability probe (constant in a7.a) |
5.3.2 Caller ID / dialer / call screening / recordingh6
| Key | Type | Inferred meaning |
|---|---|---|
SP_CALLER_ID_MODE-EYECON | int/String | Caller-ID window mode |
SP_DUAL_SIM_ALL_CONTACTS_DEFAULT-EYECON_v2 | String | Default SIM for “all contacts” import |
SP_EYECON_DEF_ACCOUNT_V2 | String | Default phone account handle |
SP_KEY_DEFAULT_PHONE_ACCOUNT | String | Selected PhoneAccount id |
SP_KEY_DEFAULT_NAVIGATION_APP | String | Preferred nav app package |
SP_KEY_RECORD_CALLS_MODE | int | Call-recording mode preference |
SP_KEY_AUDIO_RECORDING_MODE | int | In-call recording UI mode |
SP_KEY_AUDIO_RECORDING_MODE_JSON | String (JSON) | Recording-mode config JSON |
SP_KEY_AUDIO_RECORD_PROPERTIES_JSON_V4 | String (JSON) | Recording codec/rate properties |
SP_KEY_IS_CALL_RECORDING_ACTIVITY_ENABLE | bool | Call-recording screen enabled |
SP_KEY_CALL_RECORDING_MODES_BUBBLE_SHOWN | bool | Coaching bubble shown |
SP_KEY_DID_EVER_CLICK_BUBBLE_FOR_RECORDING_MODE | bool | Bubble click marker |
SP_KEY_LAST_CALL_RECORD_ID | long | Id of last recorded call |
SP_KEY_BLOCK_SPAM_CALLS | bool | Spam-call blocking toggle |
SP_KEY_DID_BLOCK_SYNC_V3 | bool | Block-list synced to server (v3) |
SP_KEY_FIXED_BLOCK_MISSING_NAME | bool | Missing-name block fix applied |
SP_KEY_CANCEL_BLOCK_NUNBER_NOTIFICATION | bool | ”Cancel block number” prompt state |
SP_KEY_DID_REPORTED_BLOCK_NUMBER_FIRST_BATCH | bool | First batch of reported blocks done |
SP_KEY_FAKE_AUTHENTICATED_CLI | String | Test/placeholder number while unauthenticated |
SP_KEY_UNAUTH_CLI_WHILE_TYPING | bool | Allow unauth CLI while typing |
SP_KEY_VALIDATION_WITH | String | Which validation flow succeeded |
SP_LAST_INCOMING_SMS_MS | long | Timestamp of last inbound SMS |
SP_KEY_LAST_SPAM_SYNC | long | Last spam-list sync |
SP_KEY_VERSION_SPAM_LIST | int | Spam-list schema version |
SP_KEY_SHOULD_SHOW_MISSED_CALL_ON_HISTORY_TITLE | bool | Missed-call badge on History title |
SP_KEY_IS_MISSED_CALL_NOTIFICATION_ENABLED | bool | Missed-call notification toggle |
SP_KEY_ONE_TIME_IGNORE_MISSED_CALLS | bool | One-shot ignore missed calls |
AMOUNT_OF_MISSED_CALLS_FOR_HISTORY_BUBBLE | int | Threshold for missed-call bubble |
SP_KEY_CLICKED_DRAW_ABOVE_GO_BTN / SP_KEY_CLICKED_ON_AUTO_START_GO_BTN | bool | Draw-over / auto-start onboarding clicks |
SP_KEY_DID_BATTERY_OPT_INTENT_FAILED | bool | Battery-optimisation intent failed |
SP_KEY_BACKGROUND_SERVICES_NOT_WORKING / SP_KEY_SUSPICIOUS_BACKGROUND_SERVICES_NOT_WORKING | bool | Diagnostics |
SP_KEY_FOREGROUND_NOTIFICATION_MODE | int | Foreground-service notification mode |
SP_KEY_DID_CLICK_CALL_TTM_BTN | bool | TTM button clicked |
SP_KEY_LAST_ROW_ID_WHATSAPP / SP_KEY_LAST_ROW_ID_WHATSAPP_W4B | int | WhatsApp contact sync cursor |
SP_KEY_IS_TOKI_ENABLED | bool | ”Toki” (temporary number) enabled |
MultiSimCallDialog_USER_CHOOSE_AS_DEFAULT | bool | Multi-SIM dialog result |
SP_KEY_SYSTEM_EVENT_SENT | bool | (see 5.3.1) |
5.3.3 SMS / external SMS / notification readerh6
| Key | Type | Inferred meaning |
|---|---|---|
SP_KEY_DONT_ASK_ABOUT_SENDING_SMS | bool | Suppress “allow sending SMS” prompt |
SP_SMS_PERMISSION_PROMPT_SHOW | bool | SMS permission prompt shown |
sp_new_external_sms_in_server | bool | New external SMS pushed to server |
SP_KEY_NEW_EXTERNAL_SMS_IN_SERVER | bool | Constant form of the above |
trash_sms_number | String (JSONArray) | Numbers whose inbox messages were deleted/hidden |
SP_IS_SMS_WINDOW_ENABLE_BY_USER | bool | User enabled the SMS window |
isSmsValidEnded | bool | Phone-number (SMS-capable) validation finished |
sendSmsEnded | bool | SMS send flow finished |
5.3.4 Contacts, photos, address book, socialh6
| Key | Type | Inferred meaning |
|---|---|---|
SP_KEY_AMOUNT_OF_CONTACTS | int | Contact count |
AMOUNT_OF_UI_CONTACTS | int | Page size for the contacts UI |
AMOUNT_OF_UI_HISTORY_LOGS | int | Page size for the history UI |
CELL_SIZE_FOR_FAVORITES_V3 | int | Favourites grid cell size (v3) |
SP_MAIN_VIEW_SORTING_MODE | int | Contacts list sort mode |
contactLangNeedUpdate | bool | Contact language needs re-sync |
SP_KEY_CURRENT_APP_LANGUAGE_v4 | String | Selected app language |
SP_KEY_TOP_CONTACTS_LANGUAGE | String | Language for Top-Contacts section |
SP_KEY_PHONE_NUMBERS_CACHE_V4 | String (JSON) | Cached phone numbers (v4) |
SP_KEY_NEED_UPDATE_PROFILE_PIC | bool | Profile picture needs updating |
updatePicsByForceDone | bool | Forced photo sync completed |
removeOldPhotos | bool | Legacy photo cleanup done |
picfile | String | Path to the current profile picture file |
SP_KEY_PICTURE_SOURCE | String | Where the picked photo came from |
SP_ALLOWED_TO_START_GIVING_PICS_BY_FORCE | bool | Force-photo hand-off allowed |
SP_KEY_PRE_EYECON_AMOUNT_OF_PHOTOS | int | Photos owned before Eyecon |
SP_KEY_IS_GIVE_PRE_EYECON_USAGE | bool | Pre-Eyecon usage flag |
SP_INIT_FRESP_PICS_COUNTER | int | Fresh-picture response counter |
SP_TELL_SERVER_CONTACT_HAS_NO_PHOTO | bool | Told server a contact has no photo (constant in a7.a) |
SP_KEY_LAST_TIME_REQUESTED_CARDS | long | Last card/photo request |
SP_KEY_SIM_CARDS_LIST_JA_CACHE_v2 | String (JSON) | Cached SIM card list (v2) |
SP_KEY_CONTACT_QUICK_GUIDE_JSON_V19 | String (JSON) | Contact quick-guide content (v19) |
SP_KEY_USER_EMAIL | String | Support e-mail (from onboarding) |
sp_eyecon_game_page_size | int | ”Eyecon game” list page size |
sp_social_installed_apps_v2 | String (JSON) | Installed social apps (v2) |
authenticatedCli / tempAuthenticatedCli | String | Authenticated / temporary caller number |
tempUserName | String | Temporary display name |
usernameserver | String | Server-provided display name |
userCountryISO | String | ISO country code |
facebookId / facebookToken | String | Facebook identity for social contacts |
SP_NEW_FACEBOOK_SOCIAL_CONTACT_ID / SP_NEW_FACEBOOK_SOCIAL_CONTACT_CLI | String/int | Facebook contact id / number |
SP_NEW_SOCIAL_DIALOG_TYPE | int | Social dialog variant |
socialSrc | String | Social source tag |
SP_KEY_DID_CLICK_SHARE_SOCIAL_BUBBLE | bool | Share-social coaching bubble click |
SP_KEY_FID__LIST__FOR_NOT_SHOW_DIALOG | String (JSON) | Facebook ids for which to suppress the dialog |
disappearingPhotosMAPV3 | String (JSON) | Map of disappearing-photo state (v3) |
5.3.5 Dynamic Area (“cards”), subjects, statisticsh6
| Key | Type | Inferred meaning |
|---|---|---|
SP_KEY_DA_LAST_TAGS | String (JSON) | Last DA tag set received |
SP_KEY_DA_LAST_BUNDLE_ID | String | Last DA bundle id |
SP_KEY_DA_NEED_PEEK_ANIMATION_V4 | bool | Peek animation needed (v4) |
SP_KEY_IS_DA_ENABLE_FOR_MAIN_SCREEN_V3 | bool | DA enabled on main screen (v3) |
EyeconDynamicArea.isUsed | bool | Has the user ever seen Dynamic Area |
SP_KEY_LAST_TIME_UPDATE_STATS_v3 | long | Last statistics update (v3) |
da_contact_call_timelast_call | long | Last “call” timestamp per contact |
da_contact_call_timelast_unanswered_outgoing_call | long | Last unanswered outgoing call |
da_contact_call_timemissed_call | long | Last missed call |
da_new_photo_last_first_card | long | Timestamp of last first “new photo” card |
da_permission_dismissed_<perm> | bool | DA 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
| Key | Type | Inferred meaning |
|---|---|---|
SP_KEY_LAST_DATE_FOR_FREE_PREMIUM | long | Last free-premium grant |
SP_KEY_LAST_DATE_FOR_VIDEO_PREMIUM | long | Last video-premium grant |
SP_KEY_LAST_DATE_FOR_VIRAL_PREMIUM | long | Last viral-premium grant |
SP_KEY_LAST_FREE_PREMIUM_TYPE | int | Type of last free premium |
SP_KEY_LAST_PREMIUM_UPDATE_CACHE | String (JSON) | Premium state cache |
SP_KEY_PREMIUM_OWN_CACHE | String (JSON) | Owned items cache |
SP_KEY_PREMIUM_PURCHASES_INFO_CACHE | String (JSON) | Purchase info cache |
SP_KEY_INAPP_PRODUCTS_CACHE | String (JSON) | In-app product catalogue cache |
SP_KEY_STORE_OWN_ITEMS | String (JSON) | Store owned items |
SP_KEY_STORE_PRODUCTS_IDS_JSON | String (JSON) | Store product ids |
SP_KEY_SUPPORT_CACHE_OWN_CACHE | String (JSON) | Support-flow owned cache |
SP_KEY_SHOW_PREMIUM_PRESENT | bool | Show the premium gift |
SP_KEY_SHOW_TIME_LIMIT_BANNER | bool | Show time-limited premium banner |
SP_KEY_WAITING_FOR_CUSTOM_THEME_PENDING_PURCHASE | bool | Pending custom-theme purchase |
SP_KEY_SELECTED_THEME_INFO | String (JSON) | Selected theme |
SP_KEY_SELECTED_THEME_E2 | String (JSON) | Selected theme (E2 experiment) |
SP_KEY_SELECTED_THEME_BY_SETTINGS_DEFAULT | bool | Theme default came from settings |
SP_KEY_IN_APP_UPDATE_COUNT_4.2026.04.23.1145 | int | In-app-update impressions for this build |
SP_KEY_IN_APP_UPDATE_LAST_SHOWN | long | Last in-app-update prompt |
SP_KEY_MY_VIRAL_ID | String | Viral/referral id |
SP_KEY_INVITER_ID | String | Inviter id |
5.3.7 Calls contesth6
| Key | Type | Inferred meaning |
|---|---|---|
SP_KEY_CONTESTS_JA | String (JSON) | Contests JSON |
SP_KEY_CONTEST_INFO | String (JSON) | Current contest info |
SP_KEY_CONTEST_NEED_UPDATE_IN_MY_SETTINGS | bool | Contest data needs refresh |
SP_KEY_CONTEST_NEED_UPDATE_IN_SERVER | bool | Server-side contest refresh |
SP_KEY_CALLING_CONTEST_DAILY_COUNT | int | Contested calls today |
<name>_singular_aggr_counter | int | Aggregated singular counter per name (p671y4.C21845b) |
5.3.8 After-call screen / surveys / ratings / consenth6
| Key | Type | Inferred meaning |
|---|---|---|
SP_AFTERCALL_TYPE | int | After-call screen type |
SP_AFTER_CALL_LOGS | String (JSON) | After-call log JSON |
SP_KEY_AFTERCALL_LOGS_START_DATE | long | After-call logs window start |
SP_KEY_SINGULAR_AFTERCALL_EVENT_HISTORY_JSON | String (JSON) | After-call event history |
SP_KEY_LAST_HISTORY_LOG | long | Last history-log row |
SP_KEY_DID_LOVE_SURVEY_SHOWN | bool | ”Do you love us” survey shown |
SP_KEY_DONT_ASK_ME_AGAIN_TRACKER | String | ”Don’t ask again” tracker JSON |
SP_KEY_LEGAL_MESSAGE_SHOWN_V2 | bool | Legal/privacy message shown (v2) |
SP_KEY_DID_EYECON_CONSENT_APPROVED | bool | Analytics consent approved |
5.3.9 Toki / Talk-To-Me (TTM) / WhatsApp / Telegram / quotesh6
| Key | Type | Inferred meaning |
|---|---|---|
SP_KEY_LAST_INIT_TTM_JSON | String (JSON) | TTM init state |
SP_KEY_PENDING_TTM_MESSAGES | int/String | Pending TTM messages |
SP_KEY_TTM_TOKENS_AMOUNT_JSONS | String (JSON) | TTM token balances |
SP_KEY_AMOUNT_OF_FULL_TOKI_SESSION | int | Full Toki sessions used |
SP_IS_WHATSAPP_WINDOW_ENABLE_BY_USER | bool | WhatsApp window enabled by user |
IS_TELEGRAM_WINDOW_ENABLE_BY_USER | bool | Telegram window enabled |
SP_WHATSAPP_BLURRY_SCREEN_DISPLAY_COUNT | int | WhatsApp blurred-screen counter |
SP_KEY_START_DYNAMIC_ANIMATION | bool | Dynamic animation started |
sp_key_start_id_plus_trail_time_ms | long | IdPlus “trail” popup start time |
SP_START_TRAIL_POPUP_SHOWN | bool | Trail popup shown |
SP_KEY_IS_CURRENT_QUOTES_LANGUAGE_SUPPORT_TTS | bool | Quotes language has TTS |
SP_KEY_SELECTED_QUOTES_LANGUAGE_v1 | String | Selected quotes language |
QuotesJson_v4 / NextQuotesUrl_v4 | String (JSON) | Quotes cache + next URL |
quotes index | int | Quotes pagination index |
5.3.10 AI assistant (auto-reply)h6
| Key | Type |
|---|---|
SP_KEY_AI_ASSISTANT_INITIAL_MESSAGE_FOR_DEFAULT | String |
SP_KEY_AI_ASSISTANT_INITIAL_MESSAGE_FOR_MISSED_CALL | String |
SP_KEY_AI_ASSISTANT_INITIAL_MESSAGE_FOR_DECLINED_CALL | String |
SP_KEY_AI_ASSISTANT_INITIAL_MESSAGE_FOR_SPAM_CALL | String |
SP_KEY_AI_ASSISTANT_INITIAL_MESSAGE_FOR_UNKNOWN_CALLER | String |
SP_KEY_IS_AI_ASSISTANT_DEFAULT_AUTO_USE_ENABLE | bool |
SP_KEY_IS_AI_ASSISTANT_MISSED_CALL_AUTO_USE_ENABLE | bool |
SP_KEY_IS_AI_ASSISTANT_DECLINED_AUTO_USE_ENABLE | bool |
SP_KEY_IS_AI_ASSISTANT_SPAM_CALL_AUTO_USE_ENABLE | bool |
SP_KEY_IS_AI_ASSISTANT_UNKNOWN_CALLER_AUTO_USE_ENABLE | bool |
5.3.11 Onboarding / permissions / settingsh6
| Key | Type | Inferred meaning |
|---|---|---|
SP_KEY_NEED_SHOW_ON_BOARDING | bool | Show onboarding |
SP_KEY_ON_BOARDING_SHORTCUT_SHOW | bool | Onboarding shortcut prompt shown |
SP_IS_OPEN_MENU_E2 | bool | E2 “open menu” experiment |
SP_IS_PERMISSION_ASKED.v1 | bool | Permission prompt already asked |
last_permission_notification_time | long | Last permission notification |
SP_KEY_REG_PERMISSIONS_SUMMERY | String (JSON) | Permissions summary |
permission_status_json / permissionDialogTiming | String | Permission state/timing |
SP_KEY_MY_SETTINGS_LOCAL_JSON | String (JSON) | Local settings snapshot |
SP_KEY_MY_SETTINGS_SERVER_JSON | String (JSON) | Server settings snapshot |
settings_version | int | Settings schema version |
SP_KEY_DONT_RECREATE_ACTIVITY_FOR_DARK_MODE | bool | Dark-mode recreation guard |
is_theme_changed_for_event | bool | Theme changed -> re-raise event |
SP_KEY_EYECON_REMOTE_CONFIG_JO_v2 | String (JSON) | Remote-config JSON (v2) |
5.3.12 Ads / remote config / analytics (app-owned)h6
| Key | Type | Inferred meaning |
|---|---|---|
SP_KEY_AD_LIMITER_LOGS | String (JSON) | Ad-limiter log |
app_open_ad_daily_count / app_open_ad_last_shown_time | int/long | App-open ad counters |
interstitial_daily_count / interstitial_last_shown_time | int/long | Interstitial counters |
interstitial_free_uses_for_add_contact | int | Free interstitials for add-contact |
interstitial_free_uses_for_photo_picker | int | Free interstitials for photo picker |
interstitial_free_uses_for_reverse_lookup | int | Free interstitials for reverse lookup |
calls_since_last_ad / calls_since_last_app_open_ad | int | Ad cooldowns |
eia_done / eia_call_counter / eia_page_view_counter | bool/int | Experiment-in-app counters |
pp_bubble_menuPP_V15 / pp_bubble_settingPP_V15 / privacy_police_agreedPP_V15 | bool/int | Paywall / privacy experiments (V15) |
Eyecon.SP_KEY_ADVID | String | Advertising id forwarded to server |
eyecon.SP_KEY_IS_BETA_USER | bool | Beta cohort flag |
Eyecon.isNeedAutoStart | bool | Auto-start still required |
eyecon_comparator_bug | bool | Comparator bug workaround |
Eyecon-targetSdkVersion | int | Target SDK the build was tuned for |
fg_notification_enable_by_af_rate | int | Foreground-notification AF rate |
fb_notification_EYECON_TYPES_v2 | String (JSON) | Facebook-notification categories seen |
fb_notification_timestamp_<id> / fb_notification_timestamp_five_stars | long | Per-notification timestamps |
fb_notification_cash_v2_<x> | String | Cash-reward notification state |
EyeInAppMessage-<id> | bool | Per in-app-message dismissal state |
EyeInAppMessageLastTimeShown | long | Last in-app message shown |
eyecon.TOPIC_SUBSCRIPTION-<topic> | bool | Per-topic push subscription |
Eyecon.Intent.Used | bool | Deep-link “already used” marker |
SP_KEY_LAST_EXIST_EVENT_TIME | long | (see 5.3.1) |
5.3.13 Spam dialogs, security, misch6
| Key | Type | Inferred meaning |
|---|---|---|
pref_key_report_spam_time_ms / pref_key_share_spam_time_ms | long | Throttle timestamps |
is_from_reverse_lookup | bool | Flow origin marker |
is_select_custom_background | bool | Custom background selected |
is_mac_updated | bool | MAC/ID update marker |
unregistered | bool | Registration state |
LAST_CLEARED_TIME | long | Last cache clear |
LAST_COPIED_NUMBER | String | Last number copied by the user |
SP_KEY_EYECON2_NOTES / SP_KEY_EYECON2_REMINDERS | String (JSON) | Voice-note / reminder lists |
sp_call_reminder_list / SP_CALL_REMINDER_LIST | String (JSON) | Call-reminder list (two keys, same purpose) |
SP_BUSY_MSG_TEXT-EYECON | String | Busy-message text |
cipher_key | String | Blowfish key suffix for contact-photo encryption (see §8) |
clientId / deviceName / osName / appVersion | String | Device fingerprint for API calls |
exceptions_history / stacktrace | String | Crash/log capture buffer |
hasInternetConnection / no_internet / isCanceled / CB_ERROR / error_code_num | mixed | Transient callback payload keys |
bubbleOptionsMain / bubbleOptionsInCallPopup | String (JSON) | Bubble options |
do_product_detail_load | bool | Product-detail load trigger |
shortcut_supported_event_send | bool | Shortcut event marker |
reveres_lookup_event_source | String | Reverse-lookup event source |
5.3.14 Dynamic key families (prefix + runtime suffix)h6
| Pattern | Built in | Suffix values |
|---|---|---|
da_permission_dismissed_ | p033b6.EnumC3480m | caller_id, default_dialer, default_dialer_with_caller_id_priority, autostart, battery_optimization, draw_above, notification_reader |
<name>_singular_aggr_counter | p671y4.C21845b | contact / event name |
fb_notification_timestamp_ | p672y5.C21851f | notification id |
fb_notification_cash_v2_ | ir.AbstractC17448b | variant id |
EyeInAppMessage- | ad/in-app code | campaign id |
eyecon.TOPIC_SUBSCRIPTION- | push code | topic |
after_call_ | af.RunnableC0127c | after-call state (lowercase) |
SP_KEY_IN_APP_UPDATE_COUNT_<versionCode> | in-app update code | build version |
6. DataStore / Proto DataStoreh4
No app-owned DataStore. All androidx.datastore usage belongs to bundled SDKs:
| Owner | DataStore name | Kind |
|---|---|---|
as.C3363x1 (Google DataTransport scheduler) | rTPhFaTmdR | Preferences DataStore |
p438nj.C18824g0 (Firebase Sessions) | AbstractC18851u.f47916a | Preferences DataStore |
p483pj.C19364j (Firebase Sessions, 2nd instance) | AbstractC18851u.f47917b | Preferences DataStore |
p000.C0000a / p483pj.C19368n | Firebase session datastore | Preferences 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
| Path | Purpose | Contents |
|---|---|---|
.Eyecon/.nomedia | Hides files/.Eyecon from the media scanner | AbstractC0039a.f201a |
.Eyecon/temp_photo.jpg | Temporary profile photo during pick/crop | AbstractC0039a.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 notes | audio |
calls/ | Backup staging: call recordings | audio |
call_logs/ | Backup staging: call-log export | text/CSV |
address_book/ | Backup staging: address-book export | text/CSV |
ttm.zip | Backup staging: TTM voicemail audio archive | zip |
eye_log.txt | Rolling app log (AbstractC0085x, p410lo.C18416a) | logs, device info, app version, locale, install date |
eye_log.zip | Zipped log bundle for support upload | logs |
image_provider/ | FileProvider share path (res/xml/image_share_filepaths.xml) | shared images |
com.crashlytics.settings.json | Crashlytics (3rd party) | json |
*.eyecon.properties | Per-process WebView/timer config (main., secondary., temp.) | key=value |
7.2 Cache (/data/data/com.eyecon.global/cache/)h5
| Path | Contents |
|---|---|
exceptions_and_logs.data | Buffered crash/exception log (AbstractC0085x:529) |
instrument/ | Facebook SDK (3rd party) |
lottie_network_cache | Lottie animation cache |
7.3 External public storageh5
| Path | Purpose |
|---|---|
Environment.getExternalStoragePublicDirectory(DIRECTORY_DOCUMENTS)/Eyecon (MyApplication.f12172q) | Base folder when external storage is mounted |
.../Documents/Eyecon/ContactsPhotos | Contact photos |
.../Documents/Eyecon/RecordedNotes | Voice-note recordings |
.../Documents/Eyecon/RecordedCalls | Call 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 withREAD_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
| Path | Purpose |
|---|---|
databases/contactdb | Main DB |
databases/eyecon_restore_temp_database | Restore staging DB |
databases/eyecon_temp_db | Swap-out during restore |
databases/com.google.android.datatransport.events | FCM event queue |
databases/androidx.work.workdb | WorkManager |
databases/metrics-db | Moloco metrics |
databases/com.amplitude.api* | Amplitude legacy |
databases/com.im_10.8.7.db | InMobi |
databases/<name>-journal, -wal, -shm | SQLite side-files |
7.5 Other filesh5
| Path / name | Purpose |
|---|---|
params.json | WebView / AMP session params (ir.AbstractC17448b, p672y5.C21856k) |
item_info.json | Dynamic-Area card item cache (p034b7.RunnableC3497d, p674y7.RunnableC21879g) |
PersistedInstallation.<id>.json | Firebase Installations |
ttad_dir | Mintegral (Mbridge) |
com.eyecon.global.fileprovider | FileProvider 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 bookeyecon_backup_for_call_logs <- call logseyecon_backup_for_calls_records <- call recordingseyecon_backup_for_notes_records <- voice noteseyecon_backup_for_ttm_audios <- voicemail audioSource: p282f5.AbstractC16422f.f40666B; staged locally as pending_temp_eyecon_backup_for_* (p282f5.C16442z).
8. Local personal / sensitive data inventoryh4
| Data class | Where exactly | Severity |
|---|---|---|
| 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 id | contactdb.sms_table (body, sender, sender_name, message_id); u7.b logs "Insert new ExternalSms: body: ... sender: ..." | Critical |
| Voicemail / TTM transcripts + originating number | contactdb.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_list | High |
| Reverse number-lookup results (number -> name) | contactdb.db_table_reverse_lookup_id | Medium |
| Facebook social graph for contacts | contactdb.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_number | contactdb.shared_preference_v2 (and legacy shared_preference BLOB) | Critical |
| Blowfish key material for contact-photo encryption | SP key cipher_key = Random().nextInt(1000); effective key = "Ewu" (R.string.cipher_key) + cipher_key + "sTQHGK" -> only 1000 possible keys | High (crypto weakness) |
| Java-deserialised legacy preference map | contactdb.shared_preference.data read via ObjectInputStream.readObject() | High (deserialisation sink) |
| Full contact DB uploaded to Google Drive | Drive appDataFolder file eyecon_backup_for_database | High |
| Exposed file tree via FileProvider | com.eyecon.global.fileprovider with path="." on every root in res/xml/file_paths.xml | Medium |
| Device/app fingerprint in logs | files/eye_log.txt, cache/exceptions_and_logs.data | Medium |
Other security-relevant behaviours observed alongside the data layer
MyApplication.uncaughtExceptionuploads a log bundle; on SP-load failure the app callsclearApplicationUserData()thenSystem.exit(0).eo.C16303bkills all other processes (Process.killProcess) on preference-load failure.allowBackup="false"andfullBackupContent="false"are declared in the manifest (good), but external-storage recordings are unaffected.- Multiprocess:
:notification_process,:restart_app.
9. Confidence / Unknownsh4
| Item | Confidence | Note |
|---|---|---|
contactdb name, version 57, and all 21 CREATE TABLE statements | High | Read directly from p008a7.C0041b.onCreate, with column constants resolved from p008a7.AbstractC0039a. |
| Migration chain 1 -> 57 | High | Read 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 column | Medium-High | Constant declared in a7.a but never referenced by any DDL/DML in the tree. |
| Room DB is dead code | High | EyeconRoomDB 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 v57 | High | Constructed with the same helper class and version constant (p282f5.C16434r:267). |
Custom SharedPreferences persisted in contactdb | High | Confirmed by eo.C16303b SQL and by the DB wipe on SQLiteFullException. |
| SP key list completeness | Medium-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 keys | Low-Medium | Keys 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 inventory | High 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 SQL | Low | DBContacts 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 schemas | High for schema, Low for active use | Mbridge’s download_record DDL string exists but the helper that opens the DB appears stripped; may be dead code. |
| DataTransport DB version 7 | High | ec.C16178l.f39900d = 7, instantiated with that version in ec.C16171e. |
| External-storage locations | Medium-High | Both Documents/Eyecon/... and files/Eyecon/... forms appear, selected by AbstractC0042b0.m178a0() (storage-availability check; decompiled body not fully inspected). |
| Blowfish key weakness | High | Random().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 blob | High | ObjectInputStream.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/ | High | assets/ contains only ad/SDK JS, fonts, dic/ and dexopt profiles. |
Exact recorded_notes <-> on-disk file mapping | Medium | file_name column + AbstractC16098a.m31460c(0|1) directory selection; the per-file naming scheme was not fully resolved. |
Room @Entity of Moloco (events) column names | Medium | Column 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:
| Tag | Meaning |
|---|---|
| OBSERVED | A literal string, a call site, a constant, a SQL statement, or a decompiled statement that exists verbatim in the code. |
| INFERRED | My 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:
- The
/* JADX INFO: renamed from: <orig> */comment above every class/member. - The inline
/* JADX DEBUG: Method not inlined, still used in: [...] */comments, which name the original class of every call site. - 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 dir | Original pkg | Key classes and recovered role |
|---|---|---|
p008a7 | a7 | AbstractC0039a = 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. |
p620w6 | w6 | C21273a = 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. |
p284f7 | f7 | SharedPreferencesC16497y = 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. |
p494q7 | q7 | EnumC19662h1 = 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. |
p262e7 | e7 | AbstractC16130b = AsyncCallbackBase (e7.b): m31516i()=onSuccess, m31515h()=onError, m31518o()=setResult, m32061b()=getResult, m32062c()=getIntResult, m32063d()=getExtras. |
p365jd | jd | RunnableC17614z = jd.z join.jsp + cli_auth/gettransport. |
p516r5 | r5 | RunnableC19875s = 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). |
p647x6 | x6 | C21649b = ApiRequestDB facade (x6.b) - durable outbound request queue. C21651d/C21652e/C21653f = queued request kinds (FormPost / RawBodyPost / FormGet). |
p648x7 | x7 | Spam 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). |
p623w9 | w9 | RunnableC21301b = spam_list DB loader + ApiRequestDB writer (w9.b). |
p646x5 | x5 | AI 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). |
p636wo | wo | C21474f = Retrofit container (wo.f) - builds http://numberlookup.instabridge.com/. |
p554t5 | t5 | C20343m = PersonDataLoader (t5.m), C20337g = PersonNameCache (t5.g), RunnableC20341k = load runnable (t5.k), EnumC20336f = cache type (t5.f), C20340j = pic callback. |
p397l5 | l5 | C18302h = CallerIdDataManager (l5.h), C18299e = Contact. |
p541s5 | s5 | RunnableC20065s = removecontacts.jsp push (s5.s), RunnableC20051e = misc. |
p572u4 | u4 | RunnableC20597o = picapproval.jsp, start-ttm, mysettings write, ad callbacks. |
p575u7 | u7 | AbstractC20698s = SmsDB (u7.s), RunnableC20680b = ExternalSmsDB (u7.b). |
p260e5 | e5 | AbstractC16098a = audio-file storage (e5.a), C16102d = AudioRecordsDataManager (e5.d), C16104f = RecordedNote model, RunnableC16111m = mega runnable (after-restore call-log rebuild, join variants). |
p261e6 | e6 | RunnableC16126b = misc (e6.b): DB atomic swap during restore, permission dialogs, waitForServerUpdate. C16128d = StatisticsCalculator (e6.d). |
p282f5 | f5 | Backup/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. |
p303g5 | g5 | C16707i = EyeconBilling (g5.i), C16708j = PurchasedProductInfo (g5.j). |
p358j5 | j5 | C17487c = FreePremium/ViralPremium (j5.c), C17496k = PaidPremium (j5.k), C17485b = viral-id fetchers. |
p687yl | yl | C21952k = Talki UDP transport (yl.k) - 15-byte-header datagram protocol with FEC. |
p633wl | wl | C21450m = TalkiSession (wl.m) - signalling JSON + audio capture/playback. |
p340i7 | i7 | RunnableC17180l0 = i7.l0 misc (UDP connect, social removal). |
p703z7 | z7 | C22122v0 = TokiFragment / Toki session UI (z7.v0). |
p381k7 | k7 | C17791c = SyncAdapter (k7.c) - onPerformSync body is EMPTY (dead code). |
p607vk | vk | RunnableC21093i = 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. |
p599v7 | v7 | RunnableC20908f = getnames.jsp (photo-picker), spamreport POST, AMR AudioRecord factory. |
p448o4 | o4 | InterfaceC19056i = AdvertisingId provider (o4.i). |
p418m5 | m5 | C18502p = DeviceInfo (m5.p: IMEI, SIM list). C18491e = call-state machine (m5.e). C18498l = misc. |
p444o0 | o0 | C19066s = misc (o0.s). |
p449o5 | o5 | AB-test / variant resolution. |
p399l7 | l7 | C18312b = PhoneNumberUtil (l7.b): normalise, +-prefix, validity, country detection. |
p673y6 | y6 | C21872a = query-parameter pair (y6.a). |
p420m7 | m7 | C18533l = OkHttpClient, C18540p = ImageLoader (m7.p, m34736e/m34732a), AbstractC18543s = BitmapUtils. |
p466oq | oq | Ktor client (oq.c0, oq.e0, oq.u2…). |
p032b5 | b5 | AbstractC3464e = Analytics/EventReporter (b5.e), C3465f = EventBuilder (b5.f). |
p360j7 | j7 | C17534t/C17515a = PermissionRequestDialog (j7.t). |
p720zq | zq | AbstractC22312g = PermissionUtil/Roles + random Toki port chooser (zq.g). |
p236d5 | d5 | AbstractC15804d = RemoteConfig (d5.d) with ~200 compiled-in default keys; C15803c = RemoteConfig singleton. |
p621w7 | w7 | C21295o = SocialMediaResolver (w7.o) - Facebook/Instagram/TikTok media scraping. |
p612vs | vs | Crackle RTB mediation adapter (third-party, mixed in). |
com.facebook.appevents.AbstractC9039h | app code, not Facebook | The 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.RunnableC2598c | app code, not AndroidX | getnames.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-gatedmorning_ads_refresh_job),Ads/CustomMediaView,Ads/Google/FullscreenAds/FullscreenNativeAdsActivity. - Delegates to:
p614w0(ad-mediation orchestrator, GAM/AdMob unit selection),p572u4(u4ad 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_requestsindirectly if a queued request is used). - Permissions:
INTERNET,ACCESS_NETWORK_STATE;POST_NOTIFICATIONSfor the FGS. - Notes (OBSERVED): AdMob app id
ca-app-pub-2959190743815944~6092014645; Crackle key inR.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_030925and bannerca-app-pub-2959190743815944/3218439299, refreshac_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(viap607vk.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 broadcasteyecon_remote_config_test). - Delegates to:
p236d5.AbstractC15804d(d5.d, RemoteConfig singletonC15803c),p032b5.AbstractC3464e(b5.e, event reporter),p466oq(Ktor) +p620w6.C21273a.m38417afor the multipart upload. - Endpoints:
eventreport.jsp(POST, JSON{name,type,attributes,timestamp}viap607vk.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 returnsretry()without it),INTERNET. - Notes (OBSERVED): the call-log worker is off by default -
analytics_worker_urldefault 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; readsMediaStore.Filesonexternal/external_primary. - Permissions:
RECORD_AUDIO,READ_EXTERNAL_STORAGE,WRITE_EXTERNAL_STORAGE. - Remote gates:
is_recording_calls_enabled_v2,call_recording_mode(default0),recording_calls_method_mode(default-1).
1.6 Backuph5
- Feature: Google Drive
appDataFolderbackup + 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.fDriveHandler base),p282f5.C16426j(f5.jdialog),p282f5.C16434r(f5.rrestore),p282f5.C16442z(f5.zupload),p282f5.C16418b0(f5.b0Drive REST wrapper),p261e6.RunnableC16126bcase 4 (atomic DB swap). - Endpoints:
GET api.eyecon-app.com/app/mysettings(to read the last-backup descriptor) andPOST api.eyecon-app.com/app/mysettings(type=backup_info,key=<epoch-ms>,value={st_ts, account}) viap620w6.C21280h.m38440m->p572u4.RunnableC20597ocase 7. Google Drive REST (appDataFolder). - DB tables: whole
contactdbfile is copied; plus dirsfiles/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.RunnableC21276dcase 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_CALLreceiver, “Talk To Me” (TTM) voicemail UI, SIM-card widgets. - Key classes:
Call/CallService(foreground,specialUseFGS: “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.ecall-state machine),im.RunnableC17334k(im.kstate dispatcher),p379k5.RunnableC17750b(TTM DBk5.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_nstates,noHistory=true,singleInstance). - Delegates to:
p474p5.AsyncTaskC19296a(p5.a0network AsyncTask),p516r5contact lookup. - Endpoints:
cantalk.jsp(recvcli,q),cantalksaw.jsp(opened when the recipient opens the question),cantalkreceived.jsp,getcantalkstatus.jsp(viap474p5.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) andis_can_talk_enable(v22->23);getupdatedcontactsinfo.jsppropagates 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),p516r5contacts,p358j5premium. - 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(handlesINSERT/INSERT_OR_EDITfor contacts andtext/x-vcard). - Delegates to:
p516r5.C19854e(r5.eContactsContract reader),p397l5.C18302h(l5.hCallerIdDataManager),p554t5.C20343m(t5.mPersonDataLoader),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; readsContactsContract.{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.fDB),p672y5.C21856k(y5.kstatistics DB),p284f7.RunnableC16473i(f7.iDynamicAreaDataLoader),p007a6(RecyclerView plumbing). - Endpoints:
cards.eyecon-app.com(Remote Configdynamic_area_url) - no direct call site found in this build; fetch is throttled bycards_update_threshold(default120min) inp516r5.RunnableC19875sdefault 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_expressiondefaults 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(internaleyecon.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_RINGINGbroadcast withTYPE_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” (
EyeconChromiumExceptionis a WebView renderer crash wrapper). - Key classes:
InCallCallerId/EyeconChromiumException. - Delegates to:
androidx.webkitChromium render-process handling; the overlay itself is rendered insideCallActivity+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(default3),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 theACTION_CALL/ACTION_DIAL/RECENT_CALLShandler, and as the launcher viacom.eyecon.global.pink|blue|greenactivity-aliases),MainScreen/MainFragment,Communication/EyeSearchActivity,Communication/ContactListInfoArea,Communication/ContactGridTextArea,Communication/Favorites/FavoritesFragment,Communication/ForYou/ForYouFragment,Communication/History/HistoryFragment. - Delegates to:
p516r5contacts,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) andC21654a(share dialog),p648x7.C21657d(spam entry),p599v7(getnames.jspphoto-picker name harvest),p621w7(shareendpoint),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.mo721c0mapsspam_type->isSpam/isSuspiciousSpamusingx7.d.m38861e(t)(=1 or 3) andx7.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-linkeyecon_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 inp647x6/p620w6. - Delegates to:
p647x6.C21649b(x6.b),p620w6.RunnableC21275ccase 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 runnableRunnableC0074r0),p315h7.C16945i(thread pools),p032b5.AbstractC3464e(crash analytics),p620w6,p623w9,p648x7(DailyWorker does the spam-list sync). - Endpoints:
spam(list_version),notifyaddcontact.jspviabm.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):
EyeconRoomDBis declared but never instantiated (dead code, seeDATA-LAYER.mdsection 3.1).MyApplicationalso installs itself as the process-wideUncaughtExceptionHandlerand overridesstartActivity/startService/startForegroundService(the latter is a no-op whenPOST_NOTIFICATIONSis missing).
1.21 Permissionsh5
- Feature: The consolidated runtime-permission + Android-role requester used everywhere.
- Key classes:
Permissions/ExtendedPermissionPromptActivity(launched fromRegistrationActivity.m10525M0whenpermission_prompt_text == "long"). - Delegates to:
p360j7.C17534t(j7.tPermissionRequestDialog) andC17515a(permission+role combined dialog),p720zq.AbstractC22312g(zq.gchecks + role requests). - Endpoints: — (per-request telemetry to
eventreport.jspviaAbstractC3464e.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
AccountManageraccount type “Eyecon” with aSyncAdapterand a contactsRawContactsstructure - 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.RawContactswith?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 Imagessearch?hl=en&udm=2&q=(reverse image search, Remote Configgoogle_photos_link_loader),api.paperquotes.com,www.facebook.com/www.instagram.com/www.tiktok.commedia. - 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_idupload). - 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-childViewFlipper),Registration/Onboarding/OnboardingActivity,Onboarding/FakeCallActivity,Registration/PhoneNumberValidationActivity(deep-linkeyecon_number_validation://),SMSRetrieverBroadcast,InstallReferrerReceiver,PreparingContactView,EyeProgressBarRegistration,RegisterNotificationWork. - Delegates to:
p494q7(callbacks),p365jd.RunnableC17614z(jd.zjoin.jsp + gettransport),p516r5.RunnableC19875s(r5.scheckcode),p607vk.RunnableC21082e0(vk.e0social_auth/use_cli_validation,social_auth/validatetoken),p284f7.C16489q(emulator detection),p262e7.AbstractC16130b(e7.bcallback 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(JobSchedulerjob id24). - Delegates to:
p621w7.C21295o(w7.omedia resolver),p621w7.C21294n(ShareVideoDB),p621w7.C21293m(w7.mshare payload). - Endpoints:
POST api.eyecon-app.com/app/sharewithapplication/jsonbody{cli_list:[...], type, content, provider}(built inp607vk.RunnableC21093icase 24), plusmessages/getpolling. - 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-uaheaders to fetch Facebook/TikTok media directly (p607vk.RunnableC21093idefault branch).
1.27 Spamh5
- Feature: Spam report + block list UI and DB.
- Key classes:
Spam/SpamAndBlockFragment, and the wholep648x7+p623w9implementation. - Delegates to:
p648x7.C21656c(x7.cdialog),C21675v(x7.vbinder),C21670q(x7.qSpamDB),C21657d(x7.dmodel),p623w9.RunnableC21301b(w9.blist loader/writer),p648x7.C21654a(x7.ashare 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, plusSP_KEY_VERSION_SPAM_LISTinshared_preference_v2and the native SP filespam_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.ltheme 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_WINDOWonly 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 type8to detect the phone being near the ear),Toki/TokiActivity,Toki/TokiContactsChooserActivity,ClearFocusOnBackEditText. - Delegates to:
p703z7.C22122v0(z7.v0TokiFragment/session),p633wl.C21450m(wl.mTalkiSession),p687yl.C21952k(yl.kUDP transport),as.C3349u2(as.u2TalkiServer, log-tag"TalkiServer"),p340i7.RunnableC17180l0(i7.l0, doesInetAddress.getByName(...)). - Endpoints: plain UDP to
ptt.eyecon-app.comon a port chosen at random from Remote Configtoki_ports(default[5620,50000-50010]). No HTTP, no TLS. - DB tables:
contacts(phone_number_in_server), plus the in-memory session registryz7.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-pageViewFlipper.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
MyApplication.attachBaseContextresolves 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.MyApplication.onCreate: setsf12172q = Documents/Eyecon, installs itself as the defaultUncaughtExceptionHandler, creates the singletonC0041b f281a = new C0041b(context)(thecontactdbhelper, v57), loads the customSharedPreferences(contactdb.shared_preference_v2), allocates twoLruCachebitmap caches sizedmaxMemory/24, and startsnew Thread(new RunnableC0074r0(this, 0)).RegistrationActivity.m10512l0()/m10514o0()read two Remote-Config strings, caching them into SP asSP_KEY_REG_FLOW_MODE_MODE(reg_flow_mode, default"auto") andSP_KEY_REG_PERMISSIONS_PROMPT_MODE(permission_prompt_text, default"baseline").
A.2 OBSERVED - the onboarding pages (ViewFlipper children)h6
| Child | Content | Analytics event |
|---|---|---|
| 0 | Welcome / language | Reg_welcome_pageView with Source = "How Eyecon works" or "First screen" (from OnboardingActivity.m10463C0() = Remote Config is_new_onboarding_enable) |
| 1 | (skipped in code) | - |
| 2 | Choose 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 |
| 3 | 4-digit code verification (EditText x4) | Reg_verification_pageView with Reg_type_chosen = Phone/Google/FB |
| 4 | Profile (display name + photo) | Reg_profile_pageView |
| 6 | Contacts 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", thenm10456b(referrer, 1). - Three recognised shapes:
referrer.startsWith("evid-")andreferrer.substring(5).length() == 12-> viral id stored in SP keySP_KEY_INVITER_ID, thenm10457c()logsInvite_successfully_accepted(guarded bysendViralityEventDone).referrer.startsWith("eyecon_ch_")-> split on_campid_into (channel, campaign id), analyticsInstall referrer channelwithChannel/Campaign ID.- otherwise parse
utm_contentfrom 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 = truetoast R.string.authentication_approvedSP: authenticatedCli = l7.b.normalise(tempAuthenticatedCli)f12984r = <"cli" extra from the callback> // the number that was validatedf12982q = tempUserNameRegistrationActivity.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_aNote the unusual construction: the whole query string is built with
lo.a.m34580q(ArrayList)(OkHttpHttpUrl.Builder), then concatenated onto the URL, and the call is made with an empty param list - sojoin.jspis aGETwhose parameters live entirely in the query string. Response headers are collected into aHashMap.
join.jsp query parameters (OBSERVED, in insertion order):
| # | Param | Value |
|---|---|---|
| 1 | cli | l7.b.m34444e(...) - normalised phone number (digits + +), or the social CLI as-is |
| 2 | username | literal "New_User_Eyecon" |
| 3 | devicename | AbstractC0062l0.m357l1() - formatted device name |
| 4 | user_lang | AbstractC0062l0.m356k1() |
| 5 | os | "Android " + Build.VERSION.RELEASE + " ; " + Build.VERSION.SDK_INT |
| 6 | public_id | SP SP_UUID (AbstractC9039h.m10625E()) |
| 7 | adv_id | advertising id (o4.i.m35361a()) |
| 8 | mc | AbstractC0042b0.m175X() - Wi-Fi MAC address |
| 9 | viral_id | SP SP_KEY_INVITER_ID |
| 10 | cli_cc | AbstractC0062l0.m371z1() - user country code |
| 11 | svr | only when is_already_social_auth; the value of q7.b1.f50539o (server id from social login) |
| 12 | transport | q7.h1.f50618a (so_sms / mo_sms / so_flash / fb / social_login) |
| 13 | generated_cli | SP SP_REJOIN_OLD_GENERATED_CLI, if non-null |
| 14 | imei | m5.p.m34691f() - device IMEI |
| 15 | os_id | AbstractC0042b0.m172U() - Settings.Secure.ANDROID_ID |
| 16 | cv | vc_749_vn_4.2026.04.23.1145_a |
join.jsp response handling (OBSERVED):
- HTTP != 200 ->
w6.h.m38437j-> maps the body integer throughq7.e1(EnumC19653e1) -> error enum. - HTTP 200 but empty body ->
EnumC19653e1.JOIN_RESPOND_CODE_200_WITHOUT_CID(“missing public id”). - Response headers:
pi-> stored into SPSP_UUID(“Authenticator/updatePublicId”);sd->isDeviceChangedwhen the value equals"0";is_new->isJoinwhen the value equals"1". - Response body: becomes the callback result and is written to SP key
clientIdbyq7.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 thejoin.jspresponse body (m32061b()of the success callback), andp494q7/C19677n.java:201- from thesocial_auth/use_cli_validationresponse body. Both are then propagated tocli_auth/gettransportasreg_id… no - thereg_idthere is the FCM token. TheclientIdis whatw6.asends as thee-authheader on every subsequent request, and what is sent asmsisdnonuploadpic.jspand ascidin the RSA-encryptedgrps/sgnbody. So: the account credential is a server-issued opaque token fromjoin.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> ®_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:
- 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",sendSmsEndedstaysfalse). - Google/Huawei SMS Retriever API -
SMSRetrieverBroadcastlistens forcom.google.android.gms.auth.api.phone.SMS_RETRIEVED_ACTIONandcom.huawei.hms.auth.api.phone.SMS_RETRIEVED_ACTION; ifEXTRA_STATUS.getStatusCode() == 0it rebroadcasts the extras under the internal actionINTENT_ACTION_AUTH_SMS_RECEIVED. This is the “App-hash”-free auto-read path. - Legacy raw PDU -
com.eyecon.global.Sms.MySmsReceiver(highest-priority,BROADCAST_SMS-protected receiver forandroid.provider.Telephony.SMS_RECEIVED) parsesSmsMessage.createFromPdu, extracts the 4-digit code withAbstractC16468f0.m195w(...), and forwards it toCallServicewith extrassmscode,incoming_number,state.CallService.onStartCommandwithextras.getInt("state") == 6launchesPhoneNumberValidationActivitywithphone_number+smscode; withstate == 7it callsDBContacts.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.jspcreates the account,gettransporttells 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), andcheckcodeconfirms 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 (theso_flashbranch readsdetails.pattern+details.timeoutandRegistrationActivity.mo10497ulogs"Reg SO flash started"and registers a receiver forEYECON_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 viaupdateuserdetails.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 Configcircus_keymeans the server can rotate the key the client presents without shipping an update. Confidence: high (seeBACKEND-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-onlypic?cli= request builder (OBSERVED, w6.h.m38434g):
// p620w6/C21280h.java:317-338if (!l7.b.m34452q("+" + str2)) { callback.mo590e(null, 812); return; } // not a valid numberString 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); // ImageLoaderFetched 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" -> booleanSpam 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): readsmap.get("CB_KEY_SPAM")->x7.d, readsmap.get(AbstractC0039a.f219g /* "name" */), then setscontact.isSuspiciousSpam = x7.d.m38862f(spam_type)(== 2) andcontact.isSpam = x7.d.m38861e(spam_type)(== 1 || == 3).ContactReminder/BaseAfterCallActivityrenderR.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 intonames_photos_cache(data_type = EnumC20336f.f52884a) and the spam verdict intocontacts.
B.5 INFERRED - Flow Bh6
- The whole design is fail-open on the network: if
getnames.jsp404s or 200s with an empty name, the client synthesisesspam_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=trueongetnames.jspand&is_callerid=true&size=...onpic?cli=are the same server-side switch, so a caller-ID photo request also returns the name/spam verdict. Confidence: medium.- The
requestApi=okHttp|URLconnectionparameter randomised once per process (Random().nextBoolean()inw6.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 == 3is treated as “is spam” by the UI but is never produced bygetnames.jsp- it comes only from the crowd-sourcedspamlist. 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:
| Store | Table | spam_type semantics | Enum |
|---|---|---|---|
| Crowd/AI spam list | spam_list | 1 = SPAM (crowd/AI), 2 = suspicious/“maybe spam”, 3 and 4 = user-authored entries (never overwritten by the server list) | x7.o -> SPAM, BLOCK |
| Block list | block_list | is_enable 0/1 + create_timestamp | same x7.o enum |
| Spam on a contact | contacts | derived 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-105int 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):
- Load the local
spam_list(SELECT * FROM spam_list ORDER BY cli_in_server). - Keep only the local entries whose
spam_typeis3or4(the user’s own reports); drop everything the server previously supplied. - Binary-search-merge in every entry from
spammersthat is not already present; sort bycli_in_server. - Rewrite the whole table in one transaction (
DELETE FROM spam_listthen per-rowinsert) and store the newversionin SPSP_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_spamReport dialog x7.c (C21656c) + binder x7.v (C21675v), layout R.layout.report_spam:
- Mode
1= report-spam (titleR.string.report_spam, CTAR.string.report_as_spam,R.id.EB_is_a_spam), with aCB_blockcheckbox (“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=ReportAndShareSpamDialogwithshareSpam, which sendsR.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_prefsunder keysreportSpam/shareSpam, written byy5.v.m39108u(type, key, text, buttonText);reportSpamis a small JSON read back atw6.dcase 26 asoptInt("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_type1/2are server-supplied (crowd reports + backend ML),3/4are 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_cachewithdata_type = <name>and invalidated viamodify_time+ the Remote Configserver_data_cacheexpiry (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, andEyeconCallScreeningServicecurrently only ever callssetDisallowCall(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.comis 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 APIRetrofit 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.C0155aenum adapter). - Interface
x5.ais a Kotlinsuspend fun(uo.dContinuation 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 class | MessageAssistantSourceKey | Start type string (x5.h.m38835c) | SP auto-use flag (x5.h.m38836d) |
|---|---|---|---|
MessageAssistantSource.IncomingCallFullscreen | IncomingCallFullscreenKey | DEFAULT | SP_KEY_IS_AI_ASSISTANT_DEFAULT_AUTO_USE_ENABLE |
MessageAssistantSource.IncomingCallNotification | IncomingCallNotificationKey | DEFAULT | same |
MessageAssistantSource.IncomingCallPopup | IncomingCallPopupKey | DEFAULT | same |
MessageAssistantSource.AfterCall | AfterCallKey | DEFAULT | same |
MessageAssistantSource.MissedCallPopup | MissedCallPopupKey | DEFAULT | same |
MessageAssistantSource.FollowUp | FollowUpKey | DEFAULT | same |
MessageAssistantSource.CallDeclinedAutoUse | CallDeclinedAutoUseKey | DECLINED_CALL | SP_KEY_IS_AI_ASSISTANT_DECLINED_CALL_AUTO_USE_ENABLE |
MessageAssistantSource.CallMissedAutoUse | CallMissedAutoUseKey | MISSED_CALL | SP_KEY_IS_AI_ASSISTANT_MISSED_CALL_AUTO_USE_ENABLE |
MessageAssistantSource.SpamCallAutoUse | SpamCallAutoUseKey | SPAM_CALL | SP_KEY_IS_AI_ASSISTANT_SPAM_CALL_AUTO_USE_ENABLE |
MessageAssistantSource.UnknownCallerAutoUse | UnknownCallerAutoUseKey | UNKNOWN_CALLER | SP_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 rulex5.h.m38842j(cli, name) // onSpamCall if (enabled) { x5.v.m38851a(ctx, new MessageAssistantStartFlowArgs(cli, name, new MessageAssistantSource.SpamCallAutoUse()), "SPAM_CALL"); return true; } return falseD.3 OBSERVED - request DTOs, field by fieldh6
MessageAssistantStartFlowArgs (Moshi, @JsonClass(generateAdapter=true)) - the WorkManager input:
| Kotlin field | JSON name | Type | Source |
|---|---|---|---|
number | number | String | C19862i.phone_number / the raw caller id |
contactName | contactName | String | C19862i.private_name |
source | source | MessageAssistantSource (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):
| # | Field | JSON name | Type | Value at start-flow | Value at reply |
|---|---|---|---|---|---|
| 1 | contactName | contactName | String | messageAssistantStartFlowArgs.getContactName() - the real contact name | flow.getNormalizedNumber() - i.e. the normalised phone number string, not a name |
| 2 | number | number | String | l7.b normalised digits-only number (C18312b.m34443d(3, number) then Character.isDigit filter) | flow.getNormalizedNumber() |
| 3 | messages | messages | List<Message> | emptyList() | last 50 messages of the conversation, newest-last |
| 4 | locale | locale | String | Configuration.getLocales().get(0).toLanguageTag() (API 24+) else locale.toLanguageTag() | same |
| 5 | messagesInSession | messagesInSession | Int | 0 | total number of messages in the session (before truncation) |
| 6 | userName | userName | String | AbstractC9039h.m10663t("") = SP key "username" | same |
RespondSmsBody.Message (nested data class, Moshi-generated adapter):
| Field | JSON name | Type | Meaning |
|---|---|---|---|
role | role | enum 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) |
message | message | String | the full SMS body text |
RespondSmsResponse - the response, a sealed interface with two implementations:
| Implementation | JSON shape | Meaning |
|---|---|---|
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:
| Field | JSON | Type |
|---|---|---|
originatingAddress | originatingAddress | String (from SmsMessage.getOriginatingAddress()) |
body | body | String (from SmsMessage.getMessageBody()) |
startedFlow | startedFlow | MessageAssistantFlow |
(container) messages | messages | List<IncomingSMSDTO> |
MessageAssistantFlow - the per-conversation state persisted locally:
| Field | Type | Meaning |
|---|---|---|
startMS | Long | epoch 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) |
normalizedNumber | String | the other party’s normalised number |
lastMessageMS | Long | epoch ms of the last message; 0 means “only the opening message” |
startType | String | DEFAULT / 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 formattingif (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 belowString 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 linex5.p.m38846c(...) (“startConversation”):
log "MessageAssistant", "startConversation, cli = %s"x5.s.m38849b(new MessageAssistantFlow(now - 10000, normalized, now, type), storage); // persistm38847d(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 answerif (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 byp511r0.C19813a(context, 22)), keySTARTED_FLOW_CONTACTS, value = a MoshiList<MessageAssistantFlow>JSON. m38848a(number, list)= find a flow whosenormalizedNumbermatches byl4.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 samenormalizedNumber, prepend the new flow, cap the list at 50, write JSON.m38850c(flow, storage)= remove only.- A static
ArrayList f57884bofx5.?observers is notified on every change; each observer re-queriesu7.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.
| Endpoint | Method | Body / params | Purpose |
|---|---|---|---|
https://voice2text.eyecon-app.com/init-ttm | POST 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-ttm | POST 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/ack | POST application/json | opaque JSON | acknowledge a transcribed message |
https://voice2text.eyecon-app.com/getText | GET | ?mycli=<authenticatedCli> | poll for new transcripts; result fed to u7.d |
https://voice2text.eyecon-app.com/ttm-call-forwarding-result | GET | ?mycli=<authenticatedCli> | check the test call-forwarding result |
https://voice2text.eyecon-app.com/ttm-get-tokens-amount | GET | ?mycli=<authenticatedCli> | remaining token balance |
https://voice2text.eyecon-app.com/no-ttm | POST 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. contactNamebeing 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
getTextto display a text version. It is paid for with token packages (purchases) and afreeTrialCode. 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 notestype == 1 -> "RecordedCalls" // call recordingsroot: 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 underDocuments/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 throwsIOExceptionare dropped).m31472f():SELECT * FROM recorded_notes ORDER BY <create_timestamp> DESC- the DB index.m31474h(diskList, dbList): sorts both, binary-search matches them, copies theUrifrom the disk entry onto the DB entry (so a re-index can hand a previously-file-backed row a MediaStoreUri), repairsnamefor type-1 rows whose name is empty/"empty name"by substitutingcli, and returns the merged list.m31467a(manager)(e5.d.a): requiresREAD+WRITE_EXTERNAL_STORAGE, thenm31475i()(mkdir),m31470d()(timestamp repair), the disk scan, the merge, theninsertany disk-only rows intorecorded_notesinside one transaction, and for type-1 rows whose name does not contain"__s__"enqueuesRunnableC16101cto export them somewhere (the share job).m31470d()(fixTimestampForDB): a one-shot migration for installs withSP_KEY_ORIGNALY_INSTALL_VERSION <= 385, guarded bySP_KEY_FIXED_RC_TIMESTAMP, that rewrites thefile_namecolumn (AbstractC0039a.f224h1).m31468b(entry)= delete: deletes the file (or MediaStore row) thenDELETE FROM recorded_notes WHERE <create_timestamp> = ?. Returns-2if the file delete failed.m31469c(entry)= update (is_played,is_saved,text_note, …).- First run posts
SP_KEY_IS_CALL_RECORDING_ACTIVITY_ENABLEand enqueuesRunnableC0070p0(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-> DriveappDataFolder), 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 throughInCallServiceonly works on OEMs that expose a call-audio source, which is consistent withrecording_calls_method_mode = -1(“auto”) and the vendor-specific permission dance inj7.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, butcall_direction(default-1, fixed to2147483647in 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:
DBContactsinit (line ~3640) with tag"app_started";p516r5.RunnableC19866kon Activity resume with tag"<name>-resume"/"<name>-resume2";bm.RunnableC3686q0case 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.jspif (!changedOut.isEmpty()) new RunnableC3686q0(changedOut, force, 12).run(); // -> addcontacts.jspSP: SP_KEY_IS_AFTER_RESTORE = falsefor (C16470g0 g : photoApproved) C21280h.m38431d([g[cli]], g[name], "yes", new String[1]); // -> picapproval.jspF.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@" socialSectionsocialSection := "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.jspboolean[] 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. initis a first-upload marker so the backend can distinguish “initial address-book dump” from “incremental change”; it is only sent whileSP_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_PHOTOis 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 rawhas_photobit. Confidence: high (the flag is read and immediately cleared in the same method).getupdatedcontactsinfo.jsppropagates changes made on the user’s other devices / the web client (viewers, subscribers, CanTalk state,vnversion) 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 nextApiRequestWorkerrun 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: readspurchase.optBoolean("acknowledged", true); if already acknowledged -> no-op; requirespurchase.getPurchaseState() == PURCHASED; readsoptString("token", optString("purchaseToken"))and callsacknowledgePurchase.m32522c(billing, ?, callback)=getAllPurchases:queryPurchasesAsync("inapp")andqueryPurchasesAsync("subs"), joined via a countdownAtomicInteger(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
- Native:
com.eyecon.global.Billing.Premium.AbstractActivityC5847areads Remote Configpremium_productsand pullsfree_premium/highlight_productkeys, theng5.i.m32534m(products, new C17507v(this,0))to bind the list.m10027E0-style helper on the WebView variant throwsPaywallException("product not found product = <id>")if a Play product has no matching paywall entry. - WebView:
WebviewPaywallActivity+C5835b(paywall config) +C5834a(a product group) +C5846m(WebViewClient).m10026D0()requires an intent extra named"placement_id"(FacebookMediationAdapter.RTB_PLACEMENT_PARAMETER) and throwsRuntimeException("missing placement_id")/RuntimeException("missing paywall")if the Remote Config JSON for that key is absent.C5835b.f11565ais the paywall URL;WebviewPaywallActivity.f11551V = Uri.parse(url).getHost().replace("www.","")and that host is then used as the allow-list domain inC5846m(host.equals(f11551V)orhost.endsWith("." + f11551V)), so navigation off the paywall origin is blocked (except an explicithttps://m.facebook.com/profile.phpcarve-out at line 111).- After
initProductsInfothe WebView isloadUrl(C5835b.f11565a). - Runtimerecord:
C5846m.m3503v-style sends areRunnableC21275c(11)etc. Purchase completion inside the page is relayed byC5844k, which handles the “pending purchase” toast (R.string.pending_purchace_toast).
- Registration interpose:
RegistrationActivity.m10529Q0showspaywall_premium_reg_get_startedonly when Remote Configregistration_premium_location == "get_started"and the local premium flag is not true. - Free premium (no purchase required):
j5.c(FreePremium) awards time-limited premium from three sources, each writingSP_KEY_LAST_FREE_PREMIUM_TYPEandSP_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).FreePremiumUserActivityis reachable from an external intent actionEYECON_PREMIUM_UPDATEand from the FCM notificationFree premium update(R.string.free_premium_noti_title/_msg, with[XXX]-templated inviter name).
- viral invite (
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 initialiserSP_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 serverif (f7.d0.f40868a["premium_cache_product"] == productId && 1 day + premium_cache_time >= now) return; // already freshf7.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
mysettingsblob is the tiebreaker when Play is unreachable or the cache is stale, andSP_KEY_PREMIUM_OWN_CACHEis a 30-day local mirror. Confidence: high. - Because
premium_cache_productis 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;acknowledgePurchaseis 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_TYPEso 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 1994this.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 listThere 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 portsocket.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...] : payloadMechanics (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]) anddata[16] = 2marks 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’sbyte[11..14]field before sending. - Retransmission: every outstanding packet is registered with a
RunnableC21943btimer at(f59241d * 50) + 200ms 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 isAudioTrack+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; // controlb[6] |= 0x80; // CONNECT flagc21952k.f59285A = new Random().nextInt(255); // session id byte 0c21952k.f59310w = new Random().nextInt(255) + 1; // first packet seqm35480z(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 type | Extra JSON fields | Meaning |
|---|---|---|
CHANNEL_INVITE | invited_clis: [...] | invite a set of contacts into a channel (m38607a) |
CHANNEL_JOIN | channel_id: Int, invitation_id: Int | accept an invitation (m38609d) |
INVITE_REJECT | channel_id, invitation_id, cli, type: Int | decline (m38610e) |
TALK_DONE | channel_id, session_id | end the session (m38612g) |
CHANNEL_BROADCAST_COMMAND | sound_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”, textR.string.toki_noti_text, tap intentTokiActivitywith actionINTENT_ACTION_REFOCUS_TOKI. onCreateregisters aSensorManagerlistener for sensor type 8 (TYPE_PROXIMITY) atSENSOR_DELAY_NORMAL, cachingmaximumRange.onSensorChanged: when the raw value< maximumRangethe phone is near the ear -> broadcastcom.eyecon.global.isonearwithisonear=true(edge-triggered,f13077clatch); when it goes back above ->isonear=false.onStartCommandreturns1(START_STICKY) only ifC22122v0.m39381H0()reports at least one live Toki session; otherwisestopSelf().onBindreturns a barep315h7.BinderC16946j()(no real AIDL surface).
H.4 OBSERVED - the PhoneAccount / contacts sync account is dead codeh6
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 BODYSo 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_serveris present on every contact row and is whatcli,getnames.jsp,pic?cli=andjoin.jspall use). - Toki’s channel/invitation rendezvous is push-driven: FCM carries the
channel_id/invitation_id/participantspayload, 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
TokiServiceis 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 cancelableScope (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 name | Local source | What it is |
|---|---|---|
eyecon_backup_for_database | context.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_records | files/calls/ (filtered C16100b(1)) | call recordings (AMR files whose names contain ..erc.) |
eyecon_backup_for_notes_records | files/notes/ (filtered C16100b(0)) | voice notes (..ern.) |
eyecon_backup_for_ttm_audios | files/ttm.zip | Talk-To-Me voicemail audio, zipped, uploaded with Content-Type: application/zip |
eyecon_backup_for_call_logs | files/call_logs/ | the archived system call log (CallLog.Calls export) |
eyecon_backup_for_address_book | files/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
- Phase 0 (parallel, 6 workers): create/move each of the 6
pending_temp_<name>Drive folders (C16418b0.m31906a(name)). - Phase 1 (parallel, 6 workers): prepare each artefact locally (
RunnableC16439wcases 1..5). - 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. - Phase 3 (
mo31914b, 95 %): upload each artefact into itspending_temp_<name>folder. - Phase 4 (96 %): delete the local staging files.
- 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());- 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
- Prepare local dirs: wipe/recreate
files/notes,files/calls; setf40747F = getDatabasePath("contactdb"),f40677j = databases/eyecon_restore_temp_database. GET https://www.googleapis.com/drive/v3/files?spaces=appDataFolder&fields=nextPageToken,files(id,name,size,parents)- paginated listing of the wholeappDataFolder.- If the account has no files ->
C16419c("Selected account has no files", 1). - 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 throwsC16419c("file name = <x>", 2). - Start 5 named threads:
BR_database_thread,BR_address_book_thread,BR_call_log_thread,BR_records_thread,BR_ttm_thread. BR_database_thread(mo31914b): open the restored DB with the 4-arg helperread itsnew p008a7.C0041b(MyApplication.f12163h, "eyecon_restore_temp_database", null, 57);historyrows newer than the local DB’s last event, rebuildhistoryrows for each, recomputecontacts.scoreviaDBContacts.m10109h(lastEvent, previousCallCount, previousScore) + 1.0, write them into the restored DB, then merge the SharedPreferences.- SP merge with an explicit denylist (OBSERVED,
C16434r.mo31914blines 553-624): read the restored DB’s SP (shared_preference_v2, falling back to the legacy serialisedshared_preference), then remove these keys before copying the current app’s map over the top:plusSP_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, HasAccessToHistorySP_KEY_SELECTED_THEME_BY_SETTINGS_DEFAULTandSP_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, andSP_KEY_STARTING_RESTORE_PROCESS,SP_KEY_NEED_SHOW_ON_BOARDING= false. - The atomic swap (OBSERVED,
p261e6.RunnableC16126bcase 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 failureRunnableC16111m(7, handler, RunnableC16428l)posts the completion callback 100 ms later; analyticsBackup restore success(orBackup restore failedwithHas 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 ownappDataFolderencryption-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
contactdbswap is not crash-safe across a process restart:contactdbis 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 leaveseyecon_temp_dbbehind; the code does not look for it on next launch. Confidence: medium. - Because the restore writes a
Java-serialised HashMapintoshared_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.
| # | Destination | Transport | Personal data sent | Trigger | Evidence |
|---|---|---|---|---|---|
| 1 | api.eyecon-app.com/app/join.jsp | TLS | phone 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 / rejoin | OBSERVED p365jd/RunnableC17614z case 1 |
| 2 | api.eyecon-app.com/app/cli_auth/gettransport | TLS | phone number, FCM push token, + the entire device-fingerprint set (see row 12), validation transport, last error | registration, validation retry | OBSERVED p365jd/RunnableC17614z default |
| 3 | api.eyecon-app.com/app/cli_auth/checkcode | TLS | phone number, 4-digit verification code, transport, social flag | registration | OBSERVED p516r5/RunnableC19875s case 1 |
| 4 | api.eyecon-app.com/app/social_auth/validatetoken | TLS | Google/Facebook OAuth token, provider, ANDROID_ID, advertising id, SIM & user country | social registration | OBSERVED p607vk/RunnableC21093i case 25 |
| 5 | api.eyecon-app.com/app/social_auth/use_cli_validation | TLS | phone number, ANDROID_ID, advertising id | bind a real number to an existing social account | OBSERVED p607vk/RunnableC21082e0 case 23 |
| 6 | api.eyecon-app.com/app/addcontacts.jsp | TLS | every 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 flag | app start, Activity resume, after restore | OBSERVED w6.h.m38428a + bm.q0.b |
| 7 | api.eyecon-app.com/app/removecontacts.jsp | TLS | number + name of deleted contacts | contact deleted | OBSERVED bm.s5.s |
| 8 | api.eyecon-app.com/app/getupdatedcontactsinfo.jsp | TLS | nothing outbound (GET); returns other-device changes. A response header sms triggers an SMS-inbox sync | server-sync cycle | OBSERVED vk.i case 26 |
| 9 | api.eyecon-app.com/app/resetupdatedstatus.jsp | TLS | nothing (version ping) | after applying a delta | OBSERVED vk.e0 case 25 |
| 10 | api.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 headers | every caller-ID lookup, person load, photo | OBSERVED w6.h.m38434g |
| 11 | api.eyecon-app.com/app/getnames.jsp | TLS | phone number, language preference, is_callerid, is_ic (is-incoming-call), source, HTTP-stack A/B tag | caller-ID, contact photo picker | OBSERVED RunnableC2598c case 6; v7.f |
| 12 | api.eyecon-app.com/app/updateuserdetails.jsp | TLS | Google advertising id, Wi-Fi MAC address, app version, username (display name), g_reg_id (FCM token), uploadp | join, name change, token refresh, MAC change | OBSERVED w6.c cases 2/3/4; r5.r case 12; vk.e0 case 24 |
| 13 | api.eyecon-app.com/app/uploadpic.jsp | TLS (multipart) | the user’s own profile photo (JPEG, quality 90) + msisdn = the account clientId | profile photo changed | OBSERVED w6.h.m38441n |
| 14 | api.eyecon-app.com/app/picapproval.jsp | TLS | comma-joined list of phone numbers whose photos the user approved + isapproved=yes | photo approval in bulk | OBSERVED u4.o case 5 |
| 15 | api.eyecon-app.com/app/spamreport | TLS (queued) | the reported/spammed number + payload JSON | user reports spam | OBSERVED v7.f |
| 16 | api.eyecon-app.com/app/spam?list_version= | TLS | account clientId only; returns the crowd-sourced spammer list ({cli,name}) | daily, Remote-Config-gated | OBSERVED DailyWorker |
| 17 | api.eyecon-app.com/app/getoptionalpics.jsp | TLS | clipics = comma-joined phone numbers | survey/photo picker | OBSERVED vk.e0 case 22 |
| 18 | api.eyecon-app.com/app/getfreshpicsjson.jsp | TLS | phone numbers | fresh-photo poll | OBSERVED RunnableC2598c lines 150/183 |
| 19 | api.eyecon-app.com/app/getnames.jsp / getcontactslang.jsp | TLS | nothing (locale only) | language refresh after a big contact upload | OBSERVED w6.h.m38428a |
| 20 | api.eyecon-app.com/app/sethistory.jsp | TLS | from = the other party’s phone number, time (epoch seconds), type (call type int) - per call event | after each call | OBSERVED vk.g0 |
| 21 | api.eyecon-app.com/app/eventreport.jsp | TLS (queued) | {name, type, attributes:{...}, timestamp} - every UI action: page views, permission dialogs, registration stage, spam reports, blocks, AI-assistant events, Joins, errors with stack traces | continuous | OBSERVED w6.h.m38436i + vk.i case 24 |
| 22 | api.eyecon-app.com/app/mysettings | TLS | server settings blob; on write: type/key/value (used for backup_info and for the premium entitlement) | app start, join, backup, purchase | OBSERVED w6.h.m38440m; w6.d case 8 |
| 23 | api.eyecon-app.com/app/get_store_products | TLS | account only; returns the store catalogue | store screen | OBSERVED w6.d case 6 |
| 24 | api.eyecon-app.com/app/cantalk.jsp / cantalksaw.jsp / cantalkreceived.jsp / getcantalkstatus.jsp | TLS | recvcli / notifiedcli = phone numbers; q/y/n answer | CanTalk feature | OBSERVED p5.RunnableC19300e |
| 25 | api.eyecon-app.com/app/virality/viral_id, /virality/viral_id_check, /virality/premium_status | TLS | account only; returns the inviter id and the premium-day budget | join, rejoin, premium | OBSERVED w6.d cases 2/4/9 |
| 26 | api.eyecon-app.com/app/notifyaddcontact.jsp | TLS | apsVersion, cv (and the new contact context) | local contact created | OBSERVED as.o |
| 27 | api.eyecon-app.com/app/grps/sgn | TLS (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 float | once after join, Remote-Config gated (sig_report_config.is_enable, cooldown cooldown_days_after_join) | OBSERVED r5.r case 11 |
| 28 | api.eyecon-app.com/app/deleteme.jsp | TLS | account | account deletion | OBSERVED v6.a |
| 29 | api.eyecon-app.com/app/messages/get | TLS | account; returns server messages | message polling | OBSERVED w6.d case 5 |
| 30 | api.eyecon-app.com/app/shorten-url | TLS | an arbitrary URL to shorten | share | OBSERVED vk.e0 case 26 |
| 31 | api.eyecon-app.com/app/share | TLS (queued) | {cli_list:[contact numbers shown on screen], type, content, provider} - the numbers of the contacts whose feed is being shared | social-media share | OBSERVED vk.i case 24 |
| 32 | api.eyecon-app.com/app/publish (pub/redirectme.jsp) | TLS | publisher attribution | install | DEX strings (no live call site found) |
| 33 | voice2text.eyecon-app.com/init-ttm | TLS | {mycli, purchases:[owned SKUs], freeTrialCode} | TTM setup | OBSERVED w6.b case 0 |
| 34 | voice2text.eyecon-app.com/start-ttm | TLS | {mycli, acli = the caller's number, calltag, purchases:[owned SKUs]} | TTM session start | OBSERVED u4.o case 6 |
| 35 | voice2text.eyecon-app.com/getText, /ack, /ttm-call-forwarding-result, /ttm-get-tokens-amount, /no-ttm | TLS | mycli (and acli for /no-ttm) | TTM polling | OBSERVED w6.d cases 1/3/7; w6.b; as.o |
| 36 | numberlookup.instabridge.com/respond-sms | CLR | the 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 count | every AI-assistant round trip | OBSERVED p636wo/C21474f:295, p646x5/InterfaceC21626a |
| 37 | cloudfront-cdn-all.eyecon-app.com/configs/ab-test.txt | CLR | nothing (a config fetch) | Remote Config load | OBSERVED 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 name | periodic worker, off by default | OBSERVED Analytics/EyeconAnalyticsWorker.m9990d/m9989c |
| 39 | ptt.eyecon-app.com | UDP, no TLS | the 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 CLIs | Toki session | OBSERVED p687yl/C21952k, p633wl/C21450m, p703z7/C22122v0 |
| 40 | googleapis.com Drive (appDataFolder) | TLS | the 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 map | user-initiated backup | OBSERVED p282f5/* |
| 41 | api.paperquotes.com/apiv1/quotes/ | TLS | Authorization: Token <R.string.quotes_api_id> (a hard-coded bearer) | after-call quotes | OBSERVED f7.j0.m32047b |
| 42 | translate.google.com/translate_a/single | TLS | MY NAME IS <contact name> - contact names to Google Translate | name normalisation | OBSERVED ic.RunnableC17219d1:590 |
| 43 | us1/eu1.locationiq.com, workers.degoo.com/location | TLS | the caller’s IP (implied) / the device IP for a lat-long gate | caller-area lookup | OBSERVED BACKEND-API.md §1 |
| 44 | api2.amplitude.com, api.eu.amplitude.com | SDK | full session/event stream, including session replay | continuous | Amplitude SDK |
| 45 | Firebase (Crashlytics / Remote Config / Performance / FCM / Sessions / In-App Messaging) | SDK | crash reports (with the app’s own tagged logs), HTTP timings via FirebasePerfUrlConnection/FirebasePerfOkHttpClient, device identifiers, FCM token | continuous | OBSERVED in w6.a / m7.p |
| 46 | firebaseremoteconfigrealtime.googleapis.com | SDK | (fetch only) | - | manifest |
| 47 | Singular (join.eyecon-app.com branded domain, cleartext) | SDK | install attribution, deep links | install | OBSERVED p671y4/C21845b:72 |
| 48 | Ad 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) | SDK | advertising id, IP, coarse location, installed-app signals, ad-interaction telemetry | ad fill | manifest + SDK classes |
| 49 | the-news-desk.com, s3.amazonaws.com/embed.footylight.com, hb.crackle.co.in, docs.google.com, www.google.com | TLS / inline script | ad-tag scripts injected into app WebViews (user_id=[advertisingId], publisher_key, sub_id=[placementId]); the welcome-back survey form | ad fill / survey | OBSERVED in d5.d defaults mobitech_*_html_script, welcome_back_notification_url |
| 50 | www.facebook.com / m.facebook.com / graph.facebook.com / www.instagram.com / www.tiktok.com | TLS | outbound 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/share | social-media share | OBSERVED vk.i default branch, w7.o |
Summary of the highest-sensitivity flows (INFERRED, high confidence):
- Complete SMS conversation text to a cleartext third-party host (
numberlookup.instabridge.com) whenever the AI assistant is used. - The complete address book (names + numbers + every linked social-account identifier) to
api.eyecon-app.comon essentially every app start. - A hardware-level device fingerprint (IMEI + Android ID + Wi-Fi MAC + advertising id) on
join.jsp,cli_auth/gettransport, andupdateuserdetails.jsp. - 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. - Live microphone audio to
ptt.eyecon-app.comover unencrypted UDP. - 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 ananalytics_worker_reload_jsonrewind (from_date) that can re-upload older history.
4. NOTEWORTHY / SURPRISING FINDINGSh4
- The account credential is server-issued, not device-derived.
clientId- which becomes thee-authheader on every request, themsisdnonuploadpic.jsp, and thecidin the encrypted signature report - is the response body ofjoin.jsp(and ofsocial_auth/use_cli_validation). This resolves the open question inBACKEND-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. - 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 tovoice2text.eyecon-app.comover HTTPS. The assistant’s own module lives undercom.eyecon.global.DefaultDialer.assistantand its DTOs are Moshi-generated Kotlin - i.e. it is a relatively recent, separately-developed module grafted into the olderm4/-era codebase. contactNameis 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.PhoneAccount.SyncServiceregisters a contactsSyncAdapterwhoseonPerformSyncbody is completely empty. The app ships a fullAccountManageraccount type + authenticator +RawContactsstructure for an Eyecon contacts account and then never uses it. All contact sync is the ad-hocaddcontacts.jsppath. (EyeconRoomDBis similarly dead code - seeDATA-LAYER.md§3.1.)- Backups are raw database-file copies. Restoring swaps
contactdbat the filesystem level withrenameTo, 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. EyeconCallScreeningService.stopService()is overridden to returnfalse, and the screening response is deliberately delayed by 2 seconds so the platform’s own timeout cannot fire. This is aggressive device-ownership behaviour.AppOpsManager-based SEND_SMS probing for Xiaomi inw6.h.m38430c, plus per-OEM permission-routing dialogs (j7.t), aSTICKYCallServicerestarted byAlarmManager.setAndAllowWhileIdleevery 5 s, and aProximity-sensor foreground service - this app is heavily engineered around Chinese OEM ROMs and aggressive battery management.- 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-authcredentials, over an unencrypted socket toptt.eyecon-app.comon a port randomly drawn from Remote Configtoki_ports(default[5620, 50000-50010]). Live microphone audio crosses the network in the clear. grps/sgnuploads the app’s own APK signing-certificate hashes, RSA-encrypted with a public key embedded inres/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: thesignaturesarray is the app’s own certificate, not the list of installed apps.)- The AI assistant reads the SMS database through
content://sms/sentandcontent://sms/inboxwithaddress LIKE '%<number>%'anddate > <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. - The spam model has four
spam_typevalues but only two are server-produced. Types3/4are local-only user reports, explicitly preserved across a full list resync (DailyWorker.m10355bdrops everything else before merging the server list) - the client trusts its own reports over the crowd list. Token IDsand 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
| Area | Confidence | Why |
|---|---|---|
| Module -> obfuscated-class mapping | High | Every 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 parameters | High | Read directly out of ArrayList<C21872a> construction sites and the URL-concatenation expressions. |
@z@ / @a@ payload grammars | High | Both the encoder (bm.q0.b) and the decoder (vk.i case 26) were read; they round-trip. |
| DB tables per module | High | Cross-checked against DATA-LAYER.md (schema and raw-SQL inventory). |
| Spam decision logic (0/1/2/-1) | High | Read from the JADX register listing, which is unambiguous control flow. |
| AI-assistant DTO field names and types | High | Each 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) | High | Read from the JADX register listing of x5.n. |
contactName = number on follow-ups | Medium-high | The 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 machine | High | cli_auth/gettransport, cli_auth/checkcode and join.jsp all read end-to-end, including response-header handling. |
| Toki UDP protocol | High for the framing/mechanics, Medium for the semantics | The 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 set | High | The six folder names are a literal array, and each is matched to a local directory in the upload and restore code. |
| Call-recording capture mechanism | Low-Medium | The 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 enumerated | Ad/analytics SDK payloads are documented as classes, not dissected. |
5.2 Open questions static analysis could NOT answerh5
- How is a phone call actually recorded? There is exactly one
AudioRecordinstantiation in the whole tree and it belongs to the Toki stack (v7.f,AudioRecord(MIC, 8000, 16, MONO, 16000)). NoMediaRecorderanywhere. The most likely explanation is that recording is delegated to the platform/OEM viaInCallService+Call.disconnect/Call.respondtiming on specific vendor ROMs (therecording_calls_method_modeRemote Config key andfree_rec_calls_actionssupport this), but the actual capture call was not recovered. This is the single biggest gap in this document. - 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 perjadx-export.log) or gated behind an unexercised branch. - What is
SP_KEY_AUDIO_RECORD_PROPERTIES_JSON_V4? It appears in the backup denylist and inDATA-LAYER.md’s key list but its writer/reader were not traced. Presumably per-recording codec parameters. - Server-side response schemas. Only request shapes are observable. The
join.jspresponse headers (pi,sd,is_new) andcli_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. E-Mark/CIDresponse headers on 401. A device-binding/attestation channel exists (w6.a.m38420d) but its semantics need server-side or runtime evidence. Unchanged fromBACKEND-API.md§9.- Whether
e-auth-kis enforced server-side at all. It ships in the APK, is the compiled-in default of Remote Configcircus_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. 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_thresholdthrottling) but the URL is assembled from Remote Config and the concrete request was not found.apps.eyecon-app.com/call-contest/a/report-callslikewise appears only as a Remote Config default (call_contest_report_calls_url) with no traced call site.SP_KEY_INIT_ADD_CONTACTS_COUNTwriter. I found the decrement (inbm.q0case 12) but not the setter, so the initial burst sizeNis unknown. Consequently the claim thatinit=1marks “the first N uploads” is medium-confidence.- Voice2Text auth.
start-ttmsends acalltagwith&access_token=<PhotosTrackerWorker.m10433e()>stripped, andinit-ttm/ackare called withaddAutoHeaders = trueso they inherit thee-authquartet - but it is unclear whether voice2text validates thee-authcredentials or a separate token. - The MoE
"padding"fields in the social sub-record.bm.q0.bappends a trailing"0@a@"after every social id, andvk.i’s decoder iteratesi += 4over acount * 2 + 4window. 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 readsis_subfrom whereverilands). 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.- Whether
daily_default_dialer_role_otheranalytics reveal competitor dialers.DailyWorkerrecordsrole_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. - 392 JADX decompilation errors (
jadx-export.log) mean some methods are stubs. Notablyp646x5.C21639n.invokeSuspend(the AI reply coroutine) and large parts ofandroidx/fragment/app/RunnableC2598care 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 ofp008a7.AbstractC0039a(e.g.f216f -> "cli_in_server",f219g -> "name",f177O1 -> "SP_TELL_SERVER_CONTACT_HAS_NO_PHOTO"). Any table reference written asAbstractC0039a.fNNNin 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
RemoteConfigdefault map inp236d5.AbstractC15804dis the single most valuable artefact for understanding this app: ~200 keys that switch features on and off server-side, includingcircus_key(thee-auth-kvalue),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:
| library | size | what it does |
|---|---|---|
libnms.so | 299,528 | anti-fraud routines, XOR string decryptor, 34+ recovered strings |
libpglarmor.so | 68,600 | cypher4 cipher, /dev/urandom key path, time-seeded permutation |
libtobEmbedPagEncrypt.so | 8,304 | software 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:
| Seg | Type | file offset | vaddr | filesz | memsz | flags |
|---|---|---|---|---|---|---|
| 1 | LOAD | 0x0 | 0x0 | 0x3F6B0 | 0x3F6B0 | R-X |
| 2 | LOAD | 0x3F6B0 | 0x436B0 | 0x8698 | 0x8698 | RW- |
| 3 | LOAD | 0x47D50 | 0x4FD50 | 0xC08 | 0x2184 | RW- |
Real vaddr → file offset: v < 0x3F6B0 maps 1<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_INITJNI_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
| Group | Imports | Inference |
|---|---|---|
| Device properties | __system_property_find / _read / _get | Android build/device property harvesting |
| Proc/file probing | stat, lstat, fopen, fread, fgets, sscanf, fdopen, feof, fclose, fseek, ftell | Reads and parses /proc/* text — TracerPid, maps, status |
| Timing | clock_gettime, time, log10, pow, getpagesize | Timing-attack detection, page-size (emulator/VM) fingerprinting |
| Threading | pthread_self | Thread identity |
| String/mem | memcmp, strstr, strnlen, strlen, memmove, memset, memcpy, malloc, calloc, free | Pattern matching, allocation |
| Stdio/parse | vsnprintf, __vsprintf_chk, __strlen_chk, atoi, strtoll | Formatting and numeric parsing |
| Hardening | __stack_chk_fail, __cxa_finalize, __cxa_atexit, __errno | Stack 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— 1FUN_0011689c— 2FUN_001185c0— 1FUN_0011ed38— 2FUN_0011efc4— 3FUN_0011f3c8— 5FUN_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/suThe 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" needleReads /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 lineGated 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:
libnms.sois a ByteDance Pangle anti-fraud / environment-integrity library, built with ByteDance’s internal security NDK fork (git.byted.org:security/android-ndk-llvm.git).- It performs no cryptography and no network I/O — 40 imports, none of either kind.
- It is a five-technique tamper-detection suite, all of whose strings were recovered:
- 24-path
subinary scan (root) /proc/self/mountsMagisk fingerprinting (magisk,core/mirror,core/img)/proc/self/attr/prevSELinux context check againstu:r:zygote:s0(injection)persist.vmos.root.enable == "1"(VMOS virtual device)/proc/self/mapsparse + readable-mapping range analysis (injected modules / patching)
- 24-path
- All strings are runtime-decrypted into
.bssby a repeating-key XOR (FUN_00115174, 30+ call sites) fed byR_AARCH64_RELATIVEaddends — fully reversible statically, as demonstrated, without executing anything. - Deliberate obfuscation: one functional export, dynamic
RegisterNatives, opaque-predicate control flow, one-time-init.bssflags.
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
RegisterNativesmethod 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_00115174with Frida, loggingsrc/dst/keyper call. - Dump
.bss(0x50960–0x51ED4real /0x150960–0x151ED3Ghidra) 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:171C15537aw.m30739b(context, "nms");// → com/pgl/ssdk/C15537aw.java:68System.loadLibrary(str); // str == "nms"It is also registered for native crash monitoring:
// com/bytedance/sdk/openadsdk/ApmHelper.java:154monitorCrashInitSDK.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:
| Selector | Argument | Meaning |
|---|---|---|
| 101 | "1" | version/protocol marker |
| 102 | this.f37985h | internal handle |
| 114 | Integer (field f37986i) | mode flag |
| 105 | StringBuilder result | assembled string |
| 152, 153 | C15516ab.m30635i/m30630d(ctx) | device identifiers |
| 106–110 | C15516ab.m30632f/m30631e/m30628c/m30629d/m30627c(ctx) | further device fingerprint values |
| 115 | this.f37987j | internal handle |
| 103, 104, 111 | String | per-check inputs |
| 165 | set.contains("boot") | SIM present |
| 166 | set.contains("mnc") | Mobile Network Code |
| 167 | set.contains("mcc") | Mobile Country Code |
| 168 | set.contains("gaid") | Google Advertising ID |
| 159 | C15516ab.m30670a(...) | device string |
| 224 | new 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 ByteDanceThe 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\(declaresghidraVersion=11.3.2against 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
| Seg | file off | vaddr | filesz | flags |
|---|---|---|---|---|
| 1 | 0x0 | 0x0 | 0x13… | R-X |
| 2 | 0x… | 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 001032d4Java_..._PglCryptUtils_bc 0010353cNote 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 memsettime srand rand__open_2 __errno __read_chk closeDT_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 .bsswhile (__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 bytesclose(__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/urandomOBSERVED: 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 XORFUN_00104040(&sbox_state, out, out, (uVar11 % 5) * 4 + 0x10); // round transformFUN_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 indicestime(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:
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.- Its key is read from
/dev/urandom(path recovered by decrypting the XOR-obfuscated string) — sound randomness. - It operates on 64-bit blocks with byte-XOR keystream and bit-manipulation round transforms.
- Its output permutation is derived from
srand(time(NULL))+ 12rand()calls — predictable to anyone who knows the timestamp window. - Single JNI export
bcwith opcodes1010/1011; no_INIT_0, key read per operation. - 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)andbc(1011)are encrypt and decrypt, or two variants. - The meaning of error codes 501–506 surfaced by
PangleEncryptUtilsType4.getCryptFailedReason.
8. Cross-referencesh4
| Document | Relationship |
|---|---|
NATIVE-libnms.md | Sibling ByteDance security library; same XOR-decryptor technique, same NDK fork, but anti-fraud rather than crypto |
NATIVE-libtobEmbedPagEncrypt.md | The fallback cipher — confirmed AES-128; this library is cypher4, the current one |
cluster-C5-media-social-extra.md | Pangle 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
| Seg | file off | vaddr | filesz | memsz | flags |
|---|---|---|---|---|---|
| 1 | 0x0 | 0x0 | 0x13E0 | 0x13E0 | R-X |
| 2 | 0x13E0 | 0x53E0 | 0x250 | 0x250 | RW- |
| 3 | 0x1630 | 0x9630 | 0x200 | 0x214 | RW- |
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 OBJECTsbox1 0x9730 size 256 OBJECTss_encrypted_size 0x0AD0 size 116 FUNCinitKey 0x1018 size 88 FUNCss_decrypt 0x0EA0 size 376 FUNCss_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 c0b7 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 7509 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 cb54 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.ntohlin the log tag implies host/network byte-order handling somewhere in the path.
4. Java wiringh4
static { System.loadLibrary("tobEmbedPagEncrypt"); }
public static byte[] encrypt(byte[] b, int n) { return ttEncrypt(b, n); } // swallows Throwablepublic 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:
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:
| Path | Implementation | Library |
|---|---|---|
| default | PangleEncryptUtils (native) | libtobEmbedPagEncrypt.so — the AES above |
Type4 | PglCryptUtils.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_sizecomputes a padded output length, which implies PKCS#7 padding and therefore CBC or padded ECB — but that is inference, not proof. Settling it requires decompilinginitKeyandss_encryptin 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 decryptorconst OFF_JOL = 0x15614; // JNI_OnLoadconst OFF_SYS = 0x3D5D8; // FUN_0013d5d8 syscall wrapper (su-path probe)const BSS_LO = 0x50960; // .bss startconst 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
| File | Purpose |
|---|---|
libnms.so | The library, arm64-v8a, SHA-256 prefix 63DA06A1E2C518C9 |
libnms-dump.js | Frida script — hooks the decryptor, JNI_OnLoad, and RegisterNatives |
libtobEmbedPagEncrypt.so | Next target if libnms.so is exhausted (8 KB, fully reversible) |
libpglarmor.so, libbuffer_pgl.so, libfile_lock_pgl.so | Lower priority, extracted for completeness |
libnms-phdr.csv | ELF program headers parsed during analysis |
What the script doesh5
- Waits for
libnms.so— installs immediately if already loaded, otherwise hooksandroid_dlopen_extand retries every 500 ms for 60 s. - Hooks
FUN_00115174(offset0x15174) — the repeating-key XOR decryptor. It readssrc/keyinx0/x2, reproduces the cipher in JS, and logs each distinct(src, dst, keylen)triple with the plaintext. - Hooks
JNI_OnLoad(offset0x15614) — callsGetEnv(VM, &env, JNI_VERSION_1_6), then attaches to theRegisterNativesslot in theJNINativeInterfacetable (index 215) so it can dump{name, signature, fnPtr}for every dynamically registered method, converting eachfnPtrto a module-relative offset. - Sweeps
.bss(0x50960–0x51ED3) 1.5 s afterJNI_OnLoadreturns, 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:
| Ghidra | real/module offset | symbol |
|---|---|---|
0x00115174 | 0x15174 | FUN_00115174 decryptor |
0x00115614 | 0x15614 | JNI_OnLoad |
0x0013d5d8 | 0x3D5D8 | FUN_0013d5d8 syscall wrapper |
0x00150960 | 0x50960 | .bss start |
0x00151ED3 | 0x51ED4 | .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 (
seg1offset 0x0 / vaddr 0x0;seg2offset 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 emulatorpip install frida-tools # frida + frida-ps + frida-trace on the hostadb push frida-server /data/local/tmpadb shell "su -c 'chmod 755 /data/local/tmp/frida-server'"adb shell "su -c '/data/local/tmp/frida-server &'" # or: adb forwardfrida-ps -Uai | grep com.eyecon.globalRunh5
Spawn and instrument from launch, so JNI_OnLoad is caught:
frida -U -f com.eyecon.global -l libnms-dump.js --no-pauseOr attach to an already-running process:
frida -U -n com.eyecon.global -l libnms-dump.jsSave output:
frida -U -f com.eyecon.global -l libnms-dump.js --no-pause > libnms-runtime.txt 2>&1Expected 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
supaths fromFUN_0011f88c(/system/bin/sutemp,/system/xbin/bstk/su, …) r,/proc/self/mounts,magisk,core/mirror,core/imgfromFUN_0011f3c8/proc/self/attr/prev,u:r:zygote:s0fromFUN_0011efc4persist.vmos.root.enable,1fromFUN_0011ed38%lx-%lx %4sfromFUN_0011689c5256fromJNI_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. readCStringis called once per hook invocation and is bounded to 128 bytes.- The script never writes to the target process —
Interceptor.attachonly 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
.javafiles, 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 inventorydocs/DATA-LAYER.md— fullcontactdbschema, migrations, key listsdocs/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 packagea7). Every entry below states the real identity recovered from/* JADX INFO: renamed from: <pkg>.<cls> */.
Evidence convention used throughout:
| Tag | Meaning |
|---|---|
| OBSERVED | Directly read in the decompiled source, resource file or manifest. Line/file references given. |
| INFERRED | Reasoned 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 claim | Verdict | Evidence |
|---|---|---|---|
| 1 | p620w6\C21273a (real w6.a) = HTTP core, TAG "NetworkHelper" | CONFIRMED | p620w6/C21273a.java:40 private static final String TAG = "NetworkHelper"; |
| 2 | rc_key = "PgdtSBeR0MumR7fO" | CONFIRMED | C21273a.java:42 private static final String rc_key = "PgdtSBeR0MumR7fO"; — note the modifier really is private static final |
| 3 | m38417a = Ktor path | CONFIRMED, and corrected | It 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) |
| 4 | m38423g = HttpURLConnection path | CONFIRMED | C21273a.java:149-209; opens URL.openConnection() wrapped in FirebasePerfUrlConnection.instrument(...) |
| 5 | m38427k applies e-auth-v, e-auth, e-auth-c, e-auth-k | CONFIRMED | C21273a.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 |
| 6 | m38421e = checksum = sum of Character.getNumericValue of the FIRST char of each dash-separated CID segment, returned as a decimal string | CONFIRMED, verbatim | C21273a.java:164-176 |
| 7 | m38420d = 401 / E-Mark / CID handling | CONFIRMED | C21273a.java:113-138 |
| 8 | p620w6\C21280h (real w6.h) = “Server” facade with addcontacts.jsp, join.jsp, uploadpic.jsp, getcontactslang.jsp, pic?cli= | CONFIRMED | C21280h.java:99-108 (uploadpic.jsp, join.jsp), :123-124 (addcontacts.jsp, getcontactslang.jsp), :106 TAG = "Server" (thread pool name at :100) |
| 9 | p008a7\AbstractC0039a (real a7.a) holds https://api.eyecon-app.com/app/ in f168L1 plus ~150 DB column constants | CONFIRMED | a7/C0041b… → AbstractC0039a.java:602 f168L1 = "https://api.eyecon-app.com/app/";. Exactly 148 C16458a0 column constants + 4 String constants |
| 10 | p284f7\* (real f7.*) = custom SharedPreferences + emulator detector | CONFIRMED, refined | f7.y (prefs) + f7.x (editor) + f7.q (emulator fingerprint data). The emulator logic lives in two other places — see §8.3 and §19.6 |
| 11 | App prefs are backed by a shared_preference_v2 table in contactdb, NOT Android SharedPreferences | CONFIRMED, and refined | a7.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 |
| 12 | clientId 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 |
| 13 | p032b5\AbstractC3464e.m3668s() overwrites the static final rc_key by reflection from RC key circus_key | CONFIRMED | p032b5/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.com | CONFIRMED | d5.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_key | CORRECTED — it cannot | d5.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 dir | Real pkg | Files | KB | Owning module | First-party? | One-line responsibility |
|---|---|---|---|---|---|---|
p008a7 | a7 | 50 | 510.5 | Others | yes | Global constants (Constants), SQLiteOpenHelper for contactdb, view/dialog/WebView/toast/notification helpers, app-instance UUID, app-wide startup lambdas |
p284f7 | f7 | 42 | 214.0 | Others | yes | Custom SharedPreferences implementation + Editor, interned DB column-name wrapper, ContentValues builder, emulator-fingerprint tables, backup-payload crypto, quotes loader, locale override |
p260e5 | e5 | 27 | 222.7 | Others | yes | Call-recorder data layer (RecordedNote) + the full emulator detector |
p420m7 | m7 | 36 | 198.9 | Others | yes | Contact-photo storage (Blowfish-encrypted bitmap cache), Google-Images scraper (JS-injection WebViews), quotes/inspiration, bitmap utilities, online image loader |
p340i7 | i7 | 39 | 130.5 | Others | yes | App-owned custom-View / Drawable / RecyclerView-adapter widget library |
p674y7 | y7 | 15 | 83.2 | Others | yes | Custom themes & dynamic-area (“Menifa” cards): theme model, font enum, apply/preview flow |
p274eo | eo | 4 | 59.5 | Others | yes | DB-backed persistence for shared_preference_v2 (read/write/migrate) + RxJava/Tasks adapter lambdas |
p005a4 | a4 | 8 | 155.6 | Others | mixed | Prefs load/migration engine, cleartext RC HTTP fetch, analytics dual-write, Digital Turbine Ignite attribution auth component |
p647x6 | x6 | 5 | 22.0 | Network | yes | ApiRequestObj persistence/retry queue (table_api_requests) wrapping the three w6.a transports |
p305g7 | g7 | 1 | 0.7 | Others | yes | Abstract BroadcastReceiver that defers work to goAsync() on the shared pool |
gf | gf | 13 | 62.1 | Others | (Google Play Core) | In-App Update (com.google.android.play.core.appupdate.*) — service binding, listener registry, install status |
p394l | l | 16 | 26.7 | Others | (Glide) | Glide image-loading internals (BitmapDrawableResource, ByteBufferInputStream, DataCacheWriter, MIME set) |
p466oq | oq | 47 | 186.8 | Analytics | (OkHttp 4.x) | OkHttp Kotlin client/builder/Request/ResponseBody/ConnectionSpec/CertificatePinner — this is the transport w6.a uses |
p506qo | qo | 38 | 248.6 | Analytics | mixed | Kotlin stdlib collections + a large app-level utility object (qo.r) holding crypto-verification, Ignite version resolution and the “A/B enabled” gate |
p486pq | pq | 5 | 5377.0 | Analytics | (okhttp3) | okhttp3.internal helpers (byte utils, BOM tables, version string, thread factory) |
p429n4 | n4 | 4 | 248.3 | Others | (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):
| Field | Value | Meaning |
|---|---|---|
f168L1 (L1) | https://api.eyecon-app.com/app/ | API base URL; every .jsp endpoint is built from it |
f171M1 (M1) | -1 | default int |
f174N1 (N1) | START_ACTIVITY_ABOVE_SCREEN_LOCKED | flag for launching UI over the keyguard |
f177O1 (O1) | SP_TELL_SERVER_CONTACT_HAS_NO_PHOTO | pref/broadcast key |
f180P1 (P1) | sp_isCalenderAvailable | pref key (note the misspelling of “Calendar”) |
f201a (a) | <sdcard>/.Eyecon/.nomedia | sentinel that hides the app’s folder from the gallery |
f204b (b) | <sdcard>/.Eyecon/temp_photo.jpg | temp 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:
| Signature | Purpose |
|---|---|
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.
| Signature | Purpose |
|---|---|
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, m318h | misc |
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:
| Check | Values |
|---|---|
Build.FINGERPRINT startsWith | generic, unknown |
Build.MODEL contains | google_sdk, sdk_gphone, Emulator, Android SDK built |
Build.MANUFACTURER contains | Genymotion |
Build.BRAND startsWith generic and Build.DEVICE startsWith generic | |
Build.PRODUCT equals | google_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)andd5.d.m31093n(String)— both returnnull/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).
| Signature | Purpose |
|---|---|
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 / m417B0 | Toast helpers |
m421F0(...) / m433R0 / m434S0 / m435T0 | NotificationCompat.Builder factories + notification builders |
m427L0(File,File,FragmentActivity,C18734w) | share a file pair via FileProvider + EXTRA_STREAM |
m422G0/m423H0/m424I0 | ValueAnimator / legacy Animation factories |
m429N0(FragmentActivity)→C18399k | ActivityResultLauncher registry for sharing |
m418C0/m419D0/m430O0/m432Q0/m440Y0 | permission / 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 exposesgetBooleanRemoteConfig,getDoubleRemoteConfig,getIntRemoteConfig,getLongRemoteConfig,getStringRemoteConfig,logEvent,setUserPropertyto 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?
m425J0is 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 (seenotSupportedKeys/EyeInAppMessagingListV2/in_app_updateind5.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))thenm328r(file, …)a7.s0(C0076s0):234—m32019n(stackTraceOf(throwable))f5.f(AbstractC16422f):171,175— error text sent to Google Drive and Eyecon backupRegistrationActivity: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.
| case | Behaviour — OBSERVED |
|---|---|
| 0-3 | synthetic/no-op or image scaling |
| 4 | registers a ContentObserver on CallLog.CONTENT_URI (needs READ_CALL_LOG) via C19858g + HandlerThread/Looper.loop() |
| 5 | Analytics 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 |
| 6 | b5.e.m3664o() |
| 7 | Looper.prepare(); b5.e.f2334a.m32776h(); Looper.loop(); — the “Events” worker thread body; b5.e.m3666q() starts new Thread(new RunnableC0070p0(7)) |
| 8 | runs every Runnable in d5.d.f38508b (the RC post-fetch callback list, set by RegistrationActivity:3735-3741/3885 and q7.RunnableC19693v) |
| 9 | PremiumFeaturesTest.updateFeaturesMap(AbstractC15804d.m31091l("premium_features", false)) |
| 10 | d5.d.m31080a(null) = Firebase Remote Config fetchAndActivate(), then reschedules itself every 7200000L ms (2 h) (RunnableC0070p0.java:334-337) |
| 11 | c6.d.m4095c(false) (Kotlin/Hilt-ish init) |
| 12 | posts case 11 onto h7.i main queue |
| 13 | m409b() — a second content-observer/C16102d (recorder) initialisation guarded by C16102d.f39635c |
| 14-17 | no-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 file | Real | Kind | Purpose / key members |
|---|---|---|---|
AbstractC0042b0 | a7.b0 | util | General 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) |
AbstractC0046d0 | a7.d0 | util | m182A/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() |
AbstractC0048e0 | a7.e0 | synthetic | ShortcutInfo.Builder construction helpers: m206d(Activity) (id "my-shortcut"), m207e(Context,String), m214l(Object), m220r() |
AbstractC0049f | a7.f | synthetic | m238f(String,String)→NotificationChannel(...,IMPORTANCE_LOW=3), m240h/m241i/m243k casts |
AbstractC0062l0 | a7.l0 | util | Density/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) |
AbstractC0068o0 | a7.o0 | synthetic | inlined DialogInterface.OnClickListener/view-listener holder (d,e,l,n,o,y) |
AbstractC0080u0 | a7.u0 | marker | public static final int f364a = 0 — Kotlin Unit singleton; decompiled as an int. Used as a marker in ~30 call sites for readability |
AbstractC0082v0 | a7.v0 | marker | public static final int f370a = 0 + static { Pattern.compile("\\+*0*([0-9]{6,})"); } — the phone-number normalisation regex (strip + and leading zeros, require ≥6 digits) |
AnimationAnimationListenerC0040a0 | a7.a0 | listener | onAnimationStart/End/Repeat |
C0043c | a7.c | data | public final Throwable f283a; equals/hashCode/toString — a Failure holder |
C0045d | a7.d | data | public final Object f285a; equals/hashCode/toString — a Pair holder |
C0050f0 | a7.f0 | lambda | MediaPlayer.OnCompletionListener |
C0052g0 | a7.g0 | mega iface impl | Implements 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 |
C0053h | a7.h | listener | ValueAnimator.OnUpdateListener over 4 floats; finalize() |
C0054h0 | a7.h0 | listener | Application.ActivityLifecycleCallbacks + m289a(Intent) |
C0055i | a7.i | listener | Animator.AnimatorListener |
C0056i0 | a7.i0 | task | AbstractC16130b impl: mo308k()(onError) / mo309m()(onSuccess) / mo310p()(background result) |
C0057j | a7.j | listener | ValueAnimator.OnUpdateListener over 2 ints |
C0059k | a7.k | listener | ValueAnimator.OnUpdateListener over float + 2 ints |
C0067o | a7.o | JS bridge | @JavascriptInterface–free close(); bound as "EyeconWebView" |
C0069p | a7.p | WebViewClient | Full WebViewClient incl. shouldInterceptRequest (both overloads), shouldOverrideUrlLoading (both), onReceivedSslError (logs only — does not proceed), onReceivedHttpAuthRequest, onReceivedLoginRequest, onSafeBrowsingHit, onReceivedClientCertRequest. Fields f341d, f342e |
C0072q0 | a7.q0 | Lottie | sizeOf(Object,Object)→int (LottieImageAsset); the two instances are MyApplication.f12166k/f12167l |
C0075s | a7.s | data | bare extends HashMap |
C0076s0 | a7.s0 | task | AbstractC16130b impl that resolves a contact name (mo721c0 reads a7.a.f219g = name from the DB) and encrypts diagnostics via m32019n |
C0078t0 | a7.t0 | listener | Application.ActivityLifecycleCallbacks with f360a |
C0079u | a7.u | data | f362b/f363c + compare(Object,Object)→int (sort holder for a7.x.m458i0) |
DialogInterfaceOnClickListenerC0051g | a7.g | listener | dialog button click |
DialogInterfaceOnKeyListenerC0071q | a7.q | listener | dialog key handling |
InterfaceC0047e | a7.e | interface | marker |
InterfaceC0086x0 | a7.x0 | interface | void mo475i(String str) — consumed by a7.w0.m447e |
KeyguardManagerKeyguardDismissCallbackC0044c0 | a7.c0 | callback | onDismissSucceeded/Cancelled/Error |
RunnableC0058j0 | a7.j0 | lambda | no-arg run() |
RunnableC0061l | a7.l | lambda | bitmap scale → ImageView on the UI thread (f318d target, f316b/f317c px) |
RunnableC0064m0 | a7.m0 | lambda | startup worker; AbstractC3464e.m3666q() call site (:158) — one-shot via AbstractC0066n0.f339f |
RunnableC0065n | a7.n | lambda | run() |
RunnableC0074r0 | a7.r0 | lambda | see §3.8 |
RunnableC0081v | a7.v | lambda | drag-and-drop toast animation (f366b…f369e) |
RunnableC0087y | a7.y | lambda | run() |
RunnableC0088z | a7.z | lambda | run() |
ViewOnClickListenerC0073r | a7.r | listener | dialog close-button click |
ViewOnTouchListenerC0083w | a7.w | listener | onTouch drag handling |
ViewTreeObserverOnPreDrawListenerC0063m | a7.m | listener | onPreDraw() — 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):
| Field | Meaning |
|---|---|
public static File f41006e | context.getFilesDir(), captured in the constructor. Base dir for the legacy *.eyecon.properties migration files |
public static final Object f41005d | the global lock guarding the in-memory map |
public static HashMap f41007f | the 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 f41008g | the write-back task (new C16303b(2)) |
public static int f41009h | write-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 f41012c | HashSet of pending keys |
Constructor — OBSERVED (:51-55):
f41006e = context.getFilesDir();m32086e(context, true); // kick off the async loadContextCompat.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):
| Signature | Notes |
|---|---|
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/edit | standard overrides, all final, all routed through m32082b |
registerOnSharedPreferenceChangeListener / unregister… | throw new UnsupportedOperationException("UNIMPLEMENTED_METHOD") — the API is deliberately not implemented |
m32086e + m32084f | the load/wait pair |
Security/robustness notes:
- No encryption. Values (including the
clientIdcredential,clientToken,SP_KEY_app_instance_id, prefcipher_key) are stored in plaintext in thecontactdbSQLite file. See §21-F2. m32084f()is a silent 3-second timeout: if the async load has not finished, everyget*silently returns the caller’s default. INFERRED (high confidence): a slow/large DB load can therefore cause the app to readclientId == nulland take error paths (e.g.w6.athrowingRuntimeException("Error missing CID")).getAll()returns a shallownew HashMap(f41007f); mutable collections in it are shared with the live map. INFERRED (medium confidence): a caller mutating a returnedSetmutates the store.getStringSetdefensively copies;getAlldoes 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 entireshared_preference_v2table (DELETE FROM shared_preference_v2+ re-insert of every key) — seeeo.b.m31713A(§5.2). There is no incremental write path. OBSERVED. apply()swallows write failures: on exception it callscb.m31515h()(onError) and returns;commit()propagates via theboolean[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:
| Signature | Delegates 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.
| Field | Value — OBSERVED |
|---|---|
f40972e (e) — emulator MSISDNs | 15555215554, 15555215556, 15555215558, 15555215560, 15555215562, 15555215564, 15555215566, 15555215568, 15555215570, 15555215572, 15555215574, 15555215576, 15555215578, 15555215580, 15555215582, 15555215584 (the Android-emulator +155552155xx range) |
f40973f (f) — emulator IMEI/MEID | 000000000000000, e21833235b6eef10, 012345678912345 |
f40974g (g) — emulator IMSI | 310260000000000 |
f40975h (h) — Genymotion sockets | /dev/socket/genyd, /dev/socket/baseband_genyd |
f40976i (i) — goldfish token | goldfish |
f40977j (j) — QEMU sockets | /dev/socket/qemud, /dev/qemu_pipe |
f40978k (k) — x86/VBox/ttVM init files | ueventd.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 files | fstab.andy, ueventd.andy.rc |
f40980m (m) — Nox files | fstab.nox, init.nox.rc, ueventd.nox.rc |
f40982a (a) — 15 C9118x system-property probes | init.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) |
f40984c | has-telephony flag |
f40985d | ArrayList of emulator package names — populated in the ctor: com.google.android.launcher.layouts.genymotion, com.bluestacks, com.bignox.app |
f40981n | unused static singleton slot |
m32070a(String[] paths)→boolean | true 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):
- 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. - Blowfish/ECB, no IV.
Cipher.getInstance("Blowfish")defaults toECBwith 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). - 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.
- RSA/ECB/PKCS1Padding = PKCS#1 v1.5. Not OAEP. INFERRED (medium-high): classic Bleichenbacher padding-oracle exposure if the server returns distinguishable decryption errors.
- 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 keypk_enc_2exists (strings.xml:1238) and is used byp516r5\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 file | Real | Kind | Purpose / key members |
|---|---|---|---|
AbstractC16490r | f7.r | WebViewClient | abstract base with public static long f40986b/f40987c counters, public final String f40988a, abstract mo407a(), and onRenderProcessGone → boolean (crash-tolerant WebView) |
C16460b0 | f7.b0 | data | TreeSet f40862a + int f40863b = 1 (an ordered unique-id set) |
C16461c | f7.c | listener | TextWatcher (f40864a flag) feeding f7.e |
C16462c0 | f7.c0 | util | Locale 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()) |
C16465e | f7.e | custom View | auto-sizing/expanding TextView (m31985b(TextView,int,int), m31986c(...)→float, m31987a/m31988d/m31989e/m31990f) |
C16466e0 | f7.e0 | util | initialValue() → new char[0]-backed ThreadLocal |
C16474i0 | f7.i0 | data | quotes item: f40904g (C16945i), f40905h (lock), f40906a/f40907b (strings), f40908c="", f40909d (Bitmap), f40910e (Runnable), f40911f; m32043a/m32044b/m32045c, toString() |
C16476j0 | f7.j0 | service | ”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 |
C16478k0 | f7.k0 | Drawable | rounded-rect Drawable (f40925a RectF, f40926b…f40929e flags, f40930f radius, f40931g Paint, f40932h stroke, f40933i ColorStateList, f40934j color) |
C16479l | f7.l | marker | empty class body |
C16481m | f7.m | data + JS bridge | calls-contest statistics, bound as "EyeconContest": getContactsCallsCount(), getNonContactsCallsCount(), getWithinEyeconContactsCallsCount(), getWithinEyeconNonContactsCallsCount(), getStartDate(), getEndDate(), getMyStartDate(), isOptIn() |
C16482m0 | f7.m0 | lambda | MediaPlayer.OnCompletionListener |
C16483n | f7.n | service | ”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) |
C16485o | f7.o | task | AbstractC16130b impl (mo415j/mo308k/mo309m) |
C16487p | f7.p | util | LinearSmoothScroller (calculateSpeedPerPixel, calculateTimeForScrolling, getVerticalSnapPreference, updateActionForInterimTarget) |
C16488p0 | f7.p0 | util | WebView 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 |
C16491s | f7.s | data | sparse-array multimap: f40989a/f40990b (SparseArray), m32071c(), m32072d(int)→Comparator, m32073e(int)→ArrayList, plus full sparse-array + Collection API |
C16495w | f7.w | task | the 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 |
C16498z | f7.z | synthetic lambda | in-app-update snackbar: watches Play Core InstallState (INSTALL_STATE_DOWNLOADED → “update downloaded / restart” snackbar coloured R.color.light_main_color; INSTALLED → unregister listener) |
InterfaceC16463d | f7.d | interface | marker |
InterpolatorC16467f | f7.f | util | getInterpolation(float) |
RunnableC16469g | f7.g | lambda | run() |
RunnableC16471h | f7.h | lambda | run() |
RunnableC16472h0 | f7.h0 | lambda | run() |
RunnableC16473i | f7.i | lambda | run() |
RunnableC16475j | f7.j | lambda | run() — also hosts f7.n’s async init |
RunnableC16477k | f7.k | lambda | run() |
RunnableC16480l0 | f7.l0 | lambda | run() throws Throwable (16.3 KB of inlined sites) |
RunnableC16486o0 | f7.o0 | listener | Application.ActivityLifecycleCallbacks with f40956a (Handler), f40957b/f40958c (bools), m32067a(boolean) |
RunnableC16492t | f7.t | lambda | run() |
RunnableC16493u | f7.u | lambda | run() |
RunnableC16494v | f7.v | lambda | run() |
ViewOnLayoutChangeListenerC16459b | f7.b | listener | onLayoutChange(...) 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:
- 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. Stringvalues 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--;).- Builds
ContentValuesper chunk:key(a7.a.f141C1),data_type(a7.a.f232k0),data_part(a7.a.f235l0, the chunk index),data(literal). helper.m160D(5000L)→getWritableDatabase()inside a transaction, thenDELETE FROM shared_preference_v2(no WHERE clause — the whole table).rawQuery("SELECT 1 FROM shared_preference_v2 LIMIT 1"); if a row survives it throwsEyeconException("After delete the table is not empty!").- 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 — ifMyApplication.f12170o(main process) — sleeps 1000 ms, resolves an internalTaskCompletionSource, sleeps 3000 ms, thenSystem.out.println("!!! killProcess killApp, System.exit(0) by eyecon !!!")andSystem.exit(0). - On
SQLiteDatabaseCorruptException | SQLiteFullExceptionspecifically (:468-478): additionally calls((ActivityManager) …getSystemService("activity")).clearApplicationUserData()and logs"EyeconTools5 / clearApplicationCache source = EyeconSharedPreference".
Security / robustness implications:
clearApplicationUserData()wipes all app data — DBs, files, prefs — which includes theclientIdcredential. The device is therefore forced into a full re-registration. Anyone able to makecontactdbhitSQLITE_FULL(e.g. by filling the device’s free space) triggers a remote-relevant data-loss event. Severity: HIGH (see §21-F3).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.- The process-kill loop at
:490-495has 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). - Logs the raw
keyanddataon 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-opif (MyApplication.m10348j().f41011b) return false; // in-flight migration guardm31713A(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
| Real | Disk | Kind | Purpose |
|---|---|---|---|
eo.a | C16302a | extends 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.c | C16304c | extends AbstractC19747n | terminal: mo31729a(EnumC21167b.f56149a); onSuccess(Boolean.FALSE); |
eo.d | C16305d | extends AtomicReference implements InterfaceC19748o, InterfaceC20176b | RxJava 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 usinga7.acolumns: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 usingurl,json_key,server_retry_count,db_column_uuid.public static AbstractC21650c m38853b(int retryCount, String json, String uuid, String endpoint)— the deserialiser. ReadsurlStr,typeRequest,isCameFromReg,requestMethod,jsonExpectedResult,endPoint,dataName,contentType,requestJsonBody,addAutoHeaders,gzipEnable,timeout(default 5000),responseCode[],params[],headerValuesMap,headerResult, then constructs the matching subclass. (Note thecontentType;key with a stray semicolon atm38853b— a cosmetic bug; the value is still readable ascontentTypeviaoptStringon a different key.)public final void m38854c(AbstractC21650c)— re-enqueue for replay onf57894b.
6.3 x6.d, x6.e, x6.f — the three transports (OBSERVED)h5
| Real | Disk | Transport called | Notes |
|---|---|---|---|
x6.d | C21651d | w6.a.m38424h(ArrayList, String url, int[] code, String endpoint, String rid) — gzip POST | Preflight AbstractC9039h.m10665v() (clientId present) else returns -1. Logs tag "GzipApiRequest". Serialises params as [{name,value},…] |
x6.e | C21652e | w6.a.m38425i(int[], boolean addAuth, String body, String dataName, String url, String endpoint, String contentType, String method, String rid) — form/JSON POST | Logs tag "JsonApiRequest". Fields f57905i (requestJsonBody), f57906j (dataName), f57907k (contentType) |
x6.f | C21653f | w6.a.m38423g(ArrayList, String url, String method, int[], boolean jsonExpected, String endpoint, HashMap headers, boolean addAuth, boolean gzip, HashMap respHeaders, String rid) — general | Logs 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 ishttp://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 noe-auth-*headers are attached and there is no request authentication or integrity protection at all. - The response is
JSONObject-parsed and handed tod5.c.m31073a(...), which merges it into the live A/B config (see §19.4). - Transport:
w6.a.m38423g→URL.openConnection(). Withhttp://the platformHttpURLConnectionis used directly; there is no certificate to validate. AndroidManifest.xml:272android:usesCleartextTraffic="true"andres/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
| case | Behaviour |
|---|---|
| 0 | Ignite: AbstractC19779r.m36265a(new JSONObject(c0033e.f112j.version())) — IgniteAuthenticationComponent / resolveIgniteServiceVersion, wrapped in C16698b.m32479b("%s: resolveIgniteServiceVersion : unable to resolve version : %s", …) on failure |
| 1 | Firebase Crashlytics AbstractC9039h.m10644a(false, exc) (suppressed for IOException) |
| 2,3 | bitmap scale-into-ImageSwitcher |
| 4 | startForegroundService(ForegroundLauncherService) with INTENT_KEY_ACTION_FLS; on failure logs "EyeconTools5, startServiceOrForeground FINAL RESULT = FAILED for action = …" |
| 5 | Drawable.createFromStream(file) then AbstractC16130b.m31518o(...) |
| 6,7 | AbstractC3458o (Mintegral/Pangle player) obtainMessage(111/110/107, …) |
| 9 | FCM topic subscription toggle — compares pref "eyecon.TOPIC_SUBSCRIPTION-" against the desired state, then FirebaseMessaging.onSuccessTask(new C2082a(topic, 3|4)) |
| 10 | b5.e.m3650a(property, value) — Firebase setUserProperty + Singular + AppsFlyer-style |
| 12 | DummyActivity 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 |
| 16 | WebView 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. |
| 19 | Ad-size test: resizes the native-ad container to AbstractC0062l0.m337F1(adHeightResPx) dp and propagates the new layout up to R.id.RL_base_da |
| 21 | Lottie animation from a FileInputStream |
| 22 | ”read more” ellipsis expansion on a TextView |
| 24 | C15854y (data-entry dialog) — disables the view, runs RunnableC0031c(12) |
| 27 | Recorder 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>:
eo.b.m31717e(C0041b.m156t())— readshared_preference_v2.eo.b.m31718g(C0041b.m156t())— read the v1 legacyshared_preferenceBLOB (Java-deserialised; see §5.1.3).- Legacy Android
SharedPreferences(PreferenceManager.getDefaultSharedPreferences(ctx)):- skip if
OLD_VERSION_COPY_TO_NEWis already true; - skip if
getAll()is null/empty; - skip if the map has no
"clientId"entry; - otherwise, if
eo.b.m31727z(map)succeeds, setdefaultSharedPreferences.edit().putBoolean("OLD_VERSION_COPY_TO_NEW", true).apply().
- skip if
- Legacy Java-serialised property files in
context.getFilesDir():main.eyecon.propertiesthensecondary.eyecon.properties. Read withObjectInputStream.readObject(); if the result is aHashMapuse it, otherwiseeo.b.m31714C((byte[]) object)(gzip+deserialize). (Note: the decompiled control flow around these two files is tangled by JADX — ther10reassignments at:154-168are 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 initialisedb5.e.f2336c.f13184a.m10682e(value, amb, b5.e.m3656g(eventName)); // FB App EventsAbstractC21888a.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
| Real | Disk | Kind | Purpose |
|---|---|---|---|
a4.a | InterfaceServiceConnectionC0029a | interface … extends ServiceConnection, p259e4.InterfaceC16097b | The 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.f | AbstractC0034f | abstract class … implements a4.a | Delegate/decorator: registers itself via f120a.mo77a(this); mo81b(this);, forwards every method, and routes events to InterfaceC16096a (onIgniteServiceAuthenticated/AuthenticationFailed/ConnectionFailed/Connected) |
a4.g | C0035g | final class … extends a4.f | A 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.e | C0033e | final class … implements a4.a | The 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
| Stage | Technique — OBSERVED |
|---|---|
| 1 | Build 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 |
| 2 | Build.SERIAL / Build.getSerial() (:213-228, gated on SDK_INT >= 26 and READ_PHONE_STATE): lower-cased contains nox |
| 3 | Generic-brand/device pair (:237-242) and PRODUCT == "google_sdk" |
| 4 | Telephony (: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") |
| 5 | Filesystem (: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) |
| 7 | Hidden-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 |
| 8 | Subprocess (: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 |
| 9 | Installed 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 validatedAbstractC16130b 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 NUMBERtask.m32066g(Boolean.TRUE, "isEmulator");C21280h.m38439l(task, EnumC19659g1.IS_CODE_VALID, EnumC19653e1.CLIENT_EMULATOR);task.m31515h(); // fail the validation callbackC21280h.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
| Real | Disk | Kind | Purpose / key members |
|---|---|---|---|
e5.a | AbstractC16098a | util | SAF 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.a0 | C16099a0 | adapter | ViewPager.Adapter (getCount, getPageTitle, instantiateItem, destroyItem, isViewFromObject, getItemPosition) + m31463a(C16099a0,Bitmap,View), m31465c(View,C18712a0,int,C18736y), m31466d(String,View,RunnableC3850i); fields f39625i/f39626j |
e5.b | C16100b | util | FileFilter — accept(File,String)→boolean (extension filter) |
e5.c | RunnableC16101c | lambda | run() |
e5.e | EnumC16103e | enum | recording filter/sort mode; f39638a int; m31478a()→C11859p builds the {android_version, manufacturer, device, mode} object consumed by a7.r0 |
e5.g | ViewOnTouchListenerC16105g | listener | onTouch(View,MotionEvent); field f39656b |
e5.h | ViewOnClickListenerC16106h | listener | onClick |
e5.i | ViewOnClickListenerC16107i | listener | onClick |
e5.j | RunnableC16108j | lambda | run() |
e5.k | RunnableC16109k | lambda | run() |
e5.l | C16110l | task | AbstractC16130b impl (mo415j/mo308k/mo309m) |
e5.n | C16112n | ItemDecoration | getItemOffsets(Rect,View,RecyclerView,State) |
e5.o | C16113o | ItemDecoration | getItemOffsets(...) |
e5.p | C16114p | ItemDecoration | getItemOffsets(...) |
e5.q | C16115q | ViewPager.OnPageChangeListener | onPageSelected, onPageScrollStateChanged, onPageScrolled; f42676a |
e5.r | C16116r | lambda | MediaPlayer.OnCompletionListener |
e5.s | RunnableC16117s | lambda | run() |
e5.t | C16118t | listener | Animator.AnimatorListener; f39685a int |
e5.u | ViewOnClickListenerC16119u | listener | onClick |
e5.v | ViewOnClickListenerC16120v | listener | onClick |
e5.w | ViewOnClickListenerC16121w | listener | onClick; f39693a = true |
e5.x | C16122x | ViewHolder/task | recordings 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.y | C16123y | adapter | RecyclerView.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.z | C16124z | Fragment | mo4090i0(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"whena7.b0.m178a0()(external storage mounted) elsegetFilesDir() + "/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); // discardedc18517d.f46765a = Cipher.getInstance("Blowfish");f46765a.init(DECRYPT_MODE, key);Defects (OBSERVED):
- Keyspace = 1000. The key is
"Ewu"+ an integer in[0,999]+"sTQHGK". Both literals are in the APK (res/values/strings.xml:272cipher_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). java.util.Random, notSecureRandom—nextInt(1000)is predictable; seeded fromSystem.nanoTime()on most devices, so the sequence is recoverable.- Blowfish/ECB, no IV — deterministic, structurally leaking, 64-bit blocks.
- 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.decodeByteArrayThe 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 keyString 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 defaultMozilla/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 withsearch_url(https://www.google.com/search?hl=en&udm=2&q=),max_links(75),links_script(JS that walksdocument.getElementsByTagName("a"), reads each anchor’s<img>childsrc/data-src, assignsresult.webview_tag,result.small/result.data, pushes intosearch_resultsand returnsJSON.stringify(search_results)), andbig_url_script(JS that clicks[ITEM_ID], waits, then regex-extracts theimgurl=query parameter from the resultinggoogle_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 off46820gand completes it with an arbitrary JS-supplied string, plus a no-optest(String).m34726e(String host)validates the host against an allow-list (AbstractC19504b.m35900b(AbstractC15999a.m31378d(0,0,host,7,false))— URL canonicalisationsha256hex) and throwsIllegalArgumentException("unexpected host: …")otherwise, andm34728g/m34729hthrowIllegalStateException("Invalid URL host: …")on malformed links. So there is host validation — but the allowed hosts are effectively whatever the RC-suppliedsearch_urlpoints 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
| Real | Disk | Kind | Purpose / key members |
|---|---|---|---|
m7.a | C18511a | Layout | Layout 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.b | C18513b | data | String f46745a + compareTo — a sortable string holder |
m7.c | AbstractC18515c | util | public static final C16945i f46749a = new C16945i(1,1,"PhotosPath",true); f46750b (static ArrayList) |
m7.c0 | C18516c0 | Fragment | photo 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.e | C18519e | WebViewClient | mo407a(), onLoadResource, onPageFinished, onReceivedHttpError, shouldOverrideUrlLoading(String)→boolean |
m7.f | C18521f | marker | empty class body |
m7.g | C18523g | lambda | onReceiveValue(Object) |
m7.g0 | C18524g0 | Fragment | zoomable image viewer: f42780i (Bitmap), f42781j (C19862i), f42782k (ZoomImageView), f42783l; mo4090i0, onCreateView, onDestroy, onPause |
m7.h | C18525h | WebView host | multi-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.i | C18527i | data | f46795a/f46796b/f46797c; getBody()→String, getFid()→String, getHeaders()→String — a prepared HTTP request descriptor |
m7.j | C18529j | lambda | onReceiveValue(Object); f46808a |
m7.j0 | C18530j0 | listener | GestureDetector.OnDoubleTapListener + onFling/onLongPress/onScroll/onSingleTapConfirmed |
m7.k | C18531k | JS bridge ("Android") | @JavascriptInterface onBigLink(String,String,String), @JavascriptInterface test(String) |
m7.l | C18533l | scraper | see §9.3 |
m7.l0 | C18534l0 | listener | ScaleGestureDetector.OnScaleGestureListener |
m7.m | C18535m | util | a 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.m0 | EnumC18536m0 | enum | 5 constants f46836a…f46840e — image-scale/format modes |
m7.n0 | C18538n0 | data | 3 floats + ImageView.ScaleType f46846d — a zoom/scale triple |
m7.o | InterfaceC18539o | interface | image-load completion callback (used by m7.p) |
m7.p | C18540p | image loader | f46847c (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.q | InterfaceC18541q | interface | marker |
m7.s | AbstractC18543s | util | bitmap 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.x | C18548x | task | AbstractC16130b impl (mo415j/mo308k/mo309m) |
m7.y | C18549y | DialogFragment | theme/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.a0 | RunnableC18512a0 | lambda | run() |
m7.b0 | RunnableC18514b0 | lambda | run() |
m7.e0 | RunnableC18520e0 | lambda | run() |
m7.f0 | GestureDetectorOnDoubleTapListenerC18522f0 | listener | onDoubleTap, onDoubleTapEvent, onSingleTapConfirmed |
m7.i0 | RunnableC18528i0 | lambda | zoom/pan animator; fields f46798a(long), f46799b…f46803f, AccelerateDecelerateInterpolator f46804g, f46805h/f46806i (PointF) |
m7.k0 | InterfaceC18532k0 | interface | marker |
m7.n | RunnableC18537n | lambda | run() |
m7.r | RunnableC18542r | lambda | run() |
m7.z | RunnableC18550z | lambda | run() |
m7.d0 | ViewOnLayoutChangeListenerC18518d0 | listener | onLayoutChange(...) |
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.
| Real | Disk | Kind | Purpose / key members |
|---|---|---|---|
i7.a | InterfaceC17157a | interface | onLoadSuccess-style callback (mo31519h()→Boolean) |
i7.a0 | C17158a0 | listener | CompoundButton.OnCheckedChangeListener |
i7.b | C17159b | listener | Animator.AnimatorListener |
i7.c | InterfaceC17161c | interface | h-shaped callback |
i7.d | C17163d | util | startScroll(int,int,int,int,int) override; f42660a |
i7.d0 | C17164d0 | layout inflater singleton | f42661d (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.e | C17165e | util | Canvas f42665a, Bitmap f42666b scratch holder |
i7.e0 | InterfaceC17166e0 | interface | dispatchTouchEvent observer |
i7.f | GestureDetectorOnGestureListenerC17167f | listener | full OnGestureListener |
i7.f0 | C17168f0 | ViewGroup | dispatchTouchEvent(MotionEvent)→boolean override + setDispatchTouchEventListener; f42668a |
i7.g | EnumC17169g | enum | 4 ints f42673a…f42676d (drawable states); m33014a(int)→ColorStateList |
i7.g0 | C17170g0 | OutlineProvider | getOutline(View,Outline) |
i7.h0 | C17172h0 | OutlineProvider | getOutline(View,Outline) |
i7.i | InterfaceC17173i | interface | mo…i(String) |
i7.j | EnumC17175j | enum | single constant f42688a |
i7.j0 | AbstractC17176j0 | view util | 21 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.k | C17177k | Drawable | draw(Canvas), getOpacity, setAlpha, setColorFilter; f42690g(int), f42691a(Paint), f42692b…f42694d(ints), f42695e(Canvas), f42696f(Bitmap) |
i7.k0 | ViewTreeObserverOnGlobalLayoutListenerC17178k0 | listener | onGlobalLayout() |
i7.l | EnumC17179l | enum | single constant f42700a |
i7.m0 | C17182m0 | util | f42709a (ArrayList); m33037b(View,Runnable), m33038c(View,Runnable)→RunnableC17180l0, m33039a(), mo3712f0(View,Runnable) |
i7.n | ViewOnLongClickListenerC17183n | listener | onLongClick(View)→boolean |
i7.n0 | InterfaceC17184n0 | interface | f0 callback |
i7.o | ViewOnClickListenerC17185o | listener | onClick |
i7.p | ViewOnClickListenerC17186p | listener | onClick |
i7.q | C17187q | OutlineProvider | getOutline; f42717b |
i7.s | C17189s | TextWatcher | beforeTextChanged/onTextChanged/afterTextChanged |
i7.t | AbstractC17190t | abstract | mo33050a/b/c/d(String), mo33053d(), mo33055f(), mo33056g(String) |
i7.u | C17191u | data | 4 ints f42723a…f42726d + equals/hashCode/toString |
i7.v | C17192v | data | 9 ints f42727a…f42735i + equals/hashCode/toString |
i7.w | C17193w | ColorStateList | getOpacity, isStateful, onStateChange(int[]); f42736a(ColorStateList), f42737b(int), f42738c(int) |
i7.x | RunnableC17194x | lambda | run() |
i7.y | C17195y | listener | TextView.OnEditorActionListener |
i7.z | InterfaceC17196z | interface | 5 methods |
i7.b0 | RunnableC17160b0 | lambda | run() |
i7.c0 | RunnableC17162c0 | lambda | run() |
i7.h | RunnableC17171h | lambda | run() |
i7.i0 | RunnableC17174i0 | lambda | run(); f42684b flag |
i7.l0 | RunnableC17180l0 | lambda | run() — 18.1 KB, also hosts the m7.d photo-read entry point (am interop) |
i7.r | AnimationAnimationListenerC17188r | listener | onAnimationStart/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.
| Real | Disk | Kind | Purpose / key members |
|---|---|---|---|
y7.i | EnumC21881i | enum | font theme enum: f59003a (name), f59004b (int), f59005c (Typeface, lazily resolved in m39120b() under synchronized) |
y7.j | EnumC21882j | enum | theme variant enum: 8 ints f59011a…f59018f incl. two booleans f59017g/f59018h |
y7.k | InterfaceC21883k | interface | a(String) |
y7.l | C21884l | theme manager singleton | f59019g (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.m | C21885m | task | AbstractC16130b impl (mo308k/mo309m) |
y7.c / y7.f | C21875c / C21878f | task | two more AbstractC16130b impls with identical shape (mo308k/mo309m) |
y7.o | C21887o | Fragment | theme 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.a | RunnableC21873a | lambda | run() |
y7.b | ViewOnClickListenerC21874b | listener | onClick |
y7.d | RunnableC21876d | lambda | run(); f58983b |
y7.e | RunnableC21877e | lambda | run() — also used by b5.e.m3650a to fan out user-property writes (AbstractC3464e.m3650a → C22071c.m39333d(new RunnableC21877e(…,3,str2))) |
y7.g | RunnableC21879g | lambda | run() |
y7.h | RunnableC21880h | lambda | run() |
y7.n | RunnableC21886n | lambda | run() (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".
| Real | Disk | Kind | Purpose / key members |
|---|---|---|---|
gf.b | InterfaceC16732b | interface | empty marker (implemented by gf.f) |
gf.c | AbstractC16733c | AppUpdateManager (35 KB) | The manager: requestUpdateInfo, startUpdateFlowForResult, completeUpdate, listener registry, and the app’s version string via AbstractC18646q.m34958V0(…, "Client") |
gf.a | C16731a | data | InstallStatus: f41565a (updateAvailability), f41566b/f41567c (PendingIntents), f41568d (already-started flag), m32579a(C16743m)→PendingIntent |
gf.d | C16734d | AppUpdateListenerRegistry | tag "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.e | C16735e | factory | zza() → either new C16734d(ctx) (listener registry) or new C16742l(ctx) (storage policy) |
gf.f | C16736f | AppUpdateManager impl | m32612a(C16731a,int,Activity)→boolean → startIntentSenderForResult(…, 104, …); m32613b(C16498z) unregister |
gf.g | C16737g | async task | requestUpdateInfo(...), completeUpdate(...), and the gms-task onComplete path |
gf.h | AbstractBinderC16738h | AIDL stub | IAppUpdateServiceCallback; mo32615d(Bundle) (onRequestInfo), zzb(Bundle) (onCompleteUpdate) |
gf.i | BinderC16739i | AIDL stub | OnCompleteUpdateCallback — maps error.code != 0 → InstallException |
gf.j | BinderC16740j | AIDL stub | OnRequestInstallCallback — 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.k | C16741k | AppUpdateService | f41588e 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.l | C16742l | storage policy | m32618a(File)→long recursive size of <filesDir>/assetpacks |
gf.m | C16743m | data | AppUpdateOptions 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
| Real | Disk | Kind | Purpose |
|---|---|---|---|
g7.a | AbstractC16716a | abstract class … extends BroadcastReceiver | goAsync() 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.
| Real | Disk | Glide role |
|---|---|---|
l.a | C18253a | Resource subclass (extends AbstractC22312g) |
l.b | AbstractC18254b | empty synthetic marker |
l.c | C18255c | Resource wrapper / ResponseHeader (extends er.AbstractC16369r), close() + read(C16354h,long) with exception capture |
l.d | C18256d | Transition/RequestOptions int pair |
l.e | C18257e | RequestBuilder coroutine continuation (invokeSuspend) |
l.f | C18258f | PreloadTarget/RequestBuilder coroutine body |
l.g | AbstractC18259g | $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.h | C18260h | BitmapDrawableResource - f45632a (BitmapDrawable) + f45633b (boolean), with equals/hashCode |
l.i | C18261i | DownsampleStrategy/PreloadOptions data class (f45634c singleton) |
l.j | C18262j | ByteBufferInputStream extends InputStream — mark/reset over a ByteBuffer (f45637a mode, f45638b = mark position, default 1073741824) |
l.k | AbstractC18263k | public static final Paint f45640a = new Paint(3) — the Glide “tag” paint used by RequestBuilder |
l.l | AbstractC18264l | the mime-type Set |
l.m | C18265m | RealResource extends AbstractC18266n — close() (closes f45647f and f45645d), mo34401d(), mo34402h() |
l.n | AbstractC18266n | abstract class … implements Closeable — mo34401d()→AbstractC22312g, mo34402h()→InterfaceC16358j |
l.o | C18267o | PooledByteBufferInputStream extends AbstractC22312g (f45648a) |
l.p | C18268p | DataCacheWriter 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.a | Class |
|---|---|
| OkHttpClient instance | oq.c0 (C19227c0) |
OkHttpClient builder/config | oq.b0 (C19225b0) |
Request builder (m35533a(k,v) = addHeader) | oq.e0 (C19231e0) |
| Request | oq.f0 (C19233f0) |
Response (FirebasePerfOkHttpClient.execute result) | oq.o0 (C19251o0) |
| MediaType | oq.y (C19268y) |
| Dispatcher/executor | oq.u (C19262u) |
Full inventory:
| Real | Disk | OkHttp role |
|---|---|---|
oq.a | C19222a | OkClient.Builder (10 fields) |
oq.a0 | C19223a0 | Builder extends oq.m0 |
oq.b | C19224b | Address implements oq.v0 |
oq.b0 | C19225b0 | OkHttpClient.Builder/config (owns the X509TrustManager, lines 12 & 83) |
oq.c | C19226c | ResponseBody (extends oq.s0) |
oq.c0 | C19227c0 | OkHttpClient — 15 hits on X509TrustManager/SSLSocketFactory (platform trust-manager selection, standard OkHttp) |
oq.d | C19228d | Connection |
oq.d0 | EnumC19229d0 | internal enum |
oq.e | C19230e | Headers/builder-adjacent helper (extends er.AbstractC16368q) |
oq.e0 | C19231e0 | Request.Builder (m35533a) |
oq.f | C19232f | RequestBody implements Closeable, Flushable |
oq.f0 | C19233f0 | Request |
oq.g | C19234g | Route |
oq.g0 | C19235g0 | Builder extends oq.m0 |
oq.h | InterfaceC19236h | Call.Factory/Call interface |
oq.h0 | C19237h0 | Builder extends oq.m0 |
oq.i | InterfaceC19238i | Cloneable interface |
oq.i0 | C19239i0 | EMPTY ByteString |
oq.j | InterfaceC19240j | interface |
oq.j0 | C19241j0 | Builder extends oq.m0 |
oq.k | C19242k | Authenticator |
oq.k0 | C19243k0 | Builder extends oq.m0 |
oq.l | C19244l | Comparator |
oq.l0 | C19245l0 | Companion |
oq.m | C19246m | Interceptor.Chain |
oq.m0 | AbstractC19247m0 | ByteString base (Companion, EMPTY) |
oq.n0 | C19249n0 | RealConnection/pool node |
oq.o | C19250o | EventListener |
oq.o0 | C19251o0 | Response (implements Closeable) |
oq.p | C19252p | CertificatePinner (plain final class, 10.7 KB) |
oq.p0 | C19253p0 | Reader subclass (challenge/auth reader) |
oq.q | C19254q | RouteException |
oq.q0 | C19255q0 | ResponseBody subclass |
oq.r | C19256r | EventListener.Factory |
oq.r0 | C19257r0 | Cache entry |
oq.s | C19258s | Builder extends oq.m0 |
oq.s0 | AbstractC19259s0 | ResponseBody implements Closeable |
oq.t | C19260t | Dns |
oq.t0 | C19261t0 | ConnectionSpec-adjacent |
oq.u | C19262u | Dispatcher implements Iterable |
oq.u0 | EnumC19263u0 | internal enum |
oq.v | C19264v | ConnectionSpec (TLS-version / cipher-suite helper) |
oq.v0 | InterfaceC19265v0 | q1 interface (address contract) |
oq.w | InterfaceC19266w | interface |
oq.x | InterfaceC19267x | interface |
oq.y | C19268y | MediaType |
oq.z | C19269z | Handshake |
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:
| Method | Purpose |
|---|---|
public static final boolean m36274e0() | return !AbstractC0084w0.m450h(); — the “A/B tests enabled” gate (emulator -> false) |
m36261X0(PublicKey, String data, String signature)→boolean | RSA signature verification |
m36264Z(String x509)→PublicKey | KeyFactory.getInstance("RSA").generatePublic(new X509EncodedKeySpec(...)) |
m36266a0(String kid)→String | key-id resolution |
m36265a(JSONObject)→C3459a | Ignite service-version resolution (IgniteAuthenticationComponent); consumed by a4.c case 0 |
m36215A0(byte[],C18240i)→byte[], m36218C(int,int,byte[]), m36302s0, m36303t, m36306u0 | array 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..m36234K | duration/time conversions |
m36225F0(JSONObject)→C16908a, m36235K0/m36239M0/m36237L0(JSONArray), m36287l(JSONObject), m36292n0/m36294o0(String,JSONObject), m36296p0(String), m36273e(String) | JSON helpers |
m36275f..m36295p | the 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)
| Real | Disk | Role |
|---|---|---|
pq.b | AbstractC19504b | okhttp3.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.a | C19503a | ResponseBody extends oq.s0 implements InterfaceC16357i0 — contentLength(), contentType() |
pq.c | AbstractC19505c | okhttp3.internal.Util BOMS — f50026a = new byte[0], f50027b = setOf("efbbbf","feff","fffe0000","fffe","0000feff"); m35901a(long,long,long) bounds check |
pq.d | ThreadFactoryC19506d | ThreadFactory — new Thread(r, name), setDaemon(flag) |
pq.e | AbstractC19507e | okhttp3.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)
| Real | Disk | Kind | Content — OBSERVED |
|---|---|---|---|
n4.a | AbstractC18698a | constants | public 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.b | AbstractC18699b | constants | SparseArray{0 → "_all"} (f47378a) — ConstraintLayout’s “all views” tag |
n4.c | AbstractC18700c | constants | HashMap(0) (f47379a) — an empty per-process scratch map |
n4.d | AbstractC18701d | constants | androidx.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):
| Signature | Behaviour — 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 f38508b | post-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():
- If RC key
dev_remote_config_tester == 1, iterate every key inf38507a.keySet()and writem31091l(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 anEyeInAppMessagingHelperreload after 1000 ms and runa7.p0case 8. - Otherwise enqueue a unique one-time
WorkManagerjob named"RemoteConfigUpdater"(:844-847) withNetworkType.CONNECTED+RequiresBatteryNotLow, 20 s initial delay,EXPONENTIALbackoff from 20 s, tag"RemoteConfigUpdater". b5.e.m3668s()— the reflectiverc_keyoverwrite (:861).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):
- If
!qo.r.m36274e0()(i.e. emulator) -> log"<EyeconRemoteConfig> update is not needed, ab tests are disabled"and return. - Load the cached config from prefs key
SP_KEY_EYECON_REMOTE_CONFIG_JO_v2viaf7.y.m32083c(key, null)(a GsonJsonObjectstored as a String). f38505a.addAll(m31075c(cached))— collecttargeting.activation_eventvalues.- Compare
EyeconRemoteConfigVersion(an int inside the cached JSON) with RC keyeyecon_remote_config_version(compiled-in default1). - 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 along & 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 ongetDisplayLanguage.versions: hardcoded749against{"from","to"}.
- Deterministic bucketing:
BigInteger(SHA-256(appInstanceId + " " + test_name).hex[0..16]).longValue() % SUM(_variant_chance), then a cumulative walk overvariants[]picking the winner; setswinner_variantand records_variant_name. "FINAL_DECISIONS_DONT_DELETE"is a magic test name (OBSERVED,:138,284,471): when it wins,m31074bcopies 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.
- if already calculated, re-apply its stored
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’sactivation_eventmatches, it marksdidActivationEventTriggered=true, fires analytics event"active_split_tests"withab_test_device_id/test_name/variant_name, then re-applies all triggered winners and persists back toSP_KEY_EYECON_REMOTE_CONFIG_JO_v2.m31078fhas a main-thread gate (AbstractC0060k0.m320j()) and, off the main thread, parks on anint[]/wait()handshake (RunnableC16943g).public final Boolean m31076d(String key, Boolean def)andpublic 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:
| Channel | Reaches d5.d (circus_key -> w6.a.rc_key -> e-auth-k)? |
|---|---|
| Firebase Remote Config backend | YES |
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 map | YES (fallback, value PgdtSBeR0MumR7fO) |
http://cloudfront-cdn-all.eyecon-app.com/configs/ab-test.txt | NO |
| Local file / DB tampering | YES (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— duringMyApplication.onCreate(main process), every launch.y5.v:861— after every successfulfetchAndActivate()(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
| # | Where | URL source | Method | Auth | Response |
|---|---|---|---|---|---|
| 1 | a4.c case 17 -> w6.a.m38423g | RC key eyecon_remote_config_url, default http://cloudfront-cdn-all.eyecon-app.com/configs/ab-test.txt | GET | none (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:
| Key | Read at |
|---|---|
reg_flow_mode | RegistrationActivity:622 |
permission_prompt_text | RegistrationActivity:652 |
allow_asking_role_multiple_times_in_a_row | RegistrationActivity:1187 |
login_order | RegistrationActivity:1397 |
eyecon_consent_show_location | RegistrationActivity:1901 |
reg_ask_runtime_permission_first | RegistrationActivity:1922 |
is_short_onboarding_enable | RegistrationActivity:4231 |
show_fake_spam_call_for_source | Registration/Onboarding/FakeCallActivity:191 |
is_new_onboarding_enable | Registration/Onboarding/OnboardingActivity:52 |
reg_ask_sms_permission | p360j7/C17534t:193,275; p494q7/C19640a0:38 |
reg_ask_location_permission | p360j7/C17534t:201 |
use_pre_happy_flow_mandatory_logic | p360j7/C17534t:220,296 |
is_fullscreen_intent_a_must | p360j7/C17534t:250 |
is_aftercall_splash_screen_ads_enable | p645x4/C21609j:95 |
is_aftercall_interstitial_ads_enable | p645x4/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 theFINAL_DECISIONS_DONT_DELETEtest 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.f38506bis persisted verbatim to prefs keySP_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
| Key | Default | Why it matters |
|---|---|---|
circus_key | PgdtSBeR0MumR7fO | the API auth key (§19.4) |
eyecon_remote_config_url | http://cloudfront-cdn-all.eyecon-app.com/configs/ab-test.txt | the cleartext channel itself |
eyecon_remote_config_version | 1 | drives 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_tester | 0 | when 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_release | false | when 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_agent | Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36 | desktop-Chrome UA spoofing (§9.3) |
google_photos_link_loader | full JSON: search_url, max_links, links_script, big_url_script | remote-controlled JS injection into the photo scraper (§9.3) |
ac_ads_test_internet_connection_url | https://www.google.com | connectivity probe destination |
dynamic_area_url | cards.eyecon-app.com | dynamic-area content host |
call_contest_report_calls_url | https://apps.eyecon-app.com/call-contest/a/report-calls | call-volume reporting endpoint |
welcome_back_notification_url | Google Forms viewform + formResponse URLs | win-back flow via a Google Form |
analytics_worker_url | disabled_by_remote | analytics upload endpoint |
server_report_url | "" | telemetry endpoint |
support_email | support@eyecon-app.com | — |
premium_features, premium_products, premium_products_v2, premium_products_ids_list, golden_users_products | large 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 / EC180623O | the ad topology |
amplitudeNotTrackableEvents | a ~200-entry JSON array of event names excluded from Amplitude | analytics policy |
singular_events_json, singular_session_timeout (43200000 ms = 12 h) | frequency caps + timeout | attribution policy |
regexForAddSocial | regexes for Facebook profile URL / fb://profile/(\d+) / Instagram profile + og:image + instagramMatchesRegex | social-profile scraping rules |
facebooke_Extract_URL_Regex, findFirstMp4UrlForInstagramRegex | browser_native_sd_url":"([^"]+)", https?://[^\s'"]+\.mp4[^\s'"]* | media scraping rules |
mobitech_video_html_script, mobitech_content_html_script, mobitech_interstitial_html_script | raw HTML+JS wrapping third-party ad tags (the-news-desk.com, s3.amazonaws.com/embed.footylight.com) | ad-tag injection |
balwan_recognizer | JSON array of URL/segment matchers, first entry fbcdn.net with 7 hardcoded Facebook CDN segment ids | recognises 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
| Key | Default | Effect |
|---|---|---|
server_connection_test | {"enabled":true,"times_pattern":[60,6]} | periodic backend reachability probing |
must_permissions_worker_enable / _repeat_time | true / 1 | a worker that repeatedly nags for permissions |
DEV_should_consume_chromium_exception | true | suppresses chromium WebView crash reporting |
mobitech_oom_max_crashes_allow, mobitech_oom_crash_counter_version | 5, V1 | disables the ad SDK after N OOM crashes |
allow_to_reset_compact_aftercall_views, compact_after_call, use_IB_ad_loading* | mixed | after-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_intents | four large intent templates | deep-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") <-- C1presence check: AbstractC9039h.m10665v()AbstractC9039h also exposes:
m10631K(Throwable)/m10632L(Throwable)— the app’s global error reporters;m10632Lis called fromeo.b.m31717ewith the raw prefs key and value on a read failure, so personal data reaches logcat.m10641U(String cid, String reason)— the destructive 401 handler (seeBACKEND-API.md§3.5).m10644a(boolean, Throwable)— Crashlytics report gate.m10661r()→long— join timestamp (used byf7.ncontest counting).f13178e— a static CID used to avoid self-de-registration.
20.2 MyApplication static facade (com.eyecon.global.Others.MyApplication, 597 lines)h5
| Member | Type | Use |
|---|---|---|
f12163h | MyApplication | app singleton |
f12170o | boolean | isMainProcess() — gates config reads, error handling and 401 handling |
f12172q | File | external-storage root (when mounted) — selects the photo store location |
f12171p | C19858g | the CallLog ContentObserver |
f12174s | RunnableC16486o0 | lifecycle dispatcher |
f12166k / f12167l | C0072q0 (a7.q0) | LottieImageAsset caches |
m10342d()→Context | app context | |
m10343e()→Resources | getString(R.string.*) — the source of pk_enc, cipher_key, facebook_app_id, quotes_api_id, amazon_app_key, applovin_key, app_vestor_* | |
m10349k()→Resources | localised strings | |
m10344f(int)→int, m10345g(int,Context)→int, m10346h(int)→int | dimension / colour resolution | |
m10347i()→SharedPreferencesEditorC16496x | write API — m32076c(value,key) + m32074a(null) | |
m10348j()→SharedPreferencesC16497y | read API — getBoolean/getString/..., m32082b(def,key), m32083c(key,defJson), m32085d(key) |
20.3 C1 APIs used from outside C1h5
| C1 symbol | Real name | External consumers (representative) |
|---|---|---|
MyApplication.m10348j() / m10347i() | f7.y / f7.x | essentially every cluster |
AbstractC0039a column constants | a7.a | e5, eo, m7, x6, f7, a4, plus all com.eyecon.global.* activities, w6, b5, q7 |
AbstractC0062l0 | a7.l0 | b5, d5, j7, q7, x4, e5, all activities (density/locale/country) |
AbstractC0077t | a7.t | every activity (Toasts/dialogs/notifications), incl. f7.z |
AbstractC16468f0 | f7.f0 | b5, w6, d5, a7, e5, m7, y7, f5, activities (esp. m32019n for backup) |
C16458a0 | f7.a0 | every DAO in q7, j7, b7, v8, … |
C0041b | a7.b | DBContacts, e5, f7, eo, m7, x6, d5 |
eo.b | eo.b | f7.y, f7.x, a4.d |
AbstractC0060k0.m314d() | a7.k0 | d5.c (A/B bucketing), d5.d |
C16489q | f7.q | e5.m |
AbstractC0084w0.m450h() | a7.w0 | qo.r -> d5.c, d5.d |
AbstractC19779r.m36274e0() | qo.r | d5.c, d5.d |
C18517d | m7.d | r5.C19854e, am.RunnableC0215t1, bm.*, lg.*, p6.* |
C18533l / C18531k | m7.l / m7.k | PhotoPickerActivity, MenifaFragment, v8.* |
C21884l | y7.l | f7.z, themes |
C16945i (p315h7) | thread pool | used by C1 everywhere; owned by another cluster |
C18416a (p410lo) | logging/formatting | used by C1 everywhere; owned by another cluster |
C21273a / C21280h | w6.a / w6.h | a4.c, d5.c, e5.m, x6.*, m7.* |
C21569d (p262e7) | AbstractC16130b | the 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.m38417ais the only place C1 configures it.gf(Play Core) —gf.c/gf.kprovide in-app update;f7.zrenders the “update downloaded / restart” snackbar.g7.a— every appBroadcastReceiverextends 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:
- Filling device storage (
SQLITE_FULL) is enough to wipe all app data, which destroys theclientIdand forces a full re-registration — a resource-pressure-triggered data-loss/DoS. - A transient lock or partially-applied transaction self-DoSes the app on every launch.
- The intended sibling-process kill loop is dead code (
pid == 0 && ...inverted comparison at:491), so multi-process state can diverge. - 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):
| Resource | Line | Value |
|---|---|---|
pk_enc | 1237 | RSA-2048 X.509 SPKI, base64 (public key — not secret, but un-rotatable) |
pk_enc_2 | 1238 | second RSA-2048 SPKI |
cipher_key | 272 | "Ewu" — the Blowfish key prefix (see F5) |
google_api_key | 770 | AIzaSyCD1Lmwj_ln-h4RMLHApAZjuPZNWAzJi3M |
google_crash_reporting_api_key | 772 | same value |
facebook_app_id | 595 | 1716660955257637 |
gcm_defaultSenderId | 745 | 820458390093 |
project_id | 1269 | eyecon-app |
firebase_database_url | 719 | https://eyecon-app.firebaseio.com |
google_storage_bucket | 774 | eyecon-app.appspot.com |
default_web_client_id | 396 | 820458390093-i5qaq32l80noq6it19dfuqummm685pbv.apps.googleusercontent.com |
quotes_api_id | 1481 | e2eeb1aa9f32eb07fa04595a0c457ecb6fadb772 — used as Authorization: Token ... at f7.j0:102 |
applovin_key | 120 | UZvHsQBqsDCf0qv_yVDKn0UeTmRknnQSgTT3HP7HFikqs8IxdD-7uWdloUQVGTn09CQ0LThxLm6tfMnKDFS9xa |
amazon_app_key | 107 | 905c0238-13dd-4cf9-af3d-90f64672a4a9 — used at o4.AbstractC19054g:88 |
app_vestor_api_token | 117 | 980d4e3f-d057-4e5b-9034-1587edadf5a4 |
app_vestor_app_key | 118 | 8c830f45-9f86-48d3-bcca-4e14fe2c1129 |
crackle_key | 370 | fai=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)onReceivedSslErrordoes not callhandler.proceed()— TLS errors are correctly fatal (p008a7/C0069p.java:174-176).gf.kcorrectly verifies the Play Store signing-certificate digest before binding.ri.m.m36727g(p526ri/C19932m.java:488-503) implements AES/GCM/NoPadding with a 12-byteSecureRandomnonce 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.j0setsLocaleList.setDefaultexplicitly (API 24+) and usescreateConfigurationContext— no global-locale leak on modern devices.f7.y’sgetStringSetreturns a defensive copy.x6’s three transports all checkAbstractC9039h.m10665v()(clientId present) before issuing a request, and catchIOException | OutOfMemoryErrorrather than crashing.
22. Corrections and additions to the existing documentsh4
| Doc / section | Statement | Correction |
|---|---|---|
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.5 | 401 path described as returning 700 | Confirmed: TypedValues.TransitionType.TYPE_DURATION decompiles to 700 (C21273a.java:134). No change. |
DATA-LAYER.md §2.19 | shared_preference_v2 schema | Confirmed 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 posture | Confirmed 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
.javain 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, plussrc/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 fromcomment). - 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 underpNNN<name>directories (e.g.p494q7=q7,p365jd=jd). - Decompiler caveats: several methods are emitted with
throw null;or JADXWARNcomments where control flow could not be reconstructed; those are called out individually.
0.1 Package index (scope, as briefed)h5
| DISK_DIR | REAL_PKG | FILES | KB | OWNING_MODULE |
|---|---|---|---|---|
as | as | 129 | 1164 | Billing |
bm | bm | 156 | 688 | Registration |
p237d6 | d6 | 49 | 359 | Billing |
p672y5 | y5 | 26 | 347 | Registration |
p252ds | ds | 22 | 316 | IdPlus |
p035b8 | b8 | 19 | 271 | Registration |
p365jd | jd | 52 | 268 | Registration |
p494q7 | q7 | 58 | 204 | Registration |
p515r4 | r4 | 16 | 187 | IdPlus |
p244df | df | 25 | 182 | IdPlus |
p489q1 | q1 | 30 | 173 | Registration |
p034b7 | b7 | 13 | 171 | Billing |
p047c7 | c7 | 25 | 140 | Billing |
p418m5 | m5 | 16 | 134 | Registration |
p358j5 | j5 | 29 | 122 | Billing |
p285f8 | f8 | 14 | 118 | Registration |
p607vk | vk | 40 | 115 | Registration |
p640x | x | 30 | 107 | Billing |
p314h6 | h6 | 16 | 87 | IdPlus |
p448o4 | o4 | 22 | 81 | IdPlus |
p048c8 | c8 | 5 | 79 | IdPlus |
p263e8 | e8 | 11 | 77 | IdPlus |
p621w7 | w7 | 14 | 77 | Billing |
p303g5 | g5 | 12 | 69 | Billing |
p261e6 | e6 | 4 | 64 | Billing |
p045c5 | c5 | 11 | 62 | Billing |
p014aj | aj | 9 | 60 | Registration |
p553t4 | t4 | 16 | 53 | Billing |
p410lo | lo | 1 | 45 | Billing |
p325hh | hh | 2 | 43 | Billing |
p397l5 | l5 | 7 | 41 | Registration |
p491q4 | q4 | 6 | 32 | IdPlus |
p518r7 | r7 | 8 | 30 | Registration |
p398l6 | l6 | 7 | 30 | Registration |
p685yj | yj | 1 | 27 | Billing |
p321hd | hd | 5 | 24 | PhoneAccount |
p313h5 | h5 | 6 | 22 | IdPlus |
p339i6 | i6 | 5 | 22 | IdPlus |
p671y4 | y4 | 2 | 13 | Registration |
p262e7 | e7 | 6 | 13 | Billing |
p040c0 | c0 | 9 | 13 | Billing |
p006a5 | a5 | 1 | 11 | Billing |
p700z4 | z4 | 3 | 10 | Registration |
p419m6 | m6 | 5 | 9 | Registration |
p376k0 | k0 | 4 | 9 | Billing |
p050ce | ce | 3 | 5 | Registration |
p381k7 | k7 | 3 | 5 | PhoneAccount |
p359j6 | j6 | 3 | 4 | IdPlus |
p527rj | rj | 5 | 2 | Billing |
p254e | e | 1 | 1 | Registration |
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 dir | Real pkg | Kind | Responsibility |
|---|---|---|---|
p494q7 | q7 | app | Registration controller/presenter layer: join/rejoin callbacks, phone-number validation state machine, error enums, onboarding UI logic |
p365jd | jd | app | join.jsp HTTP client + join request/response models |
p516r5 | r5 | app (not in C2 brief list but referenced) | cli_auth / checkcode CLI-registration client |
p489q1 | q1 | app | Play Billing service proxy: BillingClient wrapper, BillingServiceConnection, purchase flow, ack/consume, zz* protobuf JSON codecs |
p303g5 | g5 | app | Play Billing engine/manager: product + offer loading, price formatting, purchase orchestration, entitlement cache |
p358j5 | j5 | app | Premium entitlement model (PaidPremium, FreePremium) and premium Activity/Fragment UI logic |
p621w7 | w7 | app | Premium purchase dialog / price / plan selection UI |
p045c5 | c5 | app | Premium bottom-sheet / dialog UI logic |
p040c0 | c0 | app | Small UI helpers used by x/k0 (Lottie/anim dispatch) |
p376k0 | k0 | app | Lottie animation wrapper used by x |
p262e7 | e7 | app | Eyecon JavaScript bridge object + tiny store-support types |
p006a5 | a5 | app | Single-class prefs/analytics facade |
p640x | x | app | Lottie/animation player orchestration (Choreographer frame loop, thread pool) |
p685yj | yj | app | One large class: billing-callback/subscription glue (out of Billing TSV row but present) |
p410lo | lo | app | One 45 KB class: lo.a — coroutine/logging/analytics helper referenced almost everywhere |
p325hh | hh | app | Two classes: media/exo event glue |
p048c8 | c8 | app | c8.e abstract Activity base (IdPlus screens) |
p314h6 | h6 | app | IdPlus Notification-Reader UI |
p313h5 | h5 | app | IdPlus Notification-Reader premium gate UI |
p339i6 | i6 | app | IdPlus Telegram/notification small screens |
p359j6 | j6 | app | Telegram window click wiring |
p448o4 | o4 | app | IdPlus profile/account UI + InterfaceC19056i (advertising-ID provider used by Eyecon.getAdvertisingId) |
p515r4 | r4 | app | Google AdMob native/mediation ad loader used by IdPlus screens |
p263e8 | e8 | app | Facebook App Events / Graph JSON helpers |
p252ds | ds | app | IdPlus ad mediation orchestration |
p244df | df | app | Generic “bottom sheet + retry dialog” framework (used by IdPlus screens) |
p047c7 | c7 | app | Error/retry dialog framework (c7.t, c7.j) + base AlertDialog, also c7.l = C3888t paywall-ish dialog |
p034b7 | b7 | app | Base Activity (b7.e AbstractActivityC3498e) — all app Activities extend it |
p553t4 | t4 | app | AdMob preload / manager-ad wrapper |
p321hd | hd | app | ExoPlayer Metadata.Output implementation (SCTE-35 splice commands) — PhoneAccount module row but actually generic media glue |
p381k7 | k7 | app | Tiny helpers over gf.c |
p285f8 | f8 | app | Facebook-login helper (LoginManager wiring) |
p607vk | vk | app | InMobi/analytics event-dispatch runnables (mostly SDK glue) |
p418m5 | m5 | app | Shared “phone number / line” helpers + gson DTOs |
p397l5 | l5 | app | Analytics/identity bootstrap runnables |
p398l6 | l6 | app | Onboarding-ad / interstitial glue |
p518r7 | r7 | app | Onboarding UI (sliding page) + FakeCallActivity support |
p671y4 | y4 | app | Singular SDK glue runnables |
p700z4 | z4 | app | Singular/AppsFlyer-adjacent event runnables |
p419m6 | m6 | app | Small click handlers / search-UI helpers |
p671y4/p254e | see above | app | — |
p254e | e | app | Single tiny abstract class |
p527rj | rj | app | 5 marker interfaces only |
p050ce | ce | app | Google Identity (com.google.android.libraries.identity.googleid) credential-wrapper helpers — Google sign-in for join |
p014aj | aj | app | Apache-HTTP based aj.* — install-referrer / Play-Store referrer HTTP fetch? (verified below) |
p035b8 | b8 | app | Giant generated-style view-binding/form-building class b8.b (155 KB) — see §2 verdict |
p237d6 | d6 | app | Store/paywall WebView UI (store screens) |
p672y5 | y5 | app | Huge single classes y5.k (146 KB), y5.f (101 KB), y5.v (43 KB) — see §2 verdict |
as | as | 3rd-party | kotlinx.coroutines internals |
bm | bm | 3rd-party | OkHttp 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 butpublic 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 constantDate f47377a = new Date(1776946263760L). - OBSERVED:
n4.b(C18699b) holds aSparseArraywith one entryput(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 listsn4under 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) importsandroid.media.MediaCodec,MediaCrypto,MediaFormat,Surface,com.google.android.exoplayer2.ExoPlaybackException,android.opengl.*— the signature import set of ExoPlayer’sExoPlayerInternal. - OBSERVED:
zd.g(HandlerThreadC22160g) buildsEGL14/EGLDisplay/EGLContext/EGLConfig/GLES20/SurfaceTexture—GLSurfaceView-style GL thread for video rendering. - OBSERVED:
zd.n(C22167n) usesDisplayManager,Surface,WindowManager—androidx.media3.uiPlayerView-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) — importsandroid.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/writesDBContactsand 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 PlayBillingClient.Buildering5.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 theInstallReferrerReceiverflow 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.vshould 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 ViewBindingand 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) usesViewPager,CardView,Group— a page/card layout binder.
2.5 Bonus verdict: as and bm — THIRD-PARTY, misfiledh5
as=kotlinx.coroutinesinternals (as.a … as.z3, 129 files / 1.16 MB). Proof:as.aextendswo.c, andp636wo\wo.cis aCancellableContinuation(fieldintercepted, ctor(InterfaceC20763i, InterfaceC20758d));as.simportskotlinx.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=Dispatcherexecutors;bm.a0= an OkioBufferedSink— importsjava.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 intoclasses3.dexinw6.a. It is sent verbatim as thee-auth-kheader 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-4d5it is10 + 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
clientIdis 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 literallyreturn (String) AbstractC17503r.m33607k("clientId", "");andm10665v()isreturn !isEmpty((String) SharedPreferencesC16497y.m32082b(null, "clientId")); - OBSERVED: in
p365jd\RunnableC17614z(renamed from: jd.z)case 1, after a successfuljoin.jspGET the code does:c19673l0.m31518o(strM38423g);wherestrM38423gis the raw HTTP response body string. - OBSERVED:
AbstractC16130b.m31518o(Object)→C16484n0.m32065f(Object)→map.put("DEFAULT_RESULT", obj); andC16484n0.m32061b()ismap.get("DEFAULT_RESULT"). - OBSERVED:
q7.l0.mo309m()case 4then doesString str6 = (String) m32061b();andeditor.m32076c(str6, "clientId"). - Therefore:
clientIdis the completejoin.jspresponse body, stored verbatim. The same body is separately JSON-parsed into aHashMap(keyspi,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/tokenJSON field. A statement such as “clientId is thecidfield of the join response” is wrong; it is the whole payload.
3.5 Storage — CONFIRMEDh5
- OBSERVED: writes go through
MyApplication.m10348j()(p284f7\SharedPreferencesC16497y) andMyApplication.m10347i()(a freshSharedPreferencesEditorC16496x). - The app’s custom prefs table is
shared_preference_v2in thecontactdbSQLite DB (see C1/C4 data layer). Reads therefore go throughSharedPreferencesC16497y.m32082b(...)rather than through a regularSharedPreferencesfile, which is why thegetBooleanSP/getIntSP/getStringSPJS 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 class | Notes |
|---|---|
com\eyecon\global\Registration\RegistrationActivity | 210 KB. The main 7-child ViewFlipper (f12914H) registration screen: country, number, code, social login, permissions, etc. |
com\eyecon\global\Registration\PhoneNumberValidationActivity | 83 KB. Second-number / line validation screen (separate ViewFlipper f12854U.f2863x). |
com\eyecon\global\Registration\Onboarding\OnboardingActivity | 8 KB. Pre-registration onboarding. |
com\eyecon\global\Registration\Onboarding\FakeCallActivity | 15 KB. Simulated incoming-call onboarding. |
com\eyecon\global\Registration\SMSRetrieverBroadcast | 1.1 KB — SMS Retriever API receiver. |
com\eyecon\global\Registration\InstallReferrerReceiver | 7.6 KB. |
com\eyecon\global\Registration\RegisterNotificationWork | 1.7 KB — androidx.work.WorkManager worker. |
com\eyecon\global\Registration\PreparingContactView | 19 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 fromf7.n0);void m32065f(Object)→map.put("DEFAULT_RESULT", obj), re-exposed asm31518o(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()thenmo415j(false));m31516i()= onSuccess (mo31100l()thenmo309m()thenmo415j(true)). - The synthetic subclass
e7.b(boolean, long)installs aHandlerthat firesm31515h()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
-
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 (svrvalue),boolean i.m36103b(int)maps a numeric error code onto thesvrstring:-9999 → null,-999 → "2",0 → "1",-1 → "3",< -1 → "3",> 0 → "1".m36102a(String regId)enqueuesnew RunnableC17614z(cli, regId, isAlreadySocialAuth, transport, svr, new C19647c1(this, 1))on the shared “Server” executorC21280h.f56470f.f56472a.
-
jd.z(RunnableC17614z) — default branch =cli_auth/gettransport. OBSERVED request (GEThttps://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), optionallast_error=<err>. Response JSON fields:error_status(int),subcode(0..3),tr(transport),details{ send_to, timeout, body, code }ordetails{ timeout, pattern }forso_flash.subcodemapping: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 sameRunnableC17614zclass is reused with a different constructor for the network-failure path ofjd.b0(C17566b0). -
r5.s(RunnableC19875s)case 1—cli_auth/checkcode. GEThttps://api.eyecon-app.com/app/cli_auth/checkcodewithcli=<formatted>,code=<user code>,apsversion=vc_749_vn_4.2026.04.23.1145_a,tr=<transport>,is_already_social_auth=<bool>. VerbEnumC19659g1.IS_CODE_VALID. OnNumberFormatExceptionof the body it reports-1. Success condition:Integer.parseInt(body) == 0 && httpCode == 200. Retry: one retry afterAbstractC0077t.m439X0(5000L)(5 s backoff) if the first call throws. On success it setsC16489q.f40981n.f40984c = trueand runsnew RunnableC16111m(13, c16489q, new C16770d(25, cli, callback))—p308gj.C16770dis the “code accepted → now join” trampoline (it is what supplies thesvr=value used in step 4). -
jd.zcase 1—join.jsp. (This is the join itself; see 4.4.) -
Success landing —
q7.l0(C19673l0).mo309m()case 4(join success). OBSERVED, in order:boolean isJoin = ((Boolean) map.get("isJoin")).booleanValue();(isJoincomes from the join response’sis_newfield:"1"→true).RegistrationActivity.m10511a0(activity, "success");C21845b.f58845b.m39065f("onJoinSuccess");C22071c.f60175e.m39334e("onJoinSuccess", null);EyeconAnalyticsWorker.m9988b(true).AbstractC3464e.m3658i("New join")whenisJoin.- 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 bodyeditor.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))(pullmysettingsfrom the server). If join:m10549j0(new RunnableC19643b0(activity, 11)). activity.m10557u0(); if!f12947X0thenm10560x0().
-
Success landing —
q7.n(C19677n).mo309m()case 2(phone-number validation success, inPhoneNumberValidationActivity). 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 fromm32061b(), and the second additionally clearsSP_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"whereC21280h.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):
key value cliC18312b.m34437h().m34444e(rawCli)(formatted E.164-ish) — skipped ifisAlreadySocialAuthusernameliteral New_User_EyecondevicenameAbstractC0062l0.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_IDcli_cccountry code svrC19644b1.f50539o.toString()— only when already social-auth’dtrtransport enum string ( mo_sms/so_sms/so_call/so_flash/huawei/fb/social_login/auto)generated_cliprefs SP_REJOIN_OLD_GENERATED_CLIif non-nullimeiC18502p.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(...)thenm31515h(). - HTTP 200 but empty body →
JOIN_RESPOND_CODE_200_WITHOUT_CID(-16), error. - HTTP 200:
mapis filled byC21273a.m38423g(...); keys used:pi(new public id),sd("0"⇒isDeviceChanged),is_new("1"⇒isJoin). piempty →throw new Exception("Join, missing public id")(logged,isJoinforcedfalse).pipresent →MyApplication.m10347i().m32076c(pi, "SP_UUID")+RunnableC0070p0(6)event.- Then
c19673l0.m31518o(body);m32066g(isJoin,"isJoin");m32066g(isDeviceChanged,"isDeviceChanged");m31516i()→ success.
- HTTP != 200 →
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 theBroadcastReceiver. It is the Play-Services SMS Retriever path that auto-reads the server-sent MO SMS code (details{ send_to, timeout, body, code }fromcli_auth/gettransport) instead of asking the user.- The two prefs that gate the flow are written in the failure branches:
sendSmsEnded = trueandisSmsValidEnded = true(q7.l0.mo308k()case 4,q7.n.mo308k()case 2). Inq7.l0.mo309m()case 3andq7.n.mo309m()case 1the app readsisSmsValidEnded; 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): ifisEmulatoris true ormo36098a()(theq7.e1code) is3,4or7, the app callsRegistrationActivity.m10506B0()+m10550k0()(hard reset) instead of letting the user retry. Codes 3/4/6/7/8 areSERVER_ERR_CLI_IS_NOT_VALID,SERVER_ERR_MANUAL_AUTH,SERVER_ERR_UNSUPPORTED_FLOWperq7.e1; the same triple gate exists inq7.n.mo308k()case 1.
4.6 Social (Google / Facebook) registration path — OBSERVEDh5
p494q7\EnumC19659g1(q7.g1) containsFACEBOOK_LOGIN(12)andGOOGLE_LOGIN(12)— the two constants share the value12. Anyswitchonq7.g1cannot distinguish Google from Facebook; both hit the same arm. (This is an obfuscation artefact, not a logic bug — the real discriminator isC19644b1/C19647c1payload fields.)p494q7\C19640a0/C19641a1/C19657gandp285f8\*(f8.*) carry the Facebook LoginManager wiring (com.facebook.C9154o,com.facebook.FacebookException).p050ce\ce.c(C3912c)extends CustomCredentialfromcom.google.android.libraries.identity.googleidand referencesGoogleIdTokenParsingException— this is the Google Sign-In credential adapter.- After social login,
q7.l0.mo309m()case 5fills the name/photo and writesSP_KEY_PICTURE_SOURCE="google"(i13 == 1) or"facebook". - The
social_auth/use_cli_validationendpoint (p607vk\RunnableC21082e0, C2-adjacentvkpackage) 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 becomesSP_KEY_VALIDATION_WITH.
5. Flow 2 — CLI authorization / second-number provisioningh4
5.1 Endpoints (OBSERVED)h5
| Endpoint | Method | Caller | Purpose |
|---|---|---|---|
https://api.eyecon-app.com/app/join.jsp | GET | jd.z case 1 (RunnableC17614z) | Register/obtain a new CLI + the clientId |
https://api.eyecon-app.com/app/cli_auth/gettransport | GET | jd.z default branch | Ask the server how to validate the CLI (transport, message body, code, timeout) |
https://api.eyecon-app.com/app/cli_auth/checkcode | GET | r5.s case 1 (RunnableC19875s) | Submit the SMS/call code |
https://api.eyecon-app.com/app/social_auth/use_cli_validation | GET | vk\RunnableC21082e0 (logged as "join.jsp") | Re-validate an already social-authenticated CLI |
https://api.eyecon-app.com/app/updateuserdetails.jsp | GET | vk\RunnableC21082e0, r5\RunnableC19874r, w6\RunnableC21275c | Push profile/registration details |
https://api.eyecon-app.com/app/getoptionalpics.jsp | GET | vk\RunnableC21082e0 | Suggested avatars |
https://api.eyecon-app.com/app/mysettings | GET / POST | w6\RunnableC21276d case 8, u4\RunnableC20597o case 7 | Client ⇄ server key/value settings sync |
https://api.eyecon-app.com/app/get_store_products | GET | w6\RunnableC21276d case 6 | Store product catalogue |
https://api.eyecon-app.com/app/virality/premium_status, virality/viral_id, virality/viral_id_check | GET | w6\RunnableC21276d cases 3/4/9 | Free-premium/virality |
https://api.eyecon-app.com/app/uploadpic.jsp | POST multipart | w6.h.m38441n() | Avatar upload (writes raw clientId as msisdn) |
https://api.eyecon-app.com/app/picapproval.jsp | GET | u4\RunnableC20597o case 3 | Avatar moderation |
5.2 Second-number provisioning path (step by step)h5
PhoneNumberValidationActivitystartsq7.i1(C19665i1) with the new CLI.q7.i1.m36102a(regId)→jd.zdefault 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.
- The server answers which transport to use and, for
- User (or SMS Retriever) supplies the code →
r5.scase 1→cli_auth/checkcode.- Success is
parseInt(body) == 0&& HTTP 200 — a bare integer body, not JSON. C16489q.f40981n.f40984c = truethenp308gj\C16770dcontinues intojoin.jspwith thesvr=value derived fromq7.i1.m36103b(code).
- Success is
join.jspreturnsis_new:is_new == "1"→ new account;q7.n.mo309m() case 2writesclientId, clearsSP_KEY_NOT_REGISTERED_WITH_REAL_CLI_V2.- otherwise → second number on the existing account;
q7.l0.mo309m() case 4writesclientId(overwriting),SP_KEY_VALIDATION_WITH,deviceName,osName,sp_join_time,SP_JOIN_VERSION,SP_JOIN_VERSION_CODE=749,SP_KEY_IS_REJOIN=true.
- On
isEmulatoror 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→onCreatebuildsp381k7\C17790b(k7.b, anAbstractAccountAuthenticator).- Every method of
k7.bis a stub:addAccount→null;confirmCredentials→null;editProperties/getAuthToken/getAuthTokenLabel/hasFeatures/updateCredentials→throw new UnsupportedOperationException(). SyncServicebindsk7.c(AbstractThreadedSyncAdapter) whoseonPerformSyncis an empty body.AccountWithDataSetis a 3-stringParcelablewhosetoString()returns"", plus a deadC17789a(k7.a) duplicate that is instantiated and immediately discarded in both constructors.- CONCLUSION: there is no Android
AccountManagertoken issuance for Eyecon CLI in this build. All CLI authorisation is done by direct HTTP tocli_auth/*with thee-authheader. 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):
- Delegates first to
new SingularInstallReceiver().onReceive(app, intent)(Singular SDK). - Logs the entire referrer bundle to
System.outwith tagIRR/— every key and value of the INSTALL_REFERRER intent is printed to logcat in release builds. (Note: theprint_logs_in_releaseremote-config key exists but this call is a hard-codedSystem.out.println, not gated by it.) - 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 setsreferrer call = true. - Viral-id format A —
referrer.startsWith("evid-"):vid = referrer.substring(5); accepted only ifvid.length() == 12(no charset validation, no server-side verification at this point). Thenm10457c()(one-shotInvite_successfully_acceptedanalytics event +sendViralityEventDone=true) andAbstractC0037a.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 anInstall referrer channelanalytics 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 readsutm_content; if it starts withevidand the remainder is 12 chars, writesSP_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.jspsends it asviral_id(jd.z case 1) — i.e. the inviter id is sent to the backend on every join.j5.b(C17485b,FreePremium) callsvirality/viral_id(returnsviralId) andvirality/viral_id_check; on success it persistsSP_KEY_MY_VIRAL_ID(w6\RunnableC21276d case 9).j5.c(C17487c) usesSP_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_TYPEto grant and display “free premium” entitlements.- Remote config
RewardedAdGiftDays,InviteFriendPrice,daysOfTrialIdPluscontrol the amount. y5.v(C21867v)implements InstallReferrerStateListenerandp285f8,p397l5,p671y4,p700z4hold the rest of the referrer plumbing.
6.3 Free-premium (“virality”) entitlement path (OBSERVED)h5
j5.c (C17487c):
m33582f(long millis)— grant: writesSP_KEY_LAST_DATE_FOR_FREE_PREMIUM = now + millisandSP_KEY_LAST_FREE_PREMIUM_TYPE("video"/"Free Version"/…).m33583h(Runnable)— enqueue on the server executor; posts a system notification (FreePremiumUserActivityintent, extras"received premium notification", tag"free premium update").- Triggered by rewarded ads:
j5.f(C17491f) ononUserEarnedReward→C17487c.m33582f(activity.f11528X * TimeUnit.DAYS.toMillis(1))wheref11528Xis the number of days granted (remote configRewardedAdGiftDays). - 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 BillingClientw7.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 syncj5.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 inpremium_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) holdsSP_KEY_STORE_PRODUCTS_IDS_JSON(default{"subs":[],"inapp":[]}); when empty it fetchesget_store_products(w6\RunnableC21276d case 6) with"do_product_detail_load"=true; otherwise it callsg5.i.m32534m(...)to query details directly.g5.i.m32524e(ArrayList, C16703e)→zzco.zzk(list)builds theQueryProductDetailsParamsproto and callsq1.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."), excludingplay_pass_subs.- Subscription offers are modelled by
q1DTOs:C19549l(ProductDetails),C19548k(base plan / subscription offer),C19547j(offer), mapped inWebviewPaywallActivity.m10027E0toC5841h/C5839f/C5840g.
7.3 Purchase launchh5
g5.i.m32537q(Activity, C16708j, callback):
- requires the product to be a
C16710l(GoogleProduct) elseIllegalArgumentException("showProductGoogle called with non-GoogleProduct: …"); - →
RunnableC3701s3(inbm)case 2:- builds
ProductDetailsParams(C17847a) usingproductDetails.getZza().f50198d(basePlanId) and the caller-suppliedofferToken; throwsIllegalArgumentException("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 callsq1.c.mo35971f(activity, params)=BillingClient.launchBillingFlow. - result:
callback.m31518o(Integer.valueOf(responseCode)); callback.m31516i();
- builds
- INFERRED:
offerTokenis 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 chosenofferTokenagainst any server-issued price or against the plan the app intends to sell. TheproductIdis constrained (it must exist inStoreActivity.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", …)andg5.i.m32529f(list, 0, new HashMap())→ acknowledge.
g5.i.m32521b(...):- skip if
JSON.optBoolean("acknowledged", **true**)— note the default istrue, so a missingacknowledgedfield means “already acknowledged”; - skip unless
purchase.m4425a() == 1(PURCHASED); - token =
optString("token", optString("purchaseToken")); C0162c(5)=AcknowledgePurchaseParams→q1.c.mo35969d(...).- There is no
consumeAsyncanywhere ing5/q1/j5— nothing is consumed.
- skip if
- Acknowledgement retry:
g5.i.m32529frecurses up toi3 == 10then logs"acknowledgePurchases failed to acknowledge all purchases retryCount reached.". - Entitlement is computed by
p008a7\C0076s0case 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 premiumelse if (type == "inapp") { register as "inapp" product; flag=true }if (flag) prefs["SP_KEY_INAPP_PRODUCTS_CACHE"] = <list> p034b7\C3502icase 12does the same for the “load products” flow and, onCB_ERROR == 1with nothing owned, callsmo308k()(error path).g5.i.onBillingSetupFinishedon success callsj5.k.m33587c(null)→g5.i.m32531h(callback)→m32522c(true, …)→ the same entitlement refresh. It also writesSP_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 noandroidpublisher/play.google.comHTTP call, noPurchases.products.get, no RTDN (Purchases.subscriptionsv2.get) endpoint, and no server-side signature/RSA check anywhere ing5,q1,j5,w7,x,c0,k0,hh,yj. - The only thing pushed to the Eyecon backend about premium status is
mysettings(POST) with keyspremium_cache_productandpremium_cache_time(j5.k.m33588e→C16464d0.m31983a()→p008a7\RunnableC0070p0 case 18→w6.h.m38440m(...)→u4\RunnableC20597o case 7→ POSThttps://api.eyecon-app.com/app/mysettingswithtype,key,value).premium_cache_productis 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. mysettingsGET (w6\RunnableC21276d case 8) is only read back to populateSP_KEY_MY_SETTINGS_SERVER_JSON, whichj5.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 Configthis.f11622b0 = strM31091l; // the URLthis.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:
store_urlis 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", "")). BecauseUri.parse("").getHost()isnull, an emptystore_urlmakes line 2 throw an NPE — soStoreActivityis only reachable when Remote Config (or theFB_RC_<key>prefs override inm31093n) supplies a value.- NO scheme check, NO host allow-list, NO certificate pinning, NO integrity check on
store_url.WebviewPaywallActivitydoes have exactly that check (§8.5) —StoreActivityhas none of it. This is the asymmetry that makes the issue real. setJavaScriptEnabled(true),setDomStorageEnabled(true)andsetAcceptThirdPartyCookies(true)are all set unconditionally.- Two
@JavascriptInterfacebridges are attached:Eyecon(p262e7.C16132d=e7.d) andEyeconStore(p379k5.C17757i=k5.i). C5846m(the sharedWebViewClient) does not overrideshouldOverrideUrlLoadingfor theStoreActivityvariant (f11594d == 2falls through todefault: 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.onReceivedSslErrordoes not callsslErrorHandler.proceed()(it delegates tosuper.onReceivedSslErrorwhich cancels), so TLS errors are not silently accepted — but there is also no pinning, so a valid-CA interception is enough.- 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 method | Signature | Returns / effect | What it exposes |
|---|---|---|---|
getCid() | String getCid() | AbstractC9039h.m10662s() = prefs clientId | The 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 authenticatedCli | The validated phone number (MSISDN) in E.164 form. PII. |
getPublicId() | String getPublicId() | AbstractC9039h.m10625E() = prefs SP_UUID | Stable per-install public identifier used by the backend for attribution. |
getAdvertisingId() | String getAdvertisingId() | InterfaceC19056i.f48663a.m35361a() → AdvertisingIdClient.getAdvertisingIdInfo(...), cached in Eyecon.SP_KEY_ADVID | Google Advertising ID. Persistently re-identifiable cross-app tracking handle. |
getMyName() | String getMyName() | AbstractC9039h.m10663t("") = prefs username | The 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 749 | App 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() | int | AbstractC0062l0.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 method | Signature | Native effect |
|---|---|---|
close() | void close() | RunnableC17750b(this, 3) → storeActivity.finish(). |
getProductsInfo(String productsJson, String jsCallback) | void | RunnableC17334k 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) | void | RunnableC16111m 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) | void | RunnableC3589a 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) | int | g5.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) | String | g5.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) | void | Builds {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) | void | RunnableC17756h 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) | void | AbstractC9039h.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) | void | RunnableC17180l0 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) | void | RunnableC16111m(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.
| method | effect |
|---|---|
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.m425J0injects four JavaScript bridge objects / script blobs into a remote WebView whose URL is controlled bysearch_url+links_script+big_url_script. That is the Google-Photos link loader / reverse-lookup WebView. StoreActivity/WebviewPaywallActivityuse a separate, statically-named pair of bridges (Eyecon/EyeconStore/Android) added directly withaddJavascriptInterface, and thestore_urlremote-config key is used only inStoreActivity.onCreate(grep over the whole tree:store_urlappears exactly twice — the default""and that one call).- They do share one thing:
C5846m(theWebViewClient) and thef7.rbase class (AbstractC16490r, handlesonRenderProcessGone). No script injection, noevaluateJavascriptbridge 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 file | real class | kind | bytes |
|---|---|---|---|
AbstractC3247a1.java | as.a1 | abstract class | 13617 |
AbstractC3248a2.java | as.a2 | abstract class | 7697 |
AbstractC3251b.java | as.b | abstract class | 32305 |
AbstractC3255b3.java | as.b3 | abstract class | 3789 |
AbstractC3260c3.java | as.c3 | abstract class | 4379 |
AbstractC3297k0.java | as.k0 | abstract class | 3124 |
AbstractC3306m.java | as.m | abstract class | 3475 |
AbstractC3329q2.java | as.q2 | abstract class | 14561 |
AbstractC3333r1.java | as.r1 | abstract class | 2426 |
AbstractC3342t0.java | as.t0 | abstract class | 3146 |
AbstractC3346u.java | as.u | abstract class | 4252 |
AbstractC3371z.java | as.z | abstract class | 2203 |
C3245a.java | as.a | class | 1067 |
C3246a0.java | as.a0 | class | 1144 |
C3249a3.java | as.a3 | class | 130 |
C3250a4.java | as.a4 | class | 1137 |
C3252b0.java | as.b0 | class | 5586 |
C3253b1.java | - | class | 17213 |
C3254b2.java | as.b2 | class | 130 |
C3256c.java | as.c | class | 3226 |
C3257c0.java | as.c0 | class | 1643 |
C3258c1.java | as.c1 | class | 4952 |
C3259c2.java | as.c2 | class | 128393 |
C3261d.java | as.d | class | 128 |
C3262d0.java | as.d0 | class | 8936 |
C3263d1.java | as.d1 | class | 130 |
C3264d2.java | as.d2 | class | 130 |
C3265d3.java | as.d3 | class | 5535 |
C3266e.java | as.e | class | 12270 |
C3267e0.java | as.e0 | class | 130 |
C3268e1.java | as.e1 | class | 11637 |
C3269e2.java | as.e2 | class | 3256 |
C3270e3.java | as.e3 | class | 130 |
C3271f.java | as.f | class | 128 |
C3272f0.java | as.f0 | class | 5028 |
C3274f2.java | as.f2 | class | 2482 |
C3275f3.java | as.f3 | class | 5260 |
C3276g.java | as.g | class | 1690 |
C3277g0.java | - | class | 61607 |
C3278g1.java | as.g1 | class | 130 |
C3279g2.java | as.g2 | class | 77197 |
C3280g3.java | as.g3 | class | 14858 |
C3281h.java | as.h | class | 1067 |
C3282h0.java | as.h0 | class | 130 |
C3283h1.java | as.h1 | class | 18434 |
C3284h2.java | as.h2 | class | 5751 |
C3285h3.java | as.h3 | class | 683 |
C3286i.java | as.i | class | 4304 |
C3287i0.java | as.i0 | class | 8301 |
C3288i1.java | as.i1 | class | 1182 |
C3289i2.java | as.i2 | class | 3235 |
C3290i3.java | as.i3 | class | 4202 |
C3291j.java | as.j | class | 5254 |
C3292j0.java | as.j0 | class | 130 |
C3293j1.java | as.j1 | class | 8965 |
C3294j2.java | as.j2 | class | 4108 |
C3295j3.java | as.j3 | class | 130 |
C3296k.java | as.k | class | 128 |
C3298k1.java | as.k1 | class | 1070 |
C3299k2.java | as.k2 | class | 130 |
C3300k3.java | as.k3 | class | 5305 |
C3301l.java | as.l | class | 7843 |
C3302l0.java | as.l0 | class | 1076 |
C3303l1.java | as.l1 | class | 13084 |
C3304l2.java | as.l2 | class | 9692 |
C3305l3.java | as.l3 | class | 130 |
C3307m0.java | as.m0 | class | 4108 |
C3308m1.java | as.m1 | class | 12378 |
C3309m2.java | as.m2 | class | 1859 |
C3310m3.java | as.m3 | class | 1070 |
C3311n.java | as.n | class | 1067 |
C3312n0.java | as.n0 | class | 130 |
C3313n1.java | as.n1 | class | 8674 |
C3314n2.java | as.n2 | class | 130 |
C3315n3.java | as.n3 | class | 4610 |
C3317o0.java | as.o0 | class | 11250 |
C3318o1.java | as.o1 | class | 130 |
C3319o2.java | as.o2 | class | 75509 |
C3320o3.java | as.o3 | class | 8928 |
C3322p0.java | as.p0 | class | 1364 |
C3323p1.java | as.p1 | class | 5155 |
C3324p2.java | as.p2 | class | 130 |
C3325p3.java | as.p3 | class | 130 |
C3327q0.java | as.q0 | class | 1070 |
C3328q1.java | as.q1 | class | 3861 |
C3330q3.java | as.q3 | class | 25532 |
C3332r0.java | as.r0 | class | 7365 |
C3335r3.java | as.r3 | class | 1244 |
C3336s.java | as.s | class | 12732 |
C3337s0.java | as.s0 | class | 8019 |
C3338s1.java | as.s1 | class | 7289 |
C3340s3.java | as.s3 | class | 130 |
C3341t.java | as.t | class | 2491 |
C3343t1.java | as.t1 | class | 130 |
C3344t2.java | as.t2 | class | 3572 |
C3345t3.java | as.t3 | class | 5501 |
C3347u0.java | as.u0 | class | 9006 |
C3348u1.java | as.u1 | class | 22035 |
C3349u2.java | - | class | 46658 |
C3350u3.java | as.u3 | class | 348 |
C3351v.java | as.v | class | 128 |
C3352v0.java | as.v0 | class | 130 |
C3353v1.java | as.v1 | class | 3397 |
C3354v2.java | as.v2 | class | 6084 |
C3355v3.java | as.v3 | class | 1070 |
C3356w.java | as.w | class | 4797 |
C3357w0.java | as.w0 | class | 4968 |
C3358w1.java | as.w1 | class | 13992 |
C3359w2.java | as.w2 | class | 130 |
C3360w3.java | as.w3 | class | 7141 |
C3361x.java | as.x | class | 128 |
C3362x0.java | as.x0 | class | 130 |
C3363x1.java | - | class | 66195 |
C3364x2.java | as.x2 | class | 6169 |
C3365x3.java | as.x3 | class | 5806 |
C3366y.java | as.y | class | 1067 |
C3367y0.java | as.y0 | class | 1076 |
C3368y1.java | as.y1 | class | 1776 |
C3369y2.java | as.y2 | class | 130 |
C3370y3.java | as.y3 | class | 130 |
C3372z0.java | as.z0 | class | 8725 |
C3373z1.java | - | class | 11558 |
C3374z2.java | as.z2 | class | 4108 |
C3375z3.java | as.z3 | class | 97158 |
RunnableC3273f1.java | as.f1 | Runnable | 2572 |
RunnableC3316o.java | as.o | Runnable | 44520 |
RunnableC3326q.java | as.q | Runnable | 20479 |
RunnableC3334r2.java | as.r2 | Runnable | 1570 |
RunnableC3339s2.java | as.s2 | Runnable | 8409 |
$k — module Registration (MISFILED: OkHttp/Okio) — 156 filesh5
| disk file | real class | kind | bytes |
|---|---|---|---|
AbstractC3595b.java | bm.b | abstract class | 6362 |
AbstractC3601c.java | bm.c | abstract class | 7502 |
AbstractC3607d.java | bm.d | abstract class | 1774 |
AbstractC3609d1.java | bm.d1 | abstract class | 9561 |
AbstractC3615e1.java | bm.e1 | abstract class | 490 |
AbstractC3627g1.java | bm.g1 | abstract class | 2179 |
AbstractC3667n.java | bm.n | abstract class | 361 |
AbstractC3671n3.java | bm.n3 | abstract class | 255 |
AbstractC3702s4.java | bm.s4 | abstract class | 8150 |
AbstractC3705t1.java | bm.t1 | abstract class | 527 |
AbstractC3711u1.java | bm.u1 | abstract class | 1253 |
AbstractC3717v1.java | bm.v1 | abstract class | 2356 |
AbstractC3723w1.java | bm.w1 | abstract class | 9693 |
AbstractC3728x0.java | bm.x0 | abstract class | 1438 |
AbstractC3734y0.java | bm.y0 | abstract class | 1412 |
AbstractC3738y4.java | bm.y4 | abstract class | 967 |
AbstractRunnableC3608d0.java | bm.d0 | abstract class | 1782 |
C3590a0.java | bm.a0 | class | 12208 |
C3591a1.java | bm.a1 | class | 182 |
C3593a3.java | bm.a3 | class | 2132 |
C3594a4.java | bm.a4 | class | 4348 |
C3597b1.java | bm.b1 | class | 659 |
C3599b3.java | bm.b3 | class | 923 |
C3600b4.java | bm.b4 | class | 724 |
C3602c0.java | bm.c0 | class | 4564 |
C3604c2.java | bm.c2 | class | 489 |
C3605c3.java | bm.c3 | class | 11313 |
C3606c4.java | bm.c4 | class | 5788 |
C3610d2.java | bm.d2 | class | 2667 |
C3611d3.java | bm.d3 | class | 10747 |
C3613e.java | bm.e | class | 17475 |
C3616e2.java | bm.e2 | class | 1828 |
C3617e3.java | bm.e3 | class | 373 |
C3619f.java | bm.f | class | 877 |
C3621f1.java | bm.f1 | class | 1945 |
C3622f2.java | - | class | 26205 |
C3623f3.java | bm.f3 | class | 1615 |
C3624f4.java | bm.f4 | class | 573 |
C3625g.java | bm.g | class | 1448 |
C3626g0.java | bm.g0 | class | 3492 |
C3628g2.java | bm.g2 | class | 3763 |
C3629g3.java | bm.g3 | class | 1336 |
C3630g4.java | bm.g4 | class | 2141 |
C3631h.java | bm.h | class | 866 |
C3632h0.java | bm.h0 | class | 1554 |
C3635h3.java | bm.h3 | class | 4208 |
C3636h4.java | bm.h4 | class | 2610 |
C3637i.java | bm.i | class | 199 |
C3638i0.java | bm.i0 | class | 2798 |
C3639i1.java | bm.i1 | class | 4239 |
C3640i2.java | bm.i2 | class | 2934 |
C3641i3.java | bm.i3 | class | 1888 |
C3642i4.java | bm.i4 | class | 395 |
C3643j.java | bm.j | class | 1974 |
C3645j1.java | bm.j1 | class | 8078 |
C3646j2.java | bm.j2 | class | 1511 |
C3647j3.java | bm.j3 | class | 1944 |
C3648j4.java | bm.j4 | class | 2208 |
C3649k.java | bm.k | class | 2690 |
C3650k0.java | bm.k0 | class | 14545 |
C3651k1.java | bm.k1 | class | 2243 |
C3652k2.java | bm.k2 | class | 1691 |
C3653k3.java | bm.k3 | class | 5814 |
C3654k4.java | bm.k4 | class | 2533 |
C3655l.java | bm.l | class | 1195 |
C3656l0.java | bm.l0 | class | 9642 |
C3658l2.java | bm.l2 | class | 13111 |
C3659l3.java | bm.l3 | class | 2556 |
C3660l4.java | bm.l4 | class | 706 |
C3661m.java | bm.m | class | 2919 |
C3664m2.java | bm.m2 | class | 4134 |
C3665m3.java | bm.m3 | class | 4169 |
C3669n1.java | bm.n1 | class | 1341 |
C3672n4.java | bm.n4 | class | 1146 |
C3673o.java | bm.o | class | 2236 |
C3674o0.java | bm.o0 | class | 4630 |
C3675o1.java | bm.o1 | class | 6382 |
C3676o2.java | bm.o2 | class | 7131 |
C3677o3.java | bm.o3 | class | 3664 |
C3678o4.java | bm.o4 | class | 1075 |
C3679p.java | bm.p | class | 5745 |
C3682p2.java | - | class | 20820 |
C3685q.java | bm.q | class | 2915 |
C3687q1.java | - | class | 15867 |
C3688q2.java | bm.q2 | class | 11688 |
C3689q3.java | bm.q3 | class | 1274 |
C3690q4.java | bm.q4 | class | 1624 |
C3691r.java | bm.r | class | 4205 |
C3693r1.java | bm.r1 | class | 1260 |
C3694r2.java | bm.r2 | class | 3953 |
C3695r3.java | bm.r3 | class | 543 |
C3696r4.java | bm.r4 | class | 1760 |
C3697s.java | - | class | 20053 |
C3698s0.java | bm.s0 | class | 19474 |
C3699s1.java | bm.s1 | class | 9133 |
C3700s2.java | bm.s2 | class | 628 |
C3704t0.java | bm.t0 | class | 1450 |
C3706t2.java | bm.t2 | class | 7204 |
C3707t3.java | bm.t3 | class | 1225 |
C3708t4.java | bm.t4 | class | 557 |
C3710u0.java | bm.u0 | class | 1055 |
C3712u2.java | bm.u2 | class | 948 |
C3713u3.java | bm.u3 | class | 868 |
C3716v0.java | bm.v0 | class | 2102 |
C3718v2.java | bm.v2 | class | 6493 |
C3719v3.java | bm.v3 | class | 1016 |
C3720v4.java | bm.v4 | class | 3471 |
C3722w0.java | bm.w0 | class | 2646 |
C3725w3.java | bm.w3 | class | 919 |
C3726w4.java | bm.w4 | class | 1062 |
C3727x.java | bm.x | class | 1107 |
C3731x3.java | bm.x3 | class | 794 |
C3735y1.java | bm.y1 | class | 421 |
C3736y2.java | bm.y2 | class | 3777 |
C3741z1.java | bm.z1 | class | 4272 |
C3742z2.java | bm.z2 | class | 10538 |
C3743z3.java | bm.z3 | class | 2516 |
C3744z4.java | bm.z4 | class | 22908 |
EnumC3603c1.java | bm.c1 | enum | 3874 |
EnumC3680p0.java | bm.p0 | enum | 1170 |
EnumC3709u.java | bm.u | enum | 1735 |
ExecutorC3634h2.java | bm.h2 | Executor | 1961 |
ExecutorC3666m4.java | bm.m4 | Executor | 2095 |
ExecutorC3684p4.java | bm.p4 | Executor | 3814 |
InterfaceC3596b0.java | bm.b0 | interface | 162 |
InterfaceC3598b2.java | bm.b2 | interface | 196 |
InterfaceC3692r0.java | bm.r0 | interface | 137 |
InterfaceC3703t.java | bm.t | interface | 805 |
InterfaceC3714u4.java | bm.u4 | interface | 269 |
InterfaceC3715v.java | bm.v | interface | 528 |
InterfaceC3721w.java | bm.w | interface | 430 |
InterfaceC3724w2.java | bm.w2 | interface | 137 |
InterfaceC3730x2.java | bm.x2 | interface | 355 |
InterfaceC3732x4.java | bm.x4 | interface | 522 |
InterfaceC3733y.java | bm.y | interface | 159 |
InterfaceC3737y3.java | bm.y3 | interface | 211 |
InterfaceC3739z.java | bm.z | interface | 251 |
InterfaceC3740z0.java | bm.z0 | interface | 501 |
RunnableC3589a.java | - | Runnable | 38126 |
RunnableC3592a2.java | bm.a2 | Runnable | 1336 |
RunnableC3612d4.java | bm.d4 | Runnable | 1354 |
RunnableC3614e0.java | - | Runnable | 34723 |
RunnableC3618e4.java | bm.e4 | Runnable | 1456 |
RunnableC3620f0.java | bm.f0 | Runnable | 2797 |
RunnableC3633h1.java | bm.h1 | Runnable | 1302 |
RunnableC3644j0.java | bm.j0 | Runnable | 1344 |
RunnableC3657l1.java | bm.l1 | Runnable | 1811 |
RunnableC3662m0.java | bm.m0 | Runnable | 1234 |
RunnableC3663m1.java | - | Runnable | 13997 |
RunnableC3668n0.java | bm.n0 | Runnable | 1113 |
RunnableC3681p1.java | bm.p1 | Runnable | 2358 |
RunnableC3683p3.java | bm.p3 | Runnable | 1737 |
RunnableC3686q0.java | - | Runnable | 23255 |
RunnableC3701s3.java | - | Runnable | 40676 |
RunnableC3729x1.java | bm.x1 | Runnable | 3450 |
ScheduledExecutorServiceC3670n2.java | bm.n2 | ScheduledExecutorService | 5729 |
$k — module Billing — 49 filesh5
| disk file | real class | kind | bytes |
|---|---|---|---|
AbstractC15820h.java | - | abstract class | 21073 |
C15806a.java | d6.a | class | 4358 |
C15807a0.java | d6.a0 | class | 6655 |
C15808b.java | d6.b | class | 2254 |
C15810c.java | d6.c | class | 13131 |
C15812d.java | d6.d | class | 7110 |
C15814e.java | d6.e | class | 4835 |
C15817f0.java | d6.f0 | class | 1484 |
C15818g.java | d6.g | class | 1186 |
C15821h0.java | d6.h0 | class | 2413 |
C15822i.java | d6.i | class | 7286 |
C15824j.java | - | class | 15074 |
C15825j0.java | d6.j0 | class | 2097 |
C15826k.java | d6.k | class | 2506 |
C15827k0.java | d6.k0 | class | 3378 |
C15828l.java | d6.l | class | 5367 |
C15829l0.java | d6.l0 | class | 2356 |
C15830m.java | d6.m | class | 2089 |
C15831m0.java | d6.m0 | class | 3161 |
C15832n.java | d6.n | class | 1405 |
C15833n0.java | - | class | 61059 |
C15834o.java | - | class | 28132 |
C15837p0.java | d6.p0 | class | 1547 |
C15838q.java | d6.q | class | 1614 |
C15839q0.java | d6.q0 | class | 2197 |
C15840r.java | d6.r | class | 9380 |
C15841r0.java | d6.r0 | class | 860 |
C15843s0.java | d6.s0 | class | 2367 |
C15849v0.java | - | class | 20439 |
C15850w.java | d6.w | class | 1483 |
C15851w0.java | d6.w0 | class | 818 |
C15852x.java | d6.x | class | 2815 |
C15853x0.java | d6.x0 | class | 10298 |
C15854y.java | d6.y | class | 12567 |
C15855y0.java | d6.y0 | class | 9114 |
RunnableC15815e0.java | d6.e0 | Runnable | 11150 |
RunnableC15819g0.java | d6.g0 | Runnable | 2507 |
RunnableC15835o0.java | d6.o0 | Runnable | 17852 |
RunnableC15842s.java | d6.s | Runnable | 1960 |
RunnableC15845t0.java | d6.t0 | Runnable | 7915 |
RunnableC15846u.java | d6.u | Runnable | 2502 |
RunnableC15847u0.java | d6.u0 | Runnable | 2199 |
RunnableC15856z.java | d6.z | Runnable | 982 |
ViewOnClickListenerC15809b0.java | d6.b0 | inner listener class | 7442 |
ViewOnClickListenerC15811c0.java | d6.c0 | inner listener class | 4355 |
ViewOnClickListenerC15813d0.java | d6.d0 | inner listener class | 8607 |
ViewOnClickListenerC15836p.java | d6.p | inner listener class | 2520 |
ViewOnClickListenerC15848v.java | - | inner listener class | 7649 |
ViewOnTouchListenerC15816f.java | - | inner listener class | 15849 |
$k — module Registration — 26 filesh5
| disk file | real class | kind | bytes |
|---|---|---|---|
AnimationAnimationListenerC21861p.java | y5.p | AnimationListener | 2132 |
C21848c.java | y5.c | class | 7381 |
C21849d.java | y5.d | class | 1105 |
C21850e.java | y5.e | class | 6449 |
C21851f.java | - | class | 100901 |
C21853h.java | y5.h | class | 1561 |
C21854i.java | y5.i | class | 3044 |
C21856k.java | - | class | 146166 |
C21857l.java | y5.l | class | 1951 |
C21860o.java | y5.o | class | 2545 |
C21862q.java | y5.q | class | 2821 |
C21863r.java | y5.r | class | 10780 |
C21867v.java | - | class | 42860 |
C21868w.java | y5.w | class | 6370 |
C21869x.java | y5.x | class | 1029 |
EnumC21846a.java | y5.a | enum | 1442 |
GestureDetectorOnGestureListenerC21858m.java | y5.m | GestureListener | 7809 |
InterfaceC21847b.java | y5.b | interface | 422 |
InterfaceC21855j.java | y5.j | interface | 280 |
InterfaceC21859n.java | y5.n | interface | 597 |
InterfaceC21864s.java | y5.s | interface | 269 |
InterfaceC21870y.java | y5.y | interface | 140 |
InterfaceC21871z.java | y5.z | interface | 140 |
RunnableC21852g.java | y5.g | Runnable | 3347 |
RunnableC21865t.java | y5.t | Runnable | 1616 |
RunnableC21866u.java | y5.u | Runnable | 2157 |
$k — module IdPlus — 22 filesh5
| disk file | real class | kind | bytes |
|---|---|---|---|
C16005f.java | ds.f | class | 4087 |
C16006g.java | ds.g | class | 26077 |
C16007h.java | ds.h | class | 2430 |
C16008i.java | ds.i | class | 6712 |
C16009j.java | ds.j | class | 2206 |
C16010k.java | ds.k | class | 9649 |
C16011l.java | ds.l | class | 10113 |
C16012m.java | ds.m | class | 3012 |
C16013n.java | ds.n | class | 1436 |
C16014o.java | ds.o | class | 53340 |
C16015p.java | ds.p | class | 6620 |
C16016q.java | ds.q | class | 6740 |
C16017r.java | ds.r | class | 3741 |
C16018s.java | ds.s | class | 13198 |
C16019t.java | ds.t | class | 95102 |
C16020u.java | ds.u | class | 13443 |
C16021v.java | ds.v | class | 4104 |
C16022w.java | ds.w | class | 1824 |
RunnableC16000a.java | ds.a | Runnable | 6850 |
RunnableC16001b.java | ds.b | Runnable | 10060 |
RunnableC16004e.java | - | Runnable | 38831 |
ViewOnClickListenerC16003d.java | ds.d | inner listener class | 4420 |
$k — module Registration — 19 filesh5
| disk file | real class | kind | bytes |
|---|---|---|---|
C3507a.java | b8.a | class | 8034 |
C3508b.java | b8.b | class | 154957 |
C3509c.java | b8.c | class | 10170 |
C3510d.java | b8.d | class | 4842 |
C3511e.java | b8.e | class | 5263 |
C3512f.java | b8.f | class | 1254 |
C3513g.java | - | class | 25061 |
C3514h.java | b8.h | class | 5548 |
C3515i.java | b8.i | class | 9696 |
C3516j.java | b8.j | class | 1717 |
C3517k.java | b8.k | class | 5144 |
C3518l.java | b8.l | class | 5987 |
C3519m.java | b8.m | class | 3965 |
C3520n.java | b8.n | class | 3694 |
C3521o.java | b8.o | class | 6441 |
C3522p.java | b8.p | class | 10301 |
C3523q.java | b8.q | class | 4223 |
C3524r.java | b8.r | class | 5790 |
C3525s.java | b8.s | class | 5727 |
$k — module Registration — 52 filesh5
| disk file | real class | kind | bytes |
|---|---|---|---|
AbstractC17562a.java | jd.a | abstract class | 6003 |
AbstractC17567b1.java | jd.b1 | abstract class | 3333 |
AbstractC17578h.java | jd.h | abstract class | 4829 |
AbstractC17584k.java | jd.k | abstract class | 3372 |
C17563a0.java | jd.a0 | class | 302 |
C17564a1.java | jd.a1 | class | 2393 |
C17565b.java | jd.b | class | 3143 |
C17566b0.java | jd.b0 | class | 3998 |
C17568c.java | jd.c | class | 12957 |
C17570d.java | jd.d | class | 5310 |
C17571d0.java | jd.d0 | class | 4144 |
C17572e.java | jd.e | class | 6188 |
C17573e0.java | jd.e0 | class | 6944 |
C17575f0.java | jd.f0 | class | 1791 |
C17576g.java | jd.g | class | 692 |
C17577g0.java | jd.g0 | class | 14440 |
C17579h0.java | jd.h0 | class | 8191 |
C17582j.java | jd.j | class | 14954 |
C17583j0.java | jd.j0 | class | 7178 |
C17585k0.java | jd.k0 | class | 7107 |
C17586l.java | jd.l | class | 4381 |
C17587l0.java | jd.l0 | class | 1136 |
C17588m.java | jd.m | class | 545 |
C17589m0.java | - | class | 33177 |
C17590n.java | jd.n | class | 8068 |
C17591n0.java | jd.n0 | class | 984 |
C17592o.java | jd.o | class | 2607 |
C17593o0.java | jd.o0 | class | 1872 |
C17594p.java | jd.p | class | 2521 |
C17595p0.java | jd.p0 | class | 6814 |
C17596q.java | jd.q | class | 9400 |
C17597q0.java | jd.q0 | class | 2177 |
C17598r.java | jd.r | class | 851 |
C17599r0.java | jd.r0 | class | 10592 |
C17601s0.java | jd.s0 | class | 596 |
C17603t0.java | jd.t0 | class | 563 |
C17604u.java | jd.u | class | 2276 |
C17605u0.java | jd.u0 | class | 30644 |
C17608w.java | jd.w | class | 830 |
C17611x0.java | jd.x0 | class | 2408 |
C17613y0.java | jd.y0 | class | 4223 |
C17615z0.java | jd.z0 | class | 3661 |
InterfaceC17569c0.java | jd.c0 | interface | 787 |
InterfaceC17600s.java | jd.s | interface | 330 |
InterfaceC17602t.java | jd.t | interface | 805 |
InterfaceC17606v.java | jd.v | interface | 255 |
InterfaceC17607v0.java | jd.v0 | interface | 374 |
InterfaceC17609w0.java | jd.w0 | interface | 322 |
InterfaceC17610x.java | jd.x | interface | 301 |
RunnableC17581i0.java | jd.i0 | Runnable | 1332 |
RunnableC17612y.java | jd.y | Runnable | 2222 |
RunnableC17614z.java | - | Runnable | 20572 |
$k — module Registration — 58 filesh5
| disk file | real class | kind | bytes |
|---|---|---|---|
AbstractC19642b.java | q7.b | abstract class | 3159 |
C19640a0.java | q7.a0 | class | 5399 |
C19641a1.java | q7.a1 | class | 871 |
C19644b1.java | q7.b1 | class | 13762 |
C19647c1.java | q7.c1 | class | 4093 |
C19650d1.java | q7.d1 | class | 1315 |
C19651e.java | q7.e | class | 1559 |
C19655f0.java | q7.f0 | class | 4016 |
C19657g.java | q7.g | class | 3493 |
C19663i.java | q7.i | class | 520 |
C19665i1.java | q7.i1 | class | 2679 |
C19666j.java | q7.j | class | 2529 |
C19667j0.java | q7.j0 | class | 4031 |
C19670k0.java | q7.k0 | class | 1628 |
C19673l0.java | - | class | 26321 |
C19674l1.java | q7.l1 | class | 10538 |
C19675m.java | q7.m | class | 2477 |
C19677n.java | q7.n | class | 14091 |
C19678n0.java | q7.n0 | class | 6722 |
C19681p.java | q7.p | class | 4236 |
C19682p0.java | q7.p0 | class | 1247 |
C19683q.java | q7.q | class | 516 |
C19684q0.java | q7.q0 | class | 135 |
C19685r.java | q7.r | class | 2967 |
C19686r0.java | q7.r0 | class | 3484 |
C19687s.java | q7.s | class | 3753 |
C19689t.java | q7.t | class | 4578 |
C19690t0.java | q7.t0 | class | 5042 |
C19692u0.java | q7.u0 | class | 1811 |
C19698x0.java | q7.x0 | class | 5884 |
C19699y.java | q7.y | class | 594 |
C19701z.java | q7.z | class | 6083 |
C19702z0.java | q7.z0 | class | 1933 |
DialogInterfaceOnClickListenerC19679o.java | q7.o | Dialog | 526 |
DialogInterfaceOnClickListenerC19680o0.java | q7.o0 | Dialog | 544 |
EnumC19653e1.java | q7.e1 | enum | 3581 |
EnumC19659g1.java | q7.g1 | enum | 802 |
EnumC19662h1.java | q7.h1 | enum | 1207 |
InterfaceC19639a.java | q7.a | interface | 275 |
InterfaceC19656f1.java | q7.f1 | interface | 226 |
InterfaceC19668j1.java | q7.j1 | interface | 1133 |
InterfaceC19696w0.java | q7.w0 | interface | 242 |
InterfaceC19700y0.java | q7.y0 | interface | 212 |
RunnableC19643b0.java | q7.b0 | Runnable | 9494 |
RunnableC19645c.java | q7.c | Runnable | 2195 |
RunnableC19648d.java | q7.d | Runnable | 1117 |
RunnableC19660h.java | q7.h | Runnable | 3457 |
RunnableC19661h0.java | q7.h0 | Runnable | 3101 |
RunnableC19672l.java | q7.l | Runnable | 6104 |
RunnableC19676m0.java | q7.m0 | Runnable | 1331 |
RunnableC19688s0.java | q7.s0 | Runnable | 1837 |
RunnableC19693v.java | q7.v | Runnable | 5804 |
RunnableC19695w.java | q7.w | Runnable | 236 |
RunnableC19697x.java | q7.x | Runnable | 1239 |
ViewOnClickListenerC19646c0.java | q7.c0 | inner listener class | 7200 |
ViewOnClickListenerC19669k.java | q7.k | inner listener class | 3087 |
ViewOnClickListenerC19691u.java | q7.u | inner listener class | 316 |
ViewOnFocusChangeListenerC19664i0.java | q7.i0 | inner listener class | 2423 |
$k — module IdPlus — 16 filesh5
| disk file | real class | kind | bytes |
|---|---|---|---|
AbstractC19843n.java | - | abstract class | 77381 |
C19830a.java | r4.a | class | 3191 |
C19832c.java | r4.c | class | 1229 |
C19833d.java | - | class | 31177 |
C19835f.java | r4.f | class | 9017 |
C19836g.java | r4.g | class | 7127 |
C19837h.java | r4.h | class | 28214 |
C19838i.java | r4.i | class | 151 |
C19839j.java | r4.j | class | 6091 |
C19840k.java | r4.k | class | 8516 |
C19841l.java | r4.l | class | 5830 |
C19844o.java | r4.o | class | 817 |
C19845p.java | r4.p | class | 4376 |
RunnableC19831b.java | r4.b | Runnable | 1732 |
RunnableC19834e.java | r4.e | Runnable | 5674 |
RunnableC19842m.java | r4.m | Runnable | 1333 |
$k — module IdPlus — 25 filesh5
| disk file | real class | kind | bytes |
|---|---|---|---|
AbstractC15918p.java | df.p | abstract class | 4236 |
C15906d.java | df.d | class | 1663 |
C15907e.java | df.e | class | 11789 |
C15908f.java | df.f | class | 1266 |
C15909g.java | df.g | class | 1043 |
C15910h.java | df.h | class | 1879 |
C15911i.java | df.i | class | 2950 |
C15913k.java | df.k | class | 11145 |
C15914l.java | df.l | class | 1044 |
C15915m.java | df.m | class | 1510 |
C15917o.java | df.o | class | 22235 |
C15919q.java | df.q | class | 2501 |
C15920r.java | df.r | class | 1079 |
C15921s.java | df.s | class | 13058 |
C15922t.java | df.t | class | 1553 |
C15923u.java | df.u | class | 3614 |
C15924v.java | df.v | class | 14964 |
C15925w.java | df.w | class | 3915 |
C15926x.java | df.x | class | 9902 |
C15927y.java | df.y | class | 2242 |
C15928z.java | df.z | class | 4451 |
InterfaceC15903a0.java | df.a0 | interface | 142 |
RunnableC15905c.java | - | Runnable | 60956 |
ViewOnAttachStateChangeListenerC15916n.java | df.n | inner listener class | 2842 |
ViewOnFocusChangeListenerC15902a.java | df.a | inner listener class | 4878 |
$k — module Registration — 30 filesh5
| disk file | real class | kind | bytes |
|---|---|---|---|
AbstractC19534a0.java | q1.a0 | abstract class | 3505 |
AbstractC19535b.java | q1.b | abstract class | 478 |
AbstractC19538c0.java | q1.c0 | abstract class | 10004 |
BinderC19560w.java | q1.w | Binder | 775 |
C19533a.java | q1.a | class | 3329 |
C19537c.java | - | class | 54912 |
C19540d0.java | q1.d0 | class | 5311 |
C19541e.java | q1.e | class | 774 |
C19543f.java | q1.f | class | 815 |
C19544g.java | q1.g | class | 951 |
C19545h.java | q1.h | class | 133 |
C19546i.java | q1.i | class | 4668 |
C19547j.java | q1.j | class | 1133 |
C19548k.java | q1.k | class | 2898 |
C19549l.java | q1.l | class | 5891 |
C19553p.java | q1.p | class | 558 |
C19558u.java | q1.u | class | 1324 |
C19559v.java | q1.v | class | 1938 |
C19561x.java | q1.x | class | 12553 |
C19562y.java | q1.y | class | 427 |
C19563z.java | q1.z | class | 540 |
CallableC19554q.java | q1.q | Callable | 21771 |
InterfaceC19536b0.java | q1.b0 | interface | 765 |
InterfaceC19539d.java | q1.d | interface | 235 |
InterfaceC19550m.java | q1.m | interface | 250 |
InterfaceC19551n.java | q1.n | interface | 252 |
InterfaceC19552o.java | q1.o | interface | 252 |
RunnableC19542e0.java | - | Runnable | 33197 |
ServiceConnectionC19557t.java | q1.t | ServiceConnection | 6595 |
ThreadFactoryC19556s.java | q1.s | ThreadFactory | 986 |
$k — module Billing — 13 filesh5
| disk file | real class | kind | bytes |
|---|---|---|---|
AbstractActivityC3498e.java | - | abstract class | 42971 |
C3495b.java | b7.b | class | 1681 |
C3500g.java | b7.g | class | 2432 |
C3502i.java | - | class | 40512 |
C3504k.java | - | class | 21183 |
C3506m.java | b7.m | class | 1703 |
RunnableC3494a.java | b7.a | Runnable | 6692 |
RunnableC3496c.java | b7.c | Runnable | 1583 |
RunnableC3497d.java | - | Runnable | 22079 |
RunnableC3499f.java | b7.f | Runnable | 1481 |
RunnableC3501h.java | b7.h | Runnable | 1652 |
RunnableC3503j.java | b7.j | Runnable | 1795 |
RunnableC3505l.java | - | Runnable | 29144 |
$k — module Billing — 25 filesh5
| disk file | real class | kind | bytes |
|---|---|---|---|
AbstractC3871c.java | c7.c | abstract class | 8399 |
AbstractC3872d.java | c7.d | abstract class | 3470 |
AbstractC3873e.java | c7.e | abstract class | 592 |
C3869a.java | c7.a | class | 4608 |
C3877i.java | c7.i | class | 1610 |
C3878j.java | c7.j | class | 15701 |
C3879k.java | c7.k | class | 283 |
C3883o.java | c7.o | class | 2892 |
C3886r.java | c7.r | class | 12232 |
C3888t.java | c7.t | class | 21772 |
C3889u.java | c7.u | class | 1213 |
C3890v.java | c7.v | class | 6638 |
C3891w.java | c7.w | class | 1408 |
C3892x.java | c7.x | class | 2964 |
C3894z.java | c7.z | class | 2968 |
EnumC3876h.java | c7.h | enum | 1775 |
RunnableC3870b.java | - | Runnable | 20920 |
RunnableC3882n.java | c7.n | Runnable | 2174 |
ViewOnClickListenerC3874f.java | c7.f | inner listener class | 1513 |
ViewOnClickListenerC3875g.java | c7.g | inner listener class | 1318 |
ViewOnClickListenerC3880l.java | - | inner listener class | 20754 |
ViewOnClickListenerC3884p.java | c7.p | inner listener class | 1428 |
ViewOnClickListenerC3885q.java | c7.q | inner listener class | 1276 |
ViewOnClickListenerC3893y.java | c7.y | inner listener class | 1736 |
ViewOnLongClickListenerC3881m.java | c7.m | inner listener class | 3521 |
$k — module Registration — 16 filesh5
| disk file | real class | kind | bytes |
|---|---|---|---|
C18487a.java | m5.a | class | 12332 |
C18488b.java | m5.b | class | 6443 |
C18489c.java | m5.c | class | 5603 |
C18491e.java | - | class | 11869 |
C18493g.java | m5.g | class | 34959 |
C18496j.java | m5.j | class | 15199 |
C18498l.java | m5.l | class | 5522 |
C18499m.java | m5.m | class | 1152 |
C18501o.java | m5.o | class | 1498 |
C18502p.java | m5.p | class | 26377 |
C18503q.java | m5.q | class | 3658 |
C18505s.java | m5.s | class | 5139 |
InterfaceC18495i.java | m5.i | interface | 228 |
RunnableC18490d.java | m5.d | Runnable | 1117 |
RunnableC18500n.java | m5.n | Runnable | 3746 |
RunnableC18504r.java | m5.r | Runnable | 2547 |
$k — module Billing — 29 filesh5
| disk file | real class | kind | bytes |
|---|---|---|---|
AbstractC17503r.java | j5.r | abstract class | 13770 |
C17483a.java | j5.a | class | 1352 |
C17484a0.java | j5.a0 | class | 2211 |
C17485b.java | j5.b | class | 5318 |
C17487c.java | j5.c | class | 7295 |
C17488c0.java | j5.c0 | class | 3961 |
C17491f.java | j5.f | class | 3500 |
C17492g.java | j5.g | class | 9450 |
C17493h.java | j5.h | class | 930 |
C17495j.java | j5.j | class | 5300 |
C17496k.java | j5.k | class | 8186 |
C17498m.java | j5.m | class | 2700 |
C17500o.java | j5.o | class | 2110 |
C17501p.java | j5.p | class | 1129 |
C17502q.java | - | class | 18571 |
C17504s.java | j5.s | class | 473 |
C17506u.java | j5.u | class | 3940 |
C17507v.java | j5.v | class | 5817 |
C17510y.java | j5.y | class | 1450 |
C17511z.java | j5.z | class | 4105 |
EnumC17486b0.java | j5.b0 | enum | 1385 |
EnumC17505t.java | j5.t | enum | 3008 |
RunnableC17508w.java | j5.w | Runnable | 978 |
ViewOnClickListenerC17489d.java | j5.d | inner listener class | 1716 |
ViewOnClickListenerC17490e.java | j5.e | inner listener class | 3511 |
ViewOnClickListenerC17494i.java | j5.i | inner listener class | 3618 |
ViewOnClickListenerC17497l.java | j5.l | inner listener class | 5607 |
ViewOnClickListenerC17499n.java | j5.n | inner listener class | 2052 |
ViewOnTouchListenerC17509x.java | j5.x | inner listener class | 1754 |
$k — module Registration — 14 filesh5
| disk file | real class | kind | bytes |
|---|---|---|---|
AbstractC16507i.java | - | abstract class | 42027 |
AbstractC16508j.java | f8.j | abstract class | 1912 |
C16500b.java | f8.b | class | 1506 |
C16501c.java | f8.c | class | 2884 |
C16502d.java | f8.d | class | 1758 |
C16503e.java | f8.e | class | 1097 |
C16505g.java | f8.g | class | 4834 |
C16510l.java | f8.l | class | 5901 |
C16511m.java | f8.m | class | 4431 |
C16512n.java | f8.n | class | 1816 |
CallableC16509k.java | f8.k | Callable | 33176 |
ViewOnClickListenerC16499a.java | f8.a | inner listener class | 1770 |
ViewOnTouchListenerC16506h.java | f8.h | inner listener class | 1882 |
ViewTreeObserverOnGlobalLayoutListenerC16504f.java | f8.f | class | 16156 |
$k — module Registration — 40 filesh5
| disk file | real class | kind | bytes |
|---|---|---|---|
C21073b0.java | vk.b0 | class | 1419 |
C21097j0.java | vk.j0 | class | 1290 |
C21119u.java | vk.u | class | 669 |
DialogInterfaceOnClickListenerC21121v.java | vk.v | Dialog | 1867 |
RunnableC21069a.java | vk.a | Runnable | 1270 |
RunnableC21071a1.java | vk.a1 | Runnable | 1728 |
RunnableC21072b.java | vk.b | Runnable | 1037 |
RunnableC21075c.java | vk.c | Runnable | 2537 |
RunnableC21077c1.java | vk.c1 | Runnable | 1198 |
RunnableC21079d0.java | vk.d0 | Runnable | 1390 |
RunnableC21080d1.java | vk.d1 | Runnable | 1272 |
RunnableC21081e.java | vk.e | Runnable | 1195 |
RunnableC21082e0.java | - | Runnable | 30111 |
RunnableC21083e1.java | vk.e1 | Runnable | 1367 |
RunnableC21084f.java | vk.f | Runnable | 1253 |
RunnableC21086f1.java | vk.f1 | Runnable | 1688 |
RunnableC21087g.java | vk.g | Runnable | 1037 |
RunnableC21088g0.java | vk.g0 | Runnable | 2751 |
RunnableC21091h0.java | vk.h0 | Runnable | 1859 |
RunnableC21093i.java | - | Runnable | 33676 |
RunnableC21094i0.java | vk.i0 | Runnable | 1228 |
RunnableC21096j.java | vk.j | Runnable | 1214 |
RunnableC21099k.java | vk.k | Runnable | 1319 |
RunnableC21103m.java | vk.m | Runnable | 1342 |
RunnableC21105n.java | vk.n | Runnable | 1399 |
RunnableC21107o.java | vk.o | Runnable | 1461 |
RunnableC21108o0.java | vk.o0 | Runnable | 1228 |
RunnableC21110p0.java | vk.p0 | Runnable | 1935 |
RunnableC21112q0.java | vk.q0 | Runnable | 1256 |
RunnableC21113r.java | vk.r | Runnable | 1131 |
RunnableC21114r0.java | vk.r0 | Runnable | 1253 |
RunnableC21115s.java | vk.s | Runnable | 1250 |
RunnableC21116s0.java | vk.s0 | Runnable | 1400 |
RunnableC21117t.java | vk.t | Runnable | 1253 |
RunnableC21120u0.java | vk.u0 | Runnable | 1325 |
RunnableC21126x0.java | vk.x0 | Runnable | 1307 |
RunnableC21128y0.java | vk.y0 | Runnable | 1848 |
RunnableC21129z.java | vk.z | Runnable | 1695 |
RunnableC21130z0.java | vk.z0 | Runnable | 1134 |
ViewOnTouchListenerC21127y.java | vk.y | inner listener class | 1384 |
$k — module Billing — 30 filesh5
| disk file | real class | kind | bytes |
|---|---|---|---|
AbstractC21520b.java | x.b | abstract class | 142 |
AbstractC21524d.java | x.d | abstract class | 1208 |
AbstractC21527e0.java | x.e0 | abstract class | 1718 |
AbstractC21529f0.java | x.f0 | abstract class | 2703 |
AbstractC21539m.java | x.m | abstract class | 19683 |
C21519a0.java | x.a0 | class | 1516 |
C21521b0.java | x.b0 | class | 875 |
C21523c0.java | x.c0 | class | 4910 |
C21525d0.java | x.d0 | class | 461 |
C21526e.java | x.e | class | 1395 |
C21532h.java | x.h | class | 2250 |
C21533h0.java | x.h0 | class | 429 |
C21534i.java | x.i | class | 4304 |
C21535i0.java | x.i0 | class | 682 |
C21540n.java | x.n | class | 1227 |
C21541o.java | x.o | class | 1221 |
C21543q.java | x.q | class | 2244 |
C21546t.java | x.t | class | 974 |
C21548v.java | - | class | 35826 |
C21550x.java | x.x | class | 925 |
CallableC21536j.java | x.j | Callable | 10762 |
EnumC21518a.java | x.a | enum | 1409 |
EnumC21530g.java | x.g | enum | 2216 |
EnumC21531g0.java | x.g0 | enum | 1600 |
EnumC21549w.java | x.w | enum | 1183 |
InterfaceC21522c.java | x.c | interface | 138 |
InterfaceC21547u.java | x.u | interface | 155 |
InterfaceC21551y.java | x.y | interface | 170 |
InterfaceC21552z.java | x.z | interface | 4950 |
RunnableC21545s.java | x.s | Runnable | 2281 |
$k — module IdPlus — 16 filesh5
| disk file | real class | kind | bytes |
|---|---|---|---|
C16918a.java | h6.a | class | 3595 |
C16919b.java | h6.b | class | 5640 |
C16920c.java | h6.c | class | 5795 |
C16924g.java | h6.g | class | 15181 |
C16925h.java | h6.h | class | 4697 |
C16926i.java | h6.i | class | 2133 |
C16927j.java | h6.j | class | 4884 |
C16930m.java | h6.m | class | 2261 |
C16931n.java | h6.n | class | 1063 |
C16932o.java | h6.o | class | 1063 |
C16933p.java | h6.p | class | 10054 |
C16934q.java | h6.q | class | 1524 |
C16935r.java | h6.r | class | 12626 |
C16936s.java | h6.s | class | 8528 |
ViewOnClickListenerC16928k.java | h6.k | inner listener class | 7960 |
ViewOnClickListenerC16929l.java | h6.l | inner listener class | 2500 |
$k — module IdPlus — 22 filesh5
| disk file | real class | kind | bytes |
|---|---|---|---|
AbstractC19051d.java | o4.d | abstract class | 6569 |
AbstractC19054g.java | - | abstract class | 23767 |
AbstractC19055h.java | o4.h | abstract class | 738 |
AbstractC19058k.java | o4.k | abstract class | 212 |
C19052e.java | o4.e | class | 507 |
C19053f.java | o4.f | class | 780 |
C19057j.java | o4.j | class | 3810 |
C19061n.java | o4.n | class | 1822 |
C19062o.java | o4.o | class | 2029 |
C19063p.java | o4.p | class | 1960 |
C19064q.java | o4.q | class | 8212 |
C19066s.java | o4.s | class | 4162 |
C19067t.java | o4.t | class | 469 |
C19068u.java | o4.u | class | 7338 |
C19069v.java | o4.v | class | 5524 |
C19070w.java | o4.w | class | 2056 |
C19071x.java | o4.x | class | 3355 |
InterfaceC19056i.java | o4.i | interface | 501 |
RunnableC19059l.java | o4.l | Runnable | 4374 |
RunnableC19065r.java | o4.r | Runnable | 423 |
ViewOnClickListenerC19049b.java | o4.b | inner listener class | 2064 |
ViewOnTouchListenerC19050c.java | o4.c | inner listener class | 2243 |
$k — module IdPlus — 5 filesh5
| disk file | real class | kind | bytes |
|---|---|---|---|
AbstractActivityC3899e.java | - | abstract class | 46623 |
C3897c.java | c8.c | class | 7837 |
C3898d.java | c8.d | class | 1508 |
RunnableC3896b.java | c8.b | Runnable | 1884 |
ViewOnClickListenerC3895a.java | c8.a | inner listener class | 22759 |
$k — module IdPlus — 11 filesh5
| disk file | real class | kind | bytes |
|---|---|---|---|
AbstractC16137c.java | - | abstract class | 50358 |
AbstractC16140f.java | e8.f | abstract class | 6625 |
AbstractC16143i.java | e8.i | abstract class | 1786 |
C16136b.java | e8.b | class | 7690 |
C16138d.java | e8.d | class | 3377 |
C16139e.java | e8.e | class | 3323 |
C16141g.java | e8.g | class | 2011 |
C16142h.java | e8.h | class | 1087 |
EnumC16135a.java | e8.a | enum | 1000 |
EnumC16144j.java | e8.j | enum | 900 |
EnumC16145k.java | e8.k | enum | 967 |
$k — module Billing — 14 filesh5
| disk file | real class | kind | bytes |
|---|---|---|---|
C21282b.java | w7.b | class | 1672 |
C21283c.java | w7.c | class | 2092 |
C21284d.java | w7.d | class | 1606 |
C21289i.java | - | class | 32705 |
C21293m.java | w7.m | class | 6310 |
C21294n.java | w7.n | class | 6603 |
C21295o.java | w7.o | class | 13442 |
C21296p.java | w7.p | class | 1614 |
DialogC21290j.java | w7.j | Dialog | 2444 |
EnumC21291k.java | w7.k | enum | 1043 |
EnumC21292l.java | w7.l | enum | 495 |
RunnableC21281a.java | w7.a | Runnable | 4540 |
RunnableC21285e.java | w7.e | Runnable | 3732 |
RunnableC21288h.java | w7.h | Runnable | 1031 |
$k — module Billing — 12 filesh5
| disk file | real class | kind | bytes |
|---|---|---|---|
AbstractC16699a.java | g5.a | abstract class | 2815 |
C16702d.java | g5.d | class | 13592 |
C16703e.java | g5.e | class | 2012 |
C16706h.java | g5.h | class | 5994 |
C16707i.java | - | class | 21983 |
C16708j.java | g5.j | class | 5352 |
C16710l.java | g5.l | class | 4359 |
InterfaceC16709k.java | g5.k | interface | 277 |
RunnableC16700b.java | g5.b | Runnable | 1395 |
RunnableC16701c.java | g5.c | Runnable | 9515 |
RunnableC16704f.java | g5.f | Runnable | 1512 |
RunnableC16705g.java | g5.g | Runnable | 1628 |
$k — module Billing — 4 filesh5
| disk file | real class | kind | bytes |
|---|---|---|---|
C16125a.java | e6.a | class | 2927 |
C16127c.java | e6.c | class | 1670 |
C16128d.java | - | class | 41191 |
RunnableC16126b.java | - | Runnable | 19471 |
$k — module Billing — 11 filesh5
| disk file | real class | kind | bytes |
|---|---|---|---|
C3842a.java | c5.a | class | 11897 |
C3844c.java | c5.c | class | 1208 |
C3845d.java | c5.d | class | 3566 |
C3847f.java | c5.f | class | 5438 |
C3851j.java | c5.j | class | 3533 |
C3852k.java | c5.k | class | 150 |
DialogInterfaceOnKeyListenerC3843b.java | c5.b | Dialog | 1932 |
RunnableC3848g.java | c5.g | Runnable | 1308 |
RunnableC3849h.java | c5.h | Runnable | 3622 |
RunnableC3850i.java | c5.i | Runnable | 10183 |
ViewOnClickListenerC3846e.java | - | inner listener class | 20962 |
$k — module Registration — 9 filesh5
| disk file | real class | kind | bytes |
|---|---|---|---|
AbstractC0149e.java | aj.e | abstract class | 1281 |
AbstractC0153i.java | aj.i | abstract class | 1816 |
C0145a.java | aj.a | class | 7059 |
C0146b.java | aj.b | class | 3522 |
C0147c.java | aj.c | class | 16143 |
C0148d.java | aj.d | class | 18825 |
C0150f.java | aj.f | class | 9235 |
C0151g.java | aj.g | class | 1576 |
C0152h.java | aj.h | class | 2417 |
$k — module Billing — 16 filesh5
| disk file | real class | kind | bytes |
|---|---|---|---|
AbstractC20320g.java | t4.g | abstract class | 1000 |
AbstractC20321h.java | t4.h | abstract class | 645 |
AbstractC20328o.java | t4.o | abstract class | 6557 |
AbstractC20330q.java | t4.q | abstract class | 6114 |
C20315b.java | t4.b | class | 1152 |
C20316c.java | t4.c | class | 1941 |
C20318e.java | t4.e | class | 2951 |
C20319f.java | t4.f | class | 4648 |
C20322i.java | t4.i | class | 2028 |
C20324k.java | t4.k | class | 223 |
C20325l.java | t4.l | class | 1489 |
C20326m.java | t4.m | class | 3935 |
C20327n.java | t4.n | class | 9549 |
C20329p.java | t4.p | class | 3573 |
EnumC20314a.java | t4.a | enum | 2001 |
RunnableC20323j.java | t4.j | Runnable | 6845 |
$k — module Billing — 1 filesh5
| disk file | real class | kind | bytes |
|---|---|---|---|
C18416a.java | - | class | 45646 |
$k — module Billing — 2 filesh5
| disk file | real class | kind | bytes |
|---|---|---|---|
C17064a.java | hh.a | class | 6115 |
C17065b.java | - | class | 38052 |
$k — module Registration — 7 filesh5
| disk file | real class | kind | bytes |
|---|---|---|---|
C18297c.java | l5.c | class | 3449 |
C18298d.java | l5.d | class | 9049 |
C18299e.java | l5.e | class | 3311 |
C18302h.java | l5.h | class | 11023 |
RunnableC18295a.java | l5.a | Runnable | 1830 |
RunnableC18300f.java | l5.f | Runnable | 3593 |
RunnableC18301g.java | l5.g | Runnable | 9746 |
$k — module IdPlus — 6 filesh5
| disk file | real class | kind | bytes |
|---|---|---|---|
AbstractC19568c.java | q4.c | abstract class | 948 |
AbstractC19571f.java | q4.f | abstract class | 1099 |
C19566a.java | q4.a | class | 8385 |
C19567b.java | q4.b | class | 6065 |
C19569d.java | q4.d | class | 10594 |
C19570e.java | q4.e | class | 5649 |
$k — module Registration — 8 filesh5
| disk file | real class | kind | bytes |
|---|---|---|---|
C19888d.java | r7.d | class | 334 |
C19889e.java | r7.e | class | 2529 |
C19891g.java | r7.g | class | 2849 |
C19892h.java | r7.h | class | 1557 |
RunnableC19885a.java | r7.a | Runnable | 2474 |
RunnableC19886b.java | r7.b | Runnable | 7462 |
ViewOnClickListenerC19890f.java | r7.f | inner listener class | 2749 |
ViewOnTouchListenerC19887c.java | r7.c | inner listener class | 10895 |
$k — module Registration — 7 filesh5
| disk file | real class | kind | bytes |
|---|---|---|---|
C18306d.java | l6.d | class | 10727 |
C18308f.java | l6.f | class | 1196 |
C18309g.java | l6.g | class | 4445 |
EnumC18303a.java | l6.a | enum | 3678 |
EnumC18310h.java | l6.h | enum | 6465 |
InterfaceC18305c.java | l6.c | interface | 209 |
ViewOnClickListenerC18307e.java | l6.e | inner listener class | 3647 |
$k — module Billing — 1 filesh5
| disk file | real class | kind | bytes |
|---|---|---|---|
C21940a.java | - | class | 27261 |
$k — module PhoneAccount — 5 filesh5
| disk file | real class | kind | bytes |
|---|---|---|---|
C16961a.java | hd.a | class | 10901 |
C16962b.java | hd.b | class | 9942 |
C16963c.java | hd.c | class | 586 |
C16964d.java | hd.d | class | 475 |
C16965e.java | hd.e | class | 2433 |
$k — module IdPlus — 6 filesh5
| disk file | real class | kind | bytes |
|---|---|---|---|
C16914c.java | h5.c | class | 3289 |
C16915d.java | h5.d | class | 1424 |
C16916e.java | h5.e | class | 13342 |
InterfaceC16917f.java | h5.f | interface | 233 |
RunnableC16913b.java | h5.b | Runnable | 2407 |
ViewOnClickListenerC16912a.java | h5.a | inner listener class | 2247 |
$k — module IdPlus — 5 filesh5
| disk file | real class | kind | bytes |
|---|---|---|---|
C17152a.java | i6.a | class | 5734 |
C17153b.java | i6.b | class | 2831 |
C17154c.java | i6.c | class | 1988 |
C17155d.java | i6.d | class | 6922 |
ViewOnClickListenerC17156e.java | i6.e | inner listener class | 4743 |
$k — module Registration — 2 filesh5
| disk file | real class | kind | bytes |
|---|---|---|---|
C21845b.java | y4.b | class | 8867 |
RunnableC21844a.java | y4.a | Runnable | 3973 |
$k — module Billing — 6 filesh5
| disk file | real class | kind | bytes |
|---|---|---|---|
AbstractC16130b.java | e7.b | abstract class | 3080 |
AbstractC16134f.java | e7.f | abstract class | 559 |
C16132d.java | e7.d | class | 5595 |
InterfaceC16131c.java | e7.c | interface | 203 |
InterfaceC16133e.java | e7.e | interface | 244 |
RunnableC16129a.java | e7.a | Runnable | 3653 |
$k — module Billing — 9 filesh5
| disk file | real class | kind | bytes |
|---|---|---|---|
C3814a.java | c0.a | class | 1432 |
C3815b.java | c0.b | class | 1620 |
C3816c.java | c0.c | class | 706 |
C3817d.java | c0.d | class | 1399 |
C3818e.java | c0.e | class | 4712 |
C3820g.java | c0.g | class | 686 |
C3821h.java | c0.h | class | 590 |
C3822i.java | c0.i | class | 1417 |
InterfaceC3819f.java | c0.f | interface | 450 |
$k — module Billing — 1 filesh5
| disk file | real class | kind | bytes |
|---|---|---|---|
AbstractC0037a.java | a5.a | abstract class | 11590 |
$k — module Registration — 3 filesh5
| disk file | real class | kind | bytes |
|---|---|---|---|
C22071c.java | z4.c | class | 4834 |
RunnableC22069a.java | z4.a | Runnable | 2546 |
RunnableC22070b.java | z4.b | Runnable | 2785 |
$k — module Registration — 5 filesh5
| disk file | real class | kind | bytes |
|---|---|---|---|
C18506a.java | m6.a | class | 402 |
C18509d.java | m6.d | class | 2741 |
C18510e.java | m6.e | class | 3861 |
InterfaceC18508c.java | m6.c | interface | 215 |
ViewOnClickListenerC18507b.java | m6.b | inner listener class | 1855 |
$k — module Billing — 4 filesh5
| disk file | real class | kind | bytes |
|---|---|---|---|
C17729a.java | k0.a | class | 6538 |
C17730b.java | k0.b | class | 604 |
C17731c.java | k0.c | class | 1393 |
C17732d.java | k0.d | class | 724 |
$k — module Registration — 3 filesh5
| disk file | real class | kind | bytes |
|---|---|---|---|
C3910a.java | ce.a | class | 939 |
C3911b.java | ce.b | class | 735 |
C3912c.java | ce.c | class | 3625 |
$k — module PhoneAccount — 3 filesh5
| disk file | real class | kind | bytes |
|---|---|---|---|
C17789a.java | k7.a | class | 1787 |
C17790b.java | k7.b | class | 2705 |
C17791c.java | k7.c | class | 588 |
$k — module IdPlus — 3 filesh5
| disk file | real class | kind | bytes |
|---|---|---|---|
C17512a.java | j6.a | class | 530 |
C17513b.java | j6.b | class | 1466 |
ViewOnClickListenerC17514c.java | j6.c | inner listener class | 1968 |
$k — module Billing — 5 filesh5
| disk file | real class | kind | bytes |
|---|---|---|---|
InterfaceC19944a.java | rj.a | interface | 348 |
InterfaceC19945b.java | rj.b | interface | 326 |
InterfaceC19946c.java | rj.c | interface | 327 |
InterfaceC19947d.java | rj.d | interface | 287 |
InterfaceC19948e.java | rj.e | interface | 287 |
$k — module Registration — 1 filesh5
| disk file | real class | kind | bytes |
|---|---|---|---|
AbstractC16045a.java | e.a | abstract class | 907 |
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 class | kind | purpose / key members |
|---|---|---|
q7.l0 = C19673l0 | final class, 3 ctors | RegistrationActivity 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 = C19677n | final class | PhoneNumberValidationActivity callbacks. case 2 in mo309m() = second clientId write site (§4.3). case 2 in mo308k() = failure (-123 special-cased to m10468E0). |
q7.i1 = C19665i1 | final class | Per-Activity registration state (§4.3 step 1). Field int f50625b = 30000 (default timeout). void m36102a(String regId), void m36103b(int). |
q7.b1 = C19644b1 | class, 13.7 KB | Join request context; field f50539o is the svr value posted to join.jsp. |
q7.c1 = C19647c1 | class | cli_auth/gettransport callback (AbstractC16130b subclass); f44796b switch int, error keys V_ERROR_STATUS, V_CLI, V_TIMEOUT, V_SMS_MESSAGE, V_CODE. |
q7.e1 = EnumC19653e1 | enum, 41 constants | The 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 = EnumC19659g1 | enum, 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 = EnumC19662h1 | enum, 8 constants | CLI 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 = InterfaceC19656f1 | interface | int 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.z0 | misc | Registration 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 host | abstract / interface | AbstractC16130b 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 class | kind | purpose |
|---|---|---|
jd.z = RunnableC17614z | synthetic Runnable, 20.5 KB | The 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 = C17566b0 | class | Failure payload {f44269a, f44270b} handed to mo33684f(...) in jd.z case 0. |
jd.c0 = InterfaceC17569c0 | interface | Success/failure listener pair for jd.b0. |
jd.m0/jd.u0/jd.c/jd.d/jd.e | classes | Response models for join.jsp / cli_auth payloads (synthetic accessors generated by the obfuscator). |
jd.s = C17598r, jd.r = C17598r | interface/class | Error/response pair for jd.b0. |
jd.j = C17582j | class, 15 KB | Reflection-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.j0 | classes | Response 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 class | kind | purpose |
|---|---|---|
r5.s = RunnableC19875s | Runnable, 10.8 KB | case 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.a | class, 9.2 KB | Registration/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.v | classes | Contact DTOs (r5.i = 46 KB, r5.i0 = 43 KB — the big contact/DB models), EnumC19861h0 (9.9 KB enum), coroutine plumbing. |
r5.e = C19854e | class, 395 KB | Giant DTO/serialisation mega-class. Single largest file in the whole C2 + C3 region. |
r5.v(= InterfaceC19878v) | interface | marker. |
r5.q = RunnableC19873q, r5.r=RunnableC19874r | Runnables | RunnableC19874r GETs updateuserdetails.jsp. |
r5.i0 = C19863i0 | class, 43 KB | Social-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 class | kind | purpose |
|---|---|---|
g5.i = C16707i | final class, 21.9 KB, singleton f41454h | TAG "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 = C16708j | class | Product 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 = C16710l | class extends g5.j | Wraps 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 = C16702d | class | queryPurchasesAsync("inapp"/"subs") → List<C16710l>; logs "queryPurchases return with responseCode = …". |
g5.h = C16706h | class extends AbstractC16130b | Bulk acknowledge with retry. |
g5.c = RunnableC16701c | Runnable | Joins the inapp+subs queries. |
g5.b = RunnableC16700b | Runnable | m32522c(includeSubs, callback). |
g5.e = C16703e, g5.f=C16702d cb, g5.k=InterfaceC16709k | interface/class | queryProductDetails callback; listener registration for m32535o. |
g5.a = AbstractC16699a | abstract class | callback base. |
10.5 j5 (p358j5, 29 files / 122 KB) — entitlement + premium UI — APP CODEh5
| real class | kind | purpose |
|---|---|---|
j5.k = C17496k | final 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 = C17487c | final 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 = C17485b | class, TAG "FreePremium" | getViralLink / updateViralStatus (virality/viral_id, virality/premium_status, virality/viral_id_check). Persists SP_KEY_MY_VIRAL_ID. |
j5.a = C17483a | Runnable | updateVideoPremium_#onSuccess/#onFailed. |
j5.q = C17502q | class | PremiumFeatureDialog — loads products (g5.i.m32534m), renders the plan list; drives C3502i case 13. |
j5.r = AbstractC17503r | abstract class, 13.7 KB | The 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… | classes | Rewarded-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, EnumC17486b0 | classes/enums | Free-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 class | kind | purpose |
|---|---|---|
q1.c = C19537c | class, 54.9 KB, extends q1.b | BillingClient 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 = C19533a | class | BillingClient.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=AbstractC19534a0 | abstract | log/error constants. |
q1.d = C19540d0, q1.i, q1.j, q1.k, q1.l, q1.p, q1.y, q1.z, q1.d0, q1.x | classes | ServiceConnectionC19557t, 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/interfaces | BillingFlowParams, BillingResponse, ProductDetailsResponse, ProductDetails, offer DTOs, QueryProductDetailsParams.Product. |
10.7 e7 (p262e7, 6 files / 13 KB) — the callback base + the Eyecon bridge — APP CODEh5
| real class | kind | purpose |
|---|---|---|
e7.d = C16132d | final class, the Eyecon JS bridge | §8.2 — 19 @JavascriptInterface methods. |
e7.b = AbstractC16130b | abstract class | The 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 = RunnableC16129a | Runnable | Timeout dispatch for e7.b. |
e7.c = InterfaceC16131c, e7.e = InterfaceC16133e | interfaces | callback markers. |
e7.f = AbstractC16134f | abstract class | secondary 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 (titlef4339f, messagef4340g, positivef4344k,EnumC17169g f4345lcolour theme, Runnablef4346m, cancelf4349p, cancel Runnablef4352s, colourf4351r, codef4340g/f4359z). Used for the store errors ("store_error_dialog"), block-contact ("blockContactDialog"), etc.c7.t=C3888t(21.7 KB) extendsc7.d— premium feature/purchase dialog.c7.c/c7.d— baseDialogFragmentclasses;c7.l=ViewOnClickListenerC3880l(20.9 KB) — profile UI incl. the cleartext privacy-policy linkhttp://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) extendsf8.i— the Notification-Reader premium-gated screen.h6.r=C16935rextendsAdListener;h6.p=C16933pextendsAbstractC16130b;h6.s=C16936sextendsConstraintLayout;h6.k=ViewOnClickListenerC16928k.h5.e=C16916e(13.3 KB) extendsc7.k— the premium upsell inside Notification Reader;h5.c=C16914c implements g5.k(queryProductDetailslistener);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 holderf48663a = new o4.j().o4.j=C19057j— the advertising-ID provider:m35361a()usescom.google.android.gms.ads.identifier.AdvertisingIdClient.getAdvertisingIdInfo(app).getId(), caches it in prefsEyecon.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". Alsom35362b()(remote confighome_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) +AdManagerAdRequestwrapper.q4.*— Facebook Audience Network native ads (com.facebook.ads.NativeAd,AdError).ds.*— 5 coroutine buildersextends AbstractC21478j implements InterfaceC16320oplusRunnableC16004e— mediation orchestration (Mbridge/Crackle/TikTok/Amazon bid adapters).e8.*— Facebook App Events / Graph JSON helpers (AbstractC16137cis 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) — GETsgetoptionalpics.jsp,social_auth/use_cli_validation(logged as"join.jsp"),updateuserdetails.jsp,resetupdatedstatus.jsp.RunnableC21093i(33.7 KB) — GETsgetupdatedcontactsinfo.jsp.RunnableC21088g0— GETssethistory.jsp;RunnableC21129z— POSTseventreport.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) singletonf46692k. Methodm34691f()= collectTelephonyManager.getDeviceId()over up to 3 SIM slots via{"getDeviceId","getDeviceIdGemini","getSimSerialNumberGemini","getDeviceIdDs"}, dedupe, join with,→ this is theimeiparameter sent injoin.jsp. Also reflection onSubscriptionManager.getSubId(int)(setAccessible(true)), dual-SIMTelecomManagerhandling,PhoneAccountHandlecreation. Fieldf46696dlists 26 OEM intent extras probed for the SIM slot.m5.s=C18505s— reflection call-hacking:telecomManager.getClass().getDeclaredMethod("endCall", null).invoke(telecomManager, null)andTelephonyManager → getITelephony() (setAccessible) → endCall(), with agetITelephonyMSim()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-caseRunnable; used byStoreActivity(case 24),NotificationReaderWindowActivity,TelegramWindowActivity(clipboard,InputMethodManager, window-insets, dialogs).df.v=C15924vextendsAppCompatAutoCompleteTextView;df.o=C15917o,df.x=C15926xextendLinearLayout(Material text-field scaffolding);df.s=C15921s;df.a–df.ulistener 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
| pkg | classes | role |
|---|---|---|
c8 (p048c8, 5) | c8.e=AbstractActivityC3899e extends b7.e implements h5.i, c8.c, c8.a, c8.b, c8.d | IdPlus 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.d | ExoPlayer 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.c | PhoneAccount stubs — see §5.3. |
ce (p050ce, 3) | ce.c=C3912c extends CustomCredential (Google Identity), ce.a/ce.b extend GetCustomCredentialOption | Google 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 Interceptor | Firebase 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=C18299e | Photo picker + contact-block confirm dialog. |
l6 (p398l6, 7) | l6.d=C18306d extends c7.d, l6.h=EnumC18310h, l6.g=C18309g (RecyclerView.Adapter), l6.a=EnumC18303a | Onboarding 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.a | Onboarding ViewPager2 (OnboardingActivity, FakeCallActivity) + fake-incoming-call UI. |
m6 (p419m6, 5) | m6.e=C18510e extends c7.d, m6.d=C18509d, m6.b, m6.a, m6.c | Contact 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.a | Join-success event fan-out. |
j6/i6/h5/h6 | see 10.15 | IdPlus. |
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.b1even containsAES/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 13andbm.RunnableC3701s3 case 2happen 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(readsclientId,authenticatedCli,SP_UUID, GAID,username, and any string/bool/int key inshared_preference_v2) andEyeconStore=k5.i(lists owned products with Play purchase tokens, launches Play purchases with a page-suppliedofferToken, downloads ZIPs from a second remote-config URLstore_download_url). C5846mdoes not overrideshouldOverrideUrlLoadingfor this Activity, so navigation and redirects keep the bridges.- Contrast proof:
WebviewPaywallActivityperforms exactly the missing check ("https".equals(scheme) && (host.equals("eyecon-app.com") || host.endsWith(".eyecon-app.com"))elseSecurityException("Untrusted paywall URL: …")+finish()). - Impact: full account takeover (the
clientIdis thee-authbearer token for every Eyecon API call), persistent tracking via the advertising ID, and PII (validated MSISDN, display name). - Also:
Eyecon.getFloatSP/getLongSPare declaredvoid— 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 /androidpublishercall anywhere ing5,q1,j5,w7,x,c0,k0,hh,yj. - No Play signature (RSA) verification of
Purchase.getSignature()anywhere. - The only server signal is
POST /app/mysettingswith client-authoredpremium_cache_product({productType, productId, purchaseToken, purchaseState, originalJson}) andpremium_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, andj5.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()andTelephonyManager → getITelephony() (setAccessible) → endCall(), plusgetITelephonyMSim().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.cline 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’swithBrandedDomains(["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", andstore_download_urlpayloadversion_name/version_code. Six independent copies. - L-4 — Reflection/serialization. The only AES in scope is
as.b1AES/GCM/NoPadding(kotlinx-serialization, third-party). NoTrustManager/HostnameVerifieroverrides, noALLOW_ALL_HOSTNAME_VERIFIER, noMD5/DES, noBase64-as-encryption. - L-5 — Dead / no-op bridge surface.
Eyecon.getFloatSP,getLongSP,getUserPropertyreturn 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 inq7.l0.mo308k(). Client-side anti-fraud is advisory only. - L-7 —
SP_KEY_VALIDATION_WITH/SP_KEY_PICTURE_SOURCEare 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:
| Data | Destination | Where |
|---|---|---|
clientId (bearer credential), authenticatedCli, public_id, adv_id, mc, imei, os_id, deviceName, os, user_lang, viral_id, svr, tr, cli_cc, generated_cli | GET api.eyecon-app.com/app/join.jsp | jd.z case 1 |
formatted cli, code, tr, is_already_social_auth, apsversion | GET …/cli_auth/checkcode | r5.s case 1 |
cli, reg_id, tr, is_already_social_auth, last_error | GET …/cli_auth/gettransport | jd.z default |
cli, reg_id | GET …/social_auth/use_cli_validation | vk\RunnableC21082e0 |
country, language, cli, cid, version_name, version_code, product, item | POST <store_download_url>/getItem | k5.f.m34065a |
premium_cache_product (incl. purchaseToken), premium_cache_time | POST …/mysettings | j5.k.m33588e |
| arbitrary event name + props | analytics bus | Eyecon.reportEvent |
| install-referrer bundle | logcat IRR/ | InstallReferrerReceiver |
| publicId as custom user id | Singular | y4.b |
12. Corrections to previously reported claimsh4
| # | Prior claim | Verdict | Proof |
|---|---|---|---|
| 1 | ”clientId is the join.jsp response body, stored in the app’s custom prefs; read via AbstractC9039h.m10662s(), presence via m10665v().” | CORRECT | m10662s() = (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 fix | q7.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.” | CORRECT | m32077d("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.” | CORRECT | C16484n0.m32061b() = ((HashMap) f40952a).get("DEFAULT_RESULT"). §3.4. |
| 5 | ”p429n4 (248 KB / 4 files) is large — determine generated vs hand-written.” | RESOLVED: 100 % GENERATED | n4.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-WRITTEN | y5.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 incomplete | The 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 mechanism | store_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 path | In 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 TSV | It 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 correction | StoreActivity 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. |
| 14 | Implicit: “the store page is trusted because it comes from Eyecon.” | REFUTED | WebviewPaywallActivity 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. |
| 15 | Implicit: “premium is verified server-side because mysettings is synchronised.” | REFUTED | mysettings 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
.javafile 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 nullinStoreActivity.onDestroy,q7.i1.m36102a,WebviewPaywallActivityerror paths,k5.d.m34064q; andsupercall-hoisting warnings inq7.l0,q7.n,jd.z,r5.s,C5846m— in each of those thesuper(...)constructor was moved to the top of the synthetic constructor by JADX, which is safe forAbstractC16130bbecause its body only assigns fields, but should not be trusted blindly. - Not resolved: where
store_urlis 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(...)(thep268eh“t1” class, called on every join) performs additional device attestation.p268ehis 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>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
| # | Finding | Severity |
|---|---|---|
| S1 | The 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 |
| S2 | com.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 |
| S3 | The 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 |
| S4 | Hard-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 |
| S5 | Common 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 |
| S6 | Incoming 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 |
| S7 | Documents/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 |
| S8 | Two three-long-press “Ad stats” debug AlertDialogs are reachable in the after-call UI (x4.q, x4.u). | LOW |
| S9 | MICO 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(mergedv7.fcase 11)AudioRecord audioRecord = new AudioRecord(1 /* MediaRecorder.AudioSource.MIC */, 8000, 16 /* CHANNEL_IN_MONO */, 2 /* ENCODING_PCM_16BIT */, 16000);followed byaudioRecord.startRecording();(line 510) andio.kvh.media.amr.AmrEncoder.init(0). The owner isp633wl/RunnableC21441d(real namewl.RunnableC21441d) - the voice-note recorder; playback iswl.RunnableC21439bthroughAudioTrack+AmrDecoder. It capturesMIC, not call audio. - Zero
MediaRecorder, zeroMediaProjection, zerocreateAudioCapture, zeroVOICE_CALL/VOICE_DOWNLINK/VOICE_UPLINK, zerosetAudioSource, zerogetMaxAmplitudeanywhere in the decompiled tree. (VOICE_COMMUNICATIONappears only as a constant declaration insideandroidx.media.AudioAttributesCompatandcom.mbridge.msdk...C13867C- both library code, never used.) Documents/Eyecon/RecordedCallsis a storage / import / archive location, not a capture location:p260e5/AbstractC16098a.m31460c(int)(e5.a) merely resolves andmkdirs()the directory (RecordedNoteswheni3==0,RecordedCallsotherwise) -MyApplication.f12172q(external Documents) ifAbstractC0042b0.m178a0(), otherwisegetFilesDir()/Eyecon/....p008a7/RunnableC0070p0.m409b()(a7.d1merged runnable) creates both directories onSDK_INT <= 29and then callse5.d.m31467a(...), which scans pre-existing files matching.+__.+__[0-9]+(__s|__c)?(__-1|__1|__2)?__(ern|erc)\..*and bulk-inserts them into therecorded_notesSQLite table.p282f5/RunnableC16439w(f5.wcase 2) then zips/uploads those files with progress keyPROGRESS_RECORDS; case 1 zips the TTM audio dir tottm.zipwith progress keyPROGRESS_KEY_TTM.
- Conclusion (OBSERVED): MICO contains no call-audio capture code path.
RecordedCallsis 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 claim | Status |
|---|---|
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-ttm | ALL 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 listed | FULLY 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 dir | Real pkg | Files | Owner | Responsibility |
|---|---|---|---|---|
p620w6 | w6 | 8 | DefaultDialer | Server - 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. |
p646x5 | x5 | 20 | DefaultDialer | AI SMS assistant: settings fragment, initial-message templates, auto-use policy, work-request enqueuer, Retrofit service interface, follow-up network coroutine. |
p636wo | wo | 10 | DefaultDialer | [LIB] kotlin.coroutines coroutine-state-machine base classes; plus wo.f = the assistant’s Retrofit/Moshi container hard-coding http://numberlookup.instabridge.com/. |
p511r0 | r0 | 1 | AfterCall | ProGuard-merged god-class r0.a = C19813a; the assistant-relevant constructor is C19813a(Context, 22) = MessageAssistantStorage (SharedPreferences("MessageAssistantStorage"), key STARTED_FLOW_CONTACTS, Moshi List<MessageAssistantFlow>). |
p648x7 | x7 | 20 | AfterCall | Spam/block model + spam_list SQLite DAO + spam-report & block dialogs + Spam&Block pager/adapter. |
p623w9 | w9 | 2 | AfterCall | ProGuard-merged runnables: spam_list read/write/replace and SP_KEY_VERSION_SPAM_LIST. |
p399l7 | l7 | 5 | DefaultDialer | PhoneNumberHelper (libphonenumber wrapper), PhoneNumbersCache (results cache persisted to SP_KEY_PHONE_NUMBERS_CACHE_V4), DiskLruCache wrapper. |
p554t5 | t5 | 12 | Call | PersonDataLoader - the caller-ID name/photo/spam fetch-and-display engine used by the dialer, the caller-ID window and the after-call screen. |
p380k6 | k6 | 29 | DefaultDialer | CallerIdWindow - the incoming-call full-screen UI, its call-scene enum, bubble, ad slots and the AI-assistant button. |
p573u5 | u5 | 42 | DefaultDialer | CallActivity support: Call.Callbacks, in-call WebView ads, dialer/call-screening role notifications, call bubble window, TTM “redirect call” button, window insets. |
p404lg | lg | 18 | Call | [LIB] Guava ListenableFuture/executors + merged notification-channel/manifest helpers (lg.n). |
p421m8 | m8 | 19 | DefaultDialer | [LIB] Facebook in-app-purchase / Google Play Billing bridge + string helpers (m8.s). |
p575u7 | u7 | 26 | Sms | SMS list UI adapters/view-holders, the sms_table DAO (u7.b), ExternalSmsDB manager (u7.s), message comparator (u7.q), SMS-window click handlers. |
p619w5 | w5 | 16 | DefaultDialer | In-call feed RecyclerView view-holders (ad slots, history, statistics, note/reminder, extra actions); the AI-assistant action-button holder is w5.c. |
p645x4 | x4 | 22 | AfterCall | After-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). |
p599v7 | v7 | 20 | AfterCall | ProGuard-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. |
p268eh | eh | 92 | AfterCall | [LIB] Firebase Crashlytics proto models (CrashlyticsReport, Session, Device, Thread, RolloutsState, …). No MICO logic. |
p438nj | nj | 51 | DefaultDialer | [LIB] Firebase Sessions (sessionSdkVersion=2.0.8) + merged Crashlytics hook in nj.o (after_call_switch_to_compact, SP_AFTERCALL_TYPE). |
p605vh | vh | 32 | AfterCall | [LIB] Firebase In-App Messaging proto models + vh.q merged Crashlytics build-id reader. |
p637wp | wp | 13 | DefaultDialer | [LIB] kotlinx.coroutines Dispatchers / worker-pool executor. |
p484po | po | 28 | DefaultDialer | [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. |
p485pp | pp | 88 | DefaultDialer | [LIB] kotlinx.coroutines (Dispatchers, Job, CancellableContinuation, withContext). |
p587uo | uo | 11 | DefaultDialer | [LIB] kotlin.coroutines (Continuation = uo.d, CoroutineContext = uo.i, SafeContinuation = uo.k). |
p275ep | ep | 22 | DefaultDialer | [LIB] kotlin function types (ep.a = Function0, ep.o = Function2, …). |
p609vo | vo | 6 | DefaultDialer | [LIB] kotlin intrinsics (COROUTINE_SUSPENDED = vo.a). |
p715zk | zk | 36 | DefaultDialer | [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. |
p038bl | bl | 6 | DefaultDialer | [LIB] Moshi internal type adapters (bl.f = Util, bl.b = nullSafe). |
p662xo | xo | 3 | DefaultDialer | [LIB] kotlin EnumEntries (xo.b, xo.c, xo.a). |
p344ic | ic | 80 | DefaultDialer | [LIB] ExoPlayer2 core. |
p385kc | kc | 37 | Call | [LIB] ExoPlayer2 audio (AAC/AC-3/AC-4/AudioTrack) + merged DB/analytics callbacks in kc.k. |
p379k5 | k5 | 10 | Call | [LIB]/[MIXED] Google Play store item loader (k5.f, k5.i) + merged CanTalkActivity/location/Blowfish branches in k5.b. |
p449o5 | o5 | 10 | Call | TTM window UI - RecyclerView adapters, Observer, click handlers (paywall_noti_ttm, TTM window). |
p716zl | zl | 3 | Call | [LIB] Google Play Billing v6 client + TtmAudioPlayer interface zl.a (progressive download with retry). |
p430n5 | n5 | 8 | Call | TTM (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). |
p540s4 | s4 | 6 | AfterCall | After-call carousel-ads adapter/view-holder/scroller. |
p441nq | nq | 7 | AfterCall | [LIB] ViewPager2 helpers. |
p472p3 | p3 | 3 | DefaultDialer | [LIB] Room (p3.a comparator, p3.c enum). |
p476p7 | p7 | 3 | DefaultDialer | [LIB] TextToSpeech wrapper + layout helper. |
p230d | d | 1 | AfterCall | d.a = AbstractC15790a, a merged utility bag; the C3-relevant functions are m31044c(spamType, cli) / m31045d(spamType, call) (spam-type -> UI tag) and m31046e(C19862i). |
p597v5 | v5 | 2 | DefaultDialer | In-call feed ordering adapter (v5.b) + v5.a callbacks. |
p614w0 | w0 | 1 | DefaultDialer | w0.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 secretinstance= singleton;httpClient=oq.C19227c0built overC19225b0(OkHttp)
Key methods:
| Signature | Behaviour |
|---|---|
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.
| Signature | Behaviour |
|---|---|
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 / m38439l | Result/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 toSP_KEY_LAST_INIT_TTM_JSON. On 200 it stores the JSON and feeds the response inton5.h.m35053a(token balance). - case 1 -
ttmAck(line 70):POST https://voice2text.eyecon-app.com/ack, same auth/content-type. Both paths requireAbstractC9039h.m10665v()(aclientIdexists).
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-> cachesappVersion - case 6
GET .../app/upgrade_pic2eyecon.jsp - case 10 - prunes
SP_AFTER_CALL_LOGS(JSON array) to entries newer thannow - AbstractC19089d.f48805aand 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 -
ApiRequestWorkerpass: readstable_api_requests, replays each entry, deletes on 200 or afterApiRequestServerRetryCount, otherwise increments the retry counter and re-enqueuesApiRequestWorkerwith delayapiWorkRepeatIntervalMinutesminutes,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 meansC15802b.m31518o(res); m31516i(), elsem32066g(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 dismissesx4.a0, case 22SpamAndBlockFragment.m10572t0(), case 23 adds a number to the spam list UI, case 26 parses thereportSpamremote JSON and readsspamType.
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 remoteserver_report_url. - case 1 -
sendNewExternalSmsIdsToServer: POSTs the list of external SMS ids (not bodies) to.../app/messages/received(application/json) and clearssp_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
| Method | Behaviour |
|---|---|
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) throws | Maps 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) throws | Same 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)- Fires analytics
ai_assistant_button_click{source=...}. - If
type == "DEFAULT"shows the toastR.string.ai_assistant_manually_clicked. - Serialises
argsto JSON with the assistant’s Moshi instance (f57243c->zk.e0with thebl.fconfig). - Builds
WorkManager OneTimeWorkRequest(MessageAssistantStartFlowWorker.class)withData{ args_bundle: <json>, type: <type> }andConstraints{ requiredNetworkType = CONNECTED }.
Call sites of m38851a (whole tree):
| Site | Source | Type |
|---|---|---|
p380k6/C17781s.java:1593,1656,1720 (k6.s, CallerIdWindow AI button) | IncomingCallPopup | DEFAULT |
p315h7/C16937a.java:321 (case 16, AutoUseOptionsHandler) | declined / missed / unknown-caller | decl/missed |
p646x5/AbstractC21633h.java:158 (onSpamCall) | SpamCallAutoUse | SPAM_CALL |
p397l5/C18297c.java:57 (permission-gated launcher) | MissedCallPopup, AfterCall | DEFAULT |
p619w5/C21258c.java (10 merged call sites - the in-call feed AI action) | various | DEFAULT |
x5.p -> C21641p (p646x5/C21641p.java) - assistant context + SMS sendingh5
- Fields:
Context f57877a,wo.f f57878b(Retrofit container),r0.a f57879c(C19813a, theMessageAssistantStorage). 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)-> analyticsai_assistant_error{error?, number, messagetype, source, type}.static final void m38846c(self, String initialMsg, String normalizedNumber, String rawNumber, MessageAssistantSource src, String type)-> startConversation: writesnew MessageAssistantFlow(now-10 000, normalizedNumber, now, type)viax5.s.m38849b, thenm38847d(initialMsg, rawNumber, src), thenC16945i.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}; returnfor (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
AbstractC9039h.m10664u(ctx)false -> analyticsai_assistant_missing_sms_permission{source}and return.C18312b.m34437h().m34443d(3, number)-> normalize; strip every non-digit character into a digits-only string (str).null-> abort.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.
- If found and
initialMessage = AbstractC21633h.m38834b(type, AbstractC21633h.m38833a(type))- the personalised template from prefs, else the resource default.- Build
RespondSmsBody(args.getContactName(), normalizedDigits, emptyList(), x5.p.m38844a(self), 0, AbstractC9039h.m10663t(""))- history is empty andmessagesInSession = 0on the opening call. api.m38828a(body, this)->POST http://numberlookup.instabridge.com/respond-sms.- Response handling:
EndConversation-> analyticsai_assistant_send_finish_on_start_flow{number, source}; nothing is sent.NewMessage-> analyticsai_assistant_starting_message_sent{number, source, type}; thenC21641p.m38846c(self, AbstractC21633h.m38834b(type, newMessage.getMessage()), normalizedDigits, args.getNumber(), args.getSource(), type)- sends the AI’s opening SMS immediately from the device.
- On any of
JsonDataException / IOException / IllegalArgumentException / HttpException:Log.e("invokeApiCall","error happened while loading", throwable)C21641p.m38845b(..., "starting message", ...)-> analyticsai_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, observersf57884b(ArrayList<C20689j>= theu7.jlistener interface). static MessageAssistantFlow m38848a(String number, List flows)- libphonenumber comparison (thex5.rwhen-map) over the stored flows; returns the first whosenormalizedNumbermatches.static void m38849b(MessageAssistantFlow, r0.a storage)- add/update: removes any existing flow for the samenormalizedNumber, inserts the new one, sorts withu7.q(4), truncates to 50 entries, serialises withzk.land writesSharedPreferences("MessageAssistantStorage").putString("STARTED_FLOW_CONTACTS", json). Notifies observers viaRunnableC19318r(..., 25).static void m38850c(MessageAssistantFlow, r0.a storage)- remove: filters out the matchingnormalizedNumberand rewritesSTARTED_FLOW_CONTACTS.
Remaining x5 classesh5
| File | Real | Kind | Purpose / key members |
|---|---|---|---|
C21635j | x5.j | Kotlin object | toString() == "Assistant" - marks an SMS row the assistant itself sent. |
C21636k | x5.k | Kotlin object | toString() == "Caller" - marks a row from the human caller. |
InterfaceC21637l | x5.l | marker interface | Sender marker for x5.i. |
C21634i | x5.i | data class | Message(body: String, sender: x5.l, date: Long); toString() = "Message(body=..., sender=..., date=...)". |
AbstractC21638m / AbstractC21643r | x5.m / x5.r | synthetic when-maps | Kotlin when dispatch tables over x5.l and over the 5 start-types. |
C21631f | x5.f | Fragment (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)). |
C21629d | x5.d | TextWatcher | 500 ms debounce on the personalised-message edit boxes. |
C21630e | x5.e | Animator.AnimatorListener | Restores maxLines=1 / wrap-content after the grow animation. |
C21628c | x5.c | lambda | mo37220X() closes the assistant settings fragment. |
C21645t | x5.t | CoroutineScope for MessageAssistantReplyWorker.doWork | |
C21646u | x5.u | CoroutineScope for MessageAssistantStartFlowWorker.doWork | |
InterfaceC21632g | x5.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 OkHttpInterceptor(logging/diagnostics).C15950q(3)-Moshi.Builderregistering:RespondSmsResponse.class -> new RespondSmsAdapter(moshiProvider)MessageAssistantSource.class -> new MessageAssistantSourceAdapter(moshiProvider)MessageAssistantSourceKey.class -> al.C0155a(enum adapter)
C22248e0(moshiBuilder)- theMoshiinstance (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 forx5.a(Proxy.newProxyInstance(..., new m8.j(invocationHandler))). - Fields:
f57242b/f57243c=Moshi,f57244d= thex5.aproxy.
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
| File | Real | Purpose |
|---|---|---|
AbstractC21469a | wo.a | ContinuationImpl base for all assistant suspend functions (create(Object, Continuation), invokeSuspend). |
AbstractC21471c | wo.c | ScopeCoroutine-like base used by the two workers. |
AbstractC21475g | wo.g | abstract job base. |
AbstractC21476h | wo.h | AbstractC21469a + Job support. |
AbstractC21477i | wo.i | base for DispatchedTask continuations. |
AbstractC21478j | wo.j | base for the assistant’s x5.n / x5.o coroutines. |
C21470b | wo.b | Ktor CoroutineContext element dispatch (vo.b). |
InterfaceC21472d | wo.d | DisposableHandle. |
InterfaceC21473e | wo.e | CoroutineExceptionHandler. |
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 MoshiJsonAdapter<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 == 0viaMessageAssistantFlow.copy$default(..., 0L, null, startMS, null, 11, null) - returns
List<MessageAssistantFlow>
- reads
- 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 field | Type | Source at start | Source at follow-up |
|---|---|---|---|
contactName | String (non-null) | MessageAssistantStartFlowArgs.getContactName() | (register reuse ambiguity - see the x5.n caveat in section 2) |
number | String (non-null) | digits-only normalised number | flow.getNormalizedNumber() |
messages | List<RespondSmsBody.Message> (non-null) | C19750a0.f50848a = empty list | last <= 50 messages from content://sms/inbox + content://sms/sent |
locale | String (non-null) | x5.p.m38844a(self) = configuration.locales[0].toLanguageTag() | same |
messagesInSession | int | 0 | messages.size() |
userName | String (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:
| Subclass | Default key |
|---|---|
IncomingCallFullscreen | IncomingCallFullscreenKey |
IncomingCallNotification | IncomingCallNotificationKey |
IncomingCallPopup | IncomingCallPopupKey |
AfterCall | AfterCallKey |
MissedCallPopup | MissedCallPopupKey |
FollowUp | FollowUpKey |
CallDeclinedAutoUse | CallDeclinedAutoUseKey |
CallMissedAutoUse | CallMissedAutoUseKey |
SpamCallAutoUse | SpamCallAutoUseKey |
UnknownCallerAutoUse | UnknownCallerAutoUseKey |
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
| Class | Role |
|---|---|
MessageAssistantBroadcastReceiver | onReceive 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. |
OnboardingActivity | One-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). |
GeneratedJsonAdapters | Moshi 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.
| File | Real | Kind | Purpose & key members |
|---|---|---|---|
C18311a | l7.a | value class | Cached 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}. |
C18312b | l7.b | PhoneNumberHelper (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). |
C18313c | l7.c | DiskLruCacheWrapper | 250 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. |
C18314d | l7.d | lambda | l7.e.m34459b(...) on two executor variants. |
C18315e | l7.e | PhoneNumbersCache singleton | Constructor 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
| File | Real | Kind | Purpose |
|---|---|---|---|
C20343m | t5.m | loader | Constructors (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). |
RunnableC20341k | t5.k | the kick-off runnable | case 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). |
C20340j | t5.j | m7.o callback | mo590e(Bitmap, int code) - caches the bitmap, crops it, then posts RunnableC17219d1(this, cacheKey, 17, bitmap) after m37282g() ms. |
C20335e | t5.e | t5.h implementation = the per-call caller-ID record | Fields: 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. |
C20337g | t5.g | PersonDataCache | SQL over the names_photos_cache table: columns cli_in_server, modify_time, spam_type, DATA_TYPE_{NAME,SMALL_PHOTO,LARGE_PHOTO} (EnumC20336f). |
EnumC20336f | t5.f | enum | DATA_TYPE_LARGE_PHOTO, DATA_TYPE_NAME, DATA_TYPE_SMALL_PHOTO. |
C20342l | t5.l | AbstractC16130b | photo-thumbnail (photo_uri, photo_thumbnail) load. |
RunnableC20331a / RunnableC20332b | t5.a / t5.b | merged runnables | notify the attached view holders when name/photo/spam arrive. |
AbstractC20333c | t5.c | abstract holder | base for in-call feed holders (u5.q0, u5.n0, u5.r0, u5.j0). |
InterfaceC20334d / InterfaceC20338h | t5.d / t5.h | interfaces | holder 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:
com.eyecon.global.Call.MyPhoneReceiver.onReceive(Context, Intent)(MyPhoneReceiver.java:37-54) - exported, priorityMAX_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)).
- Accepted actions:
im.RunnableC17334kcase 7 (im/RunnableC17334k.java:868-1111):CALL_SCREENING_NEW_CALLandINTENT_ACTION_START_CALL_WHATSAPPare remapped toandroid.intent.action.PHONE_STATE(lines 871-873).CallStateService.m10199x()(default dialer installed) -> theINTENT_KEY_CALLER_ID_NOTIFICATION_ENABLEextra.- Builds
Intent(ctx, CallService.class)actionEYECON_INTENT_ACTION_CALL, copies the attacker’s extras verbatim, addsEYECON_INTENT_KEY_ACTION, then startsCallService. - 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 offCallStateService.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): marksf54442d = wasIncoming, records the call viaAbstractC19327e.m35649f(cis)(the in-call map) and firesm34678f(cis, cli, ACTIVE_INCOMING_CALL_ANSWERED / ...WAITING_CALL_ANSWERED).- Finally
CallService.m10051d(intent), and forNEW_OUTGOING_CALL/RINGING/IDLEa 1-second-debouncedAbstractC20656z.m37804b(false)(after-call display kickoff).
C16937acase 3 (p315h7/C16937a.java) ->C18302h(p397l5/C18302h, the caller-ID orchestrator) ->t5.m/t5.k.t5.kcase 0 / case 2 ->DBContacts.f11707J.m10142n(cis, useAddressBook, cb)(address book) and/orw6.h.m38433f(tag, ..., cli, true, C18622a(...,29))->GET https://api.eyecon-app.com/app/getnames.jspandw6.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.t5.j.mo590e(bitmap, code)->t5.k->t5.e.mo723w(bitmap)/C18622a(...,29)->t5.e.mo721c0(AbstractC16130b)which setsf52873f(name) andf52877j= thex7.dspam entry obtained from the callback map key"CB_KEY_SPAM".t5.e.mo722s()->CallStateService.mo10213d / mo10214g / mo10215h / mo10216l(all four re-verify thatC21776dstill reports the same CLI) ->CallStateService.m10208I(call, cis, false)->m10209J(c20616d0, call, cis, false), which rebuilds the ongoing notification: channeleyecon_normal_calls_v3for a popup vseyecon_quiet_calls_v3otherwise,setFullScreenIntent(true), and three actions - Answer, Decline, and AI-assistant-or-Ignore.CallStateService.onCallAdded(Call)(lines 1557-1788) - the real telephony-side interception (MICO is the default dialer, soInCallService.onCallAddedfires):AbstractC19642b.m36086b(cli)- if the number is queued to be flushed:w0.a.m38343q(call,"REJECT")and return.m10207G(call)creates theu5.d0Call.Callbackplus theC20335e(t5.e) record;m10212c(...)adds it tocallsDetailsForAfterCall.spamBlockIfNeeded(lines 1604-1721) - see section 9.- Then
AbstractC21232a.m38319D(call2)init,C3465fanalyticsincoming_call/outgoing_call,AbstractC16794a.m32644F(...), the call-bubbleViewOnTouchListenerC20643r, aPARTIAL_WAKE_LOCK(6 h) and aPROXIMITY_SCREEN_OFF_WAKE_LOCK(6 h).
- Spam decision (section 9), then
AbstractC19327e.m35649f(cis)registers the live call andm10208I(call, cis, z13)renders the first notification. - Display: the in-call UI is
com.eyecon.global.DefaultDialer.CallActivity; the incoming-call overlay isk6.s(CallerIdWindow).k6.s.m34080B()(lines 454+) computesz11 = !isInCallScene && isIncomingCallState,z12 = remote("use_ai_assistant") && m10664u(app) && z11, showsEB_ai_assistantonly whenz11 && z12, and fires analyticsai_assistant_button_show{source=IncomingCallPopup}once per window. The caller-ID bubble path isk6.j(IB fullscreen ad shown/not shown for key: %s) andk6.i(waitForCallActivityOnCreate#onReceive - activity shown). Name/photo/spam unpacking on the UI side isk6.b(C17761b), which readsCB_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):
| Field | Column | Meaning |
|---|---|---|
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_type | Meaning |
|---|---|
-1 | Unknown if spam |
0 | Not a spam by server |
1 | Spam by server |
2 | Suspicious as spam |
3 | Spam by user |
4 | Not 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 listf57972c. 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 theSpamDBexecutor; used byCallStateService.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 inx5.f.m38832w0(assistant settings) andp397l5/C18297ccase 0, which flipsSP_KEY_IS_AI_ASSISTANT_SPAM_CALL_AUTO_USE_ENABLEandSP_KEY_BLOCK_SPAM_CALLStogether. - 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 tospam_list, toDBContacts, 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) plusNotification.PRIORITY_*.
9.6 Report / block flows to the serverh5
| Path | Class | Endpoint | Body |
|---|---|---|---|
| Report spam (user) | x7.v.m38873f(cli, cis, isSpam, spamType, name) -> C20337g.m37273d + bm.RunnableC3663m1 | POST 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 tag | x4.b.m38813f(cis, prevName, newName, tag) -> bm.RunnableC3589a.m3809a | POST .../app/resuggest_name | {"cli", "previous_name"?, "suggested_name"?, "tag"?, "langs"} - tag is suppressed when "not_set" or "spam". |
| Block/unblock one number | InterfaceC21655b.mo3140g(str, str2, boolean) -> DBContacts | (local) | |
| Notify contacts about spam | C21867v.m39106p() / m39108u() + RunnableC21865t / RunnableC20081f | (local DBContacts + UI) | the remote reportSpam flag gates the “add to spam” card |
| Bulk spam-list replacement | w9.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
| File | Real | Purpose |
|---|---|---|
EnumC21668o | x7.o | enum SPAM, BLOCK - the two Spam&Block pager tabs. |
C21670q | x7.q | the DAO, see 9.3. |
C21654a | x7.a | ReportAndShareDialogFragment (R.layout.report_and_share_dialog) showing the reported name/phone plus a Share button. Static m38858q0(cli, cis, name). |
C21656c | x7.c | ReportSpamDialogFragment (R.layout.report_spam) implementing x7.u; static m38859i0(cli, cis, name, source, spamType, FragmentManager); shown as "ReportSpamDialog". |
C21661h | x7.h | BlockAdapter (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. |
C21660g | x7.g | ViewHolder (blockLayout, blockTag, TV_name, TV_number, EA_contact_image). |
C21659f | x7.f | loader callback - pulls CB_KEY_SPAM + name from the async result and updates the avatar with d.a.m31044c(...). |
C21664k | x7.k | lambda x7.b - inserts a freshly reported number at the head of the Spam list and refreshes the empty-state view. |
C21665l | x7.l | AbstractC16130b for the Spam&Block fragment load (spam list + blocked list merge). |
C21666m | x7.m | ViewPager.OnPageChangeListener - analytics blockSpam_section_swipe{Section: to_block|to_spam}. |
C21667n | x7.n | add/remove result handler for the Block/Spam adapters (toasts "Failed with error code: ..." on failure). |
C21669p | x7.p | PagerAdapter - page 0 = Block, page 1 = Spam (R.string.block / R.string.spam). |
RunnableC21663j | x7.j | progress-dialog show/dismiss. |
ViewOnClickListenerC21662i | x7.i | add-number / add-spam button handlers. |
ViewOnFocusChangeListenerC21673t | x7.t | analytics Spam dialog edittext, has focus = <bool>. |
RunnableC21672s | x7.s | focuses the “suggested name” field / dismisses the waiting dialog after 3 s. |
C21675v | x7.v | SpamReportHandler - 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 / InterfaceC21655b | x7.u / x7.b | dialog->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 rowsinsert("spam_list", ...); keepsC21670q.f57972cin sync; entries withspam_type == 4are 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.
- case 1: read one spam entry (
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 writeSP_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.
- case 5: builds the after-call tag buttons (
10. Flow 3 - AI SMS assistant, end to endh4
10.1 Trigger sources (all 10 MessageAssistantSource variants and where they are produced)h5
| Source | Produced at | Gating |
|---|---|---|
IncomingCallFullscreen | u5.a (C20609a) cases 1/default -> CallActivity.m10167N0(...); w5.c C21258c.java:811 | use_ai_assistant remote flag |
IncomingCallPopup | k6.s (CallerIdWindow) lines 1593, 1656, 1720 | use_ai_assistant and SMS permissions |
IncomingCallNotification | CallStateService.m10209J line 1076 | when !use_ai_assistant && !f11855i, the notification’s third action becomes “Ignore” instead of “AI assistant” |
MissedCallPopup | p492q5/RunnableC19573b.java:79, ViewOnClickListenerC19574c.java:65 | m10664u -> m10651h permission dialog |
AfterCall | x4.e (RunnableC21604e) case 4, line 129 | !m10666w() (tutorial already seen) |
FollowUp | x5.n (the assistant itself, on each follow-up) | - |
CallDeclinedAutoUse | x5.h.m38840h <- CallStateService.m10185f line 201 | SP_KEY_IS_AI_ASSISTANT_DECLINED_AUTO_USE_ENABLE and call age < 2 min |
CallMissedAutoUse | x5.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 |
SpamCallAutoUse | x5.h.m38842j <- CallStateService.onCallAdded line 1634 | SP_KEY_IS_AI_ASSISTANT_SPAM_CALL_AUTO_USE_ENABLE and the number’s spam_type in {1,3} |
UnknownCallerAutoUse | referenced 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 anyway10.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
- The feature must be enabled remotely -
AbstractC15804d.m31081b("use_ai_assistant")(thed5remote-config bag) gates every entry point, andx5.h.m38838f(type)gates all four auto-use variants. - Runtime SMS permissions -
AbstractC9039h.m10664u(ctx)requiresREAD_SMS,SEND_SMSandRECEIVE_SMS. Where the check fails, aFeaturePermissionsDialog(C17517c, stringssms_permission_required/sms_permission_msg, iconR.drawable.ic_messages) is shown first (AbstractC9039h.m10651h), and only after the permission doesp397l5/C18297c.mo34422Zstart the flow. If the user denies,ai_assistant_missing_sms_permission{source}is fired fromC18297c.mo34423a0. - Auto-use (no tap) variants:
SP_KEY_IS_AI_ASSISTANT_{DEFAULT,DECLINED,UNKNOWN_CALLER,MISSED_CALL,SPAM_CALL}_AUTO_USE_ENABLE. - Call-age window for declined/missed auto-use:
System.currentTimeMillis() - callTimeMS < TimeUnit.MINUTES.toMillis(2). - Spam auto-use additionally requires the spam row’s
spam_type in {1, 3}. - Conversation idempotence: a new start for a number that already has a live flow aborts (
"sendStartingMessage canceled, conversion already alive"). - 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>/RecordedCallswhenAbstractC0042b0.m178a0()(external storage present), otherwisegetFilesDir()/Eyecon/RecordedNotes|RecordedCalls; alwaysmkdirs().m31461d(String name, String dir)-> MediaStore insert; API >= 29relative_path = Documents/Eyecon/<dir>, else legacy_data = <external>/<dir>/<name>;mime_type = audio/amr.m31459b(int, Uri, String)(delete) andm31462e(Comparable src, String name, String dir)(move; returns the newUri/File).
p260e5/C16102d (e5.d, AudioRecordsDataManager):
m31471e(boolean)scans MediaStore for rows underDocuments/Eyecon/RecordedCalls/orDocuments/Eyecon/RecordedNotes/(or_data LIKE '%<externalFiles>%'pre-29) whose_display_namematches.+__.+__[0-9]+(__s|__c)?(__-1|__1|__2)?__(ern|erc)\..*, opens each stream to prove non-emptiness, and parsesC16104fmetadata (throwsParseExceptionon 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_TIMESTAMPmigration.
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(...):
| Field | Pending name | MIME | Progress key |
|---|---|---|---|
f40671d | pending_temp_eyecon_backup_for_calls_records | (per-file) | PROGRESS_RECORDS |
f40672e | pending_temp_eyecon_backup_for_notes_records | (per-file) | PROGRESS_RECORDS |
f40673f | pending_temp_eyecon_backup_for_ttm_audios | application/zip (<filesDir>/ttm.zip) | PROGRESS_KEY_TTM |
f40674g | pending_temp_eyecon_backup_for_database | application/x-sqlite3 (contactdb) | PROGRESS_DATABASE |
f40675h | pending_temp_eyecon_backup_for_call_logs | application/json | PROGRESS_CALL_LOG |
f40676i | pending_temp_eyecon_backup_for_address_book | application/json | PROGRESS_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): iff11852f(dontShowAftercall, e.g. the TTM redirect number97243727022) ->CallActivity.m10159J0(); if already shown -> skip. Otherwise it builds aC11856mJSON array ofx4.o.m38816b(...)records, then callsBaseAfterCallActivity.m9928z1(this, json, jLastCallMS, isPremium, true).x4.c(RunnableC21602c) picks the activity class:compact_after_callremote flag orSP_AFTERCALL_TYPE == 2->CompactAfterCallActivity; otherwiseDefaultAfterCallActivity(orAfterCallWithNoAutoFinishwhen 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 aSplashInterstitialActivityfor 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:
| Variant | Layout | Yes action | No action |
|---|---|---|---|
| 0 | R.layout.yes_no_question_layout | after_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) |
| 1 | R.layout.yes_no_question_layout (EBuserFeedbackYes / EBuserFeedbackNo) | as above | as above |
| 2 | R.layout.yes_no_question_layout (YES = spam) | after_call_is_spam_yes_click | after_call_is_spam_no_click + m38873f(..., 4, ...) |
| 3 | R.layout.spam_question_layout (confirm_contact_name) | x4.b.m38812e(..., "correct_name_no" / "add_name", "after_call", ...) + m38813f | same |
| 5 | string from AbstractC19949a.m36768f(i3) (nickname confirm) | x4.b.m38812e(..., "nickname_yes", "after_call", ...) + m38813f | same |
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
| File | Real | Purpose |
|---|---|---|
C21600a0 | x4.a0 | UserFeedbackDialog - tag chips (BUSINESS / spam / other / FRIEND), suggested-name field, restart checkbox, ebSave handled by x4.y. |
C21606g | x4.g | merged BroadcastReceiver: CLOSE_SYSTEM_DIALOGS (homekey / recentapps), ACTION_SCREEN_OFF, AftercallAdRefreshWorker.ACTION. |
C21608i | x4.i | AbstractC16130b with a 3 s timeout that continues RunnableC21602c for the activity launch. |
C21609j | x4.j | merged ad-loading holder. |
C21610k | x4.k | ViewPager.OnPageChangeListener for the after-call pager (drives m9934D1/m9979x1/…). |
C21611l | x4.l | after-call hook dispatcher (mo9962a1 / mo9960Y0 / mo9965d1 / mo9961Z0). |
C21612m | x4.m | AbstractC19051d tiered ad-refresh checkpoint (high / low tier). |
C21613n | x4.n | ad-slot Comparable (sorts by the acascore_<key> preference, then index). |
C21617r | x4.r | after-call PagerAdapter (per-call page; name/avatar binding; EB_name_edit). |
RunnableC21607h | x4.h | case 0 close/finish; default = share_my_location analytics + share. |
RunnableC21618s | x4.s | bubble-options-after-call. |
RunnableC21621v | x4.v | after-call view helper. |
RunnableC21622w | x4.w | SplashInterstitial ad sequencing (aftercall_interstitial_bubble, calls_since_last_ad, calls_since_last_app_open_ad, app_open_ad_feature_enabled). |
ViewOnClickListenerC21603d | x4.d | no-op click listener (base-activity guard). |
ViewOnClickListenerC21624y | x4.y | ebSave -> validates R.string.error_message_save_name_and_tag, calls m38807r0(name), optionally requests restartEyecon. |
ViewOnClickListenerC21625z | x4.z | tag-chip selector (sets f57735V / f57995r). |
ViewOnLongClickListenerC21616q / ViewOnLongClickListenerC21620u | x4.q / x4.u | three 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
| Class | Role |
|---|---|
n5.b -> C18703b | SIM/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 -> C18705d | TtmDatabase - 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 -> C18707f | Message(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 -> C18708g | TtmMessagesPolling - 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 -> C18709h | TtmTokens - 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
| Step | Where | What happens |
|---|---|---|
| 1. Balance check | n5.h ctor -> w6.d case 7 | GET .../ttm-get-tokens-amount?mycli= (JSON accept, Eyecon users only). |
| 2. Zero balance => teardown | n5.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 init | w6.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 session | p572u4/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 text | w6.d case 1 (RunnableC21276d.java:90) | GET .../getText?mycli= -> C15802b (p236d5, out of cluster) m31518o(res) / m31516i(), or m32066g(code, "CB_ERROR"); m31515h(). |
| 6. Acknowledge | w6.b case 1 (RunnableC21274b.java:70) | POST .../ack with the polled payload. |
| 7. Verify call-forwarding setup | w6.d case 3 (RunnableC21276d.java:145) | GET .../ttm-call-forwarding-result?mycli= -> C3463d parses the boolean. |
| 8. Teardown | as/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, national04-372-7022) -AbstractC17503r.m33607k("SP_KEY_TTM_PHONE_NUMBER", "97243727022"), and hard-coded atCallStateService.java:530,1696,1766;p315h7/C16937acase 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. PhoneAccountHandlerole 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) fromMyApplication.f12163hand setsf11652b = true. - Hands everything to
im.RunnableC17334k(action, ctx, extras, false, 7).
Reachable effects of a crafted broadcast (all in im/RunnableC17334k.java:868-1111):
CallService.f11630f = 1andstartService(Intent(CallService, "EYECON_INTENT_ACTION_CALL") + putExtras(attackerBundle))- arbitrary extras enter the service.CallStateServicestatic 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).statemaps toIDLE=0 / OFFHOOK=2 / RINGING=1(any other value throws aRuntimeExceptionthat is only logged);incoming_numberis normalised tocisand then used verbatim.- On RINGING:
C18302hcaller-ID lookup is kicked off for the attacker’s number;CallStateService.m34675c/m34677e/m34678ffire; if remoteIncomingCallPrefetchMode == "when ringing"the after-call pipeline is armed. - On IDLE from RINGING/OFFHOOK:
m34674b(1|2|3, ...)classifies the call as declined / missed / answered; the missed branch may scheduleAbstractC19578g.m36035a(6, ...)(contact reminder) and aDBContactswrite. NEW_OUTGOING_CALL:extras.getString("android.intent.extra.PHONE_NUMBER")becomes the outgoing CLI;AbstractC19327e.m35649f(...)andAbstractC16794a.m32647J("Call started")fire; the number is written into the in-call map.AbstractC19642b.m36086b(number)(“flush call”) ->C18505s.m34701a("flush call")-> a server round-trip for that number, and the resultingisSpamvalue can driveCall.disconnect()throughCallStateService.onCallAdded(lines 1569-1572, which runs regardless of who triggered the broadcast).- Every path ends with
CallService.m10051d(intent)and the 1-second-debounced after-call kickoffAbstractC20656z.m37804b(false)forNEW_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 returnsisNewMessage=false+noReadMessages+dateMessage; otherwise it inserts{date, noReadMessages=1, body, sender, text, messageId}withinsertWithOnConflict(..., CONFLICT_REPLACE)and logs"Insert new ExternalSms: body: <BODY> sender: <SENDER> messageId: <id>"(S6).u7.s->AbstractC20698s(30 KB) = theExternalSmsDBmanager (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 byC18302h).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 thesms_permission_pre_prompt_msgpre-prompt whenSEND_SMSis 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_popupcontainer,bubbleOptionsInCallPopup, and the AI-assistant button (EB_ai_assistant, see section 8 step 9). ImplementsInterfaceC20639p(bubble click) andInterfaceC20338h(person-data callbacks).k6.a->EnumC17759acall 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; readsCB_KEY_SPAM,received_name,received_photo,suspicious_spam,contactand fireseventX, updateEventCallInformationIfNeeded$onDone.k6.i->C17771i: screen-state receiver forEYECON.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:EyeDynamicDrawablewith colours#D2D5DD/#F38A2A/#F94960and aMAYBE SPAMlabel.k6.k->Handler.CallbackforEYECON, 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= theCall.Callback: fieldsEYECON.EXTRA_KEY_CALL_CIS,EYECON.EYECON_CALL_ACTION_STATE_UPDATE,EyeconCallDetails, plusf54436a(cli),f54437b(cis),f54438c(t5.eperson record),f54439d(Call),f54440e(isIncoming),f54441f(call type),f54444i(ignored),f54445j(blocked),f54447l(answered),f54448m,f54454p. Methodsm37732a()(state),m37733b()(start time),m37734c(),m37735d()(detach).u5.a->C20609a: the TTM redirect button. Case 0 setsSP_KEY_DID_CLICK_CALL_TTM_BTN=true, marks the call ignored, shows"Redirecting call to Talk to me...", rejects the call viaCallStateService.m10185f(), generatesUUID.randomUUID(), and registers a TTM pending message withn5.g.m35052d. Case 1/default firesCallActivity.m10167N0(new MessageAssistantSource.IncomingCallFullscreen()).u5.f-> mergedBroadcastReceiverforEYECON.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 - stringsdefault_dialer,call_screening,must_permissions,general,default_dialer_enable,last_permission_notification_time,*_notification_interval_hours, and the deep linkeyecon://open_must_permissions.u5.h/u5.j-> additionalCall.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-bubbleWindowManagerview (addBubbleWindow,CallBubble,privateFlags).u5.l0-> a merged runnable withAction/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 includeAI 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 mergedx5.v.m38851acall 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, tagreportBlock, body{"type":"blocked","blocked_numbers":[...]}(requiresAbstractC9039h.m10665v()). - case 5 PhotoPicker name search:
GET .../app/getnames.jspwithcli,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 theAudioTrackplayback completion. Also case 23 (dynamic_area_ticketsupdate), case 8 (table_share_mediadelete), cases 26-28 (theme background rendering), case 22 (Singular consent queue). Remainingv7files: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.yand runnablesv7.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 containsCanTalkActivitywiring,https://eu1.locationiq.com/v1/search.php/https://us1.locationiq.com/v1/search.phpgeocoding,Blowfish+cipher_key, andfb_mobile_obtain_push_token. TheBlowfish/cipher_keypair 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 flagsincall_feed_big_small_ads_position(small_top_big_bottomvs the reverse) andincall_feed_middle_ad_enabled; in premium it drops slots 8, 7 and 3 entirely.lg.n(AbstractC18372n) - merged notification-channel helper used byCallStateService.m10209J(AbstractC18372n.m34530r(this, channelId, title, importance, ...)).
16. Tables, endpoints and pref-keys touched by C3h4
SQLite tablesh5
| Table | Where | Columns touched |
|---|---|---|
spam_list | x7.q, w9.a, w9.b | contact_id, cli_in_server, cli, name, spam_type, create_timestamp |
sms_table | u7.b, u7.s | f144D1 (date), f147E1 (unread), f150F1 (body), f153G1 (sender), f156H1 (text), messageId |
ttm | n5.d (insert via im/RunnableC17334k case 11) | cli, cli_in_server, date, audio_url, message_id, is_read, is_played |
recorded_notes | e5.d | created at p008a7/C0041b.java:318; columns added by migrations f230j1 and f149F0 |
table_api_requests | w6.c case 12, w9.b case 6 | the retry queue |
table_share_media | v7.f case 8 | share tracking |
dynamic_area_tickets | v7.f case 23 | f148F, f268w0 |
names_photos_cache | t5.g (PersonDataCache) | cli_in_server, modify_time, spam_type, data-type discriminator |
| contact block list | DBContacts (first-party) | blocked C16470g0 rows |
HTTP endpoints touched from C3h5
| Endpoint | Method | Called from | Purpose |
|---|---|---|---|
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_a | GET | w6.h.m38434g | caller-ID photo (and name, via the pic record) |
.../app/getnames.jsp | GET | w6.h.m38433f, v7.f case 5 | caller-ID name lookup / PhotoPicker search |
.../app/spamreport | POST | bm.RunnableC3663m1, v7.f case 4 | {cli,name,is_spam} / {type:"blocked",blocked_numbers:[]} |
.../app/resuggest_name | POST | bm.RunnableC3589a.m3809a via x4.b.m38813f | {cli,previous_name?,suggested_name?,tag?,langs} |
.../app/addcontacts.jsp | POST | w6.h.m38428a | full address book |
.../app/getcontactslang.jsp | GET | w6.h.m38428a | contact-language detection |
.../app/addfbtoken.jsp | GET | w6.c case 1 | Facebook token |
.../app/updateuserdetails.jsp | GET | w6.c cases 2-4 | adv_id, mc, app version |
.../app/upgrade_pic2eyecon.jsp | GET | w6.c case 6 | photo upgrade nudge |
.../app/uploadpic.jsp | POST multipart | w6.h.m38441n | profile photo |
.../app/messages/received | POST | w6.f case 1 | external SMS ids (not bodies) |
.../app/messages/get | GET | w6.d case 5 | external SMS sync |
.../app/virality/premium_status, .../app/virality/viral_id, .../app/virality/viral_id_check | GET | w6.d cases 2/4/9 | virality |
.../app/get_store_products | GET | w6.d case 6 | store |
.../app/mysettings | GET/POST | w6.d case 8, p572u4 case 7 | settings |
.../app/cantalk.jsp, .../app/getcantalkstatus.jsp | GET | p474p5 [out of cluster] | CanTalk |
https://voice2text.eyecon-app.com/init-ttm | POST JSON | w6.b case 0 | TTM session init |
https://voice2text.eyecon-app.com/ack | POST JSON | w6.b case 1 | TTM ack |
https://voice2text.eyecon-app.com/getText | GET | w6.d case 1 | TTM poll |
https://voice2text.eyecon-app.com/ttm-call-forwarding-result | GET | w6.d case 3 | TTM CF check |
https://voice2text.eyecon-app.com/ttm-get-tokens-amount | GET | w6.d case 7 | TTM balance |
https://voice2text.eyecon-app.com/start-ttm | POST JSON | p572u4 [out of cluster] | TTM start |
https://voice2text.eyecon-app.com/no-ttm | GET | as [out of cluster] | TTM teardown |
http://numberlookup.instabridge.com/respond-sms | POST JSON (Retrofit) | x5.a / x5.n / x5.o | the AI assistant |
https://eu1|us1.locationiq.com/v1/search.php | GET | k5.b [merged, out of C3 flows] | geocoding |
remote server_report_url | POST gzip | w6.f case 0 | call report |
| 6 x Google-Drive pending upload URLs | POST | p282f5 [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
- 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 thenewMessagepayload can cause MICO to send attacker-chosen SMS to arbitrary destinations chosen by the assistant. - S2 - exported, unpermissioned, max-priority
MyPhoneReceiver. Any app on the device can injectstate/incoming_numberand drive the call state machine, DB writes, analytics, and (while MICO owns a call) forceCall.disconnect(). Full reachable surface enumerated in section 14.
Mediumh5
- 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. - S4 - hard-coded shared API secret
PgdtSBeR0MumR7fOshipped in the APK and attached to every*.eyecon-app.comrequest ase-auth-k;e-auth-cis a trivially reversible function of the CID. - 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. - 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. - S7 - call-recording audio in shared external storage (
Documents/Eyecon/RecordedCalls|RecordedNotes), with the pre-Android-Q path written through the legacy_datacolumn andrequestLegacyExternalStorage="true"set. MICO does not create the files, but it indexes them, displays them, and uploads them (plus the plaintextcontactdb) to Drive. Recording-consent enforcement therefore rests entirely outside the app.
Lowh5
- S8 - debug “Ad stats” dialogs reachable by three long-presses in both after-call activities.
x7.q.m38870bbuilds 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.- Spam-tag semantics are enforced by a UI allow-list that is also the outbound payload: if
x7.v/x4.a0ever gains a tag value not in the list, the suppression inx4.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/receivedcarries ids only). The only SMS-content egress is the assistant’srespond-smscall. - No
TelecomManager.endCall()anywhere. - No
cantalksaw.jspanywhere. - No
crylpted/EncryptedSharedPreferencesusage in the C3 paths - all C3 storage is plaintext.
18. Traceability indexh4
| Briefing requirement | Section |
|---|---|
| Package list, file counts, responsibilities | 0.4 |
| Per-package class tables | 1-7, 9.8, 15 |
| Flow 1 - incoming-call interception -> lookup -> display -> spam decision | 8 |
| Flow 2 - spam classification and blocking | 9 |
| Flow 3 - AI SMS assistant end to end | 2, 3, 4, 5, 10 |
| Flow 4 - call recording (capture -> storage -> upload) | 0.2, 11 |
| Flow 5 - after-call feedback / follow-up | 12 |
| Flow 6 - TTM lifecycle | 13 |
MyPhoneReceiver reachable surface | 14 |
| Tables / endpoints / pref-keys | 16 |
| Security findings + corrections | 0.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):
| field | value | field | value | field | value |
|---|---|---|---|---|---|
| f207c | ab_raw_id | f210d | contact_id | f213e | cli |
| f216f | cli_in_server | f219g | name | f222h | is_pic_synced |
| f225i | update_date | f228j | call_count | f231k | cli_type |
| f234l | is_primary | f237m | is_single | f240n | sent_to_server |
| f243o | is_survey_pic | f246p | survey_pic_url | f249q | survey_pic_tag |
| f252r | has_photo | f255s | is_sub | f258t | is_viewer |
| f261u | os_type | f264v | sms_count | f267w | whatsapp_count |
| f270x | can_talk_count | f273y | is_can_talk_enable | f276z | vn |
| f133A | essage | f136B | source | f139C | is_group |
| f142D | name_group | f145E | photo | f148F | id |
| f151G | fav_pin_pos | f154H | last_action_date | f157I | action_type |
| f160J | is_favorite | f163K | pinned_position | f166L | is_like |
| f169M | was_invited | f172N | pic_process_done | f175O | last_approved_tag |
| f178P | facebook_id | f181Q | favorite_timestamp | f183R | can_talk_status |
| f185S | can_talk_timestamp | f187T | pic_status_in_server | f189U | contact_photo_uri |
| f191V | score | f193W | last_action_date_long | (f195X alias) | duration |
| f197Y | contact_action_date_mili | f199Z | fresh_pic_init_url | f202a0 | fresh_pic_init_tag |
| f205b0 | has_fresh_pic_init | f208c0 | has_thumbnail_photo | f211d0 | custom_phone_number_label |
| f214e0 | pre_eyecon_usage | f217f0 | eyecon_favorites_pinned_pos | f220g0 | default_cis |
| f223h0 | storage_photo_path | f226i0 | ab_photo_type | f229j0 | extra_values_json |
| f232k0 | data_type | f235l0 | data_part | f238m0 | type |
| f241n0 | subject | f244o0 | priority | f247p0 | max_appearances |
| f250q0 | appearances | f253r0 | schedule_expression | f256s0 | last_seen |
| f259t0 | subject_description | f262u0 | subject_path | f265v0 | screens |
| f268w0 | had_user_interaction | f271x0 | local_ticket | f274y0 | order_value |
| f277z0 | max_per_session | f134A0 | absolute_position | f137B0 | first_position |
| f140C0 | steps | f143D0 | one_per_x_days | f146E0 | question_type |
| f149F0 | call_direction | f152G0 | main_cis | f155H0 | main_cli |
| f158I0 | secondary_cis | (f161J0 = duration alias) | f164K0 | social_type | f167L0 |
| f182Q0 | big_pic_url | f184R0 | small_pic_url | f186S0 | pic_seen |
| f188T0 | received_date | f190U0 | fresh_pic_tag | f192V0 | ROWID |
| f194W0 | is_hash_updated | f196X0 | default_sim_id | f198Y0 | is_enable |
| f200Z0 | create_timestamp | f203a1 | rowid | f206b1 | DB_COLUMN_UNICODE_SCORE |
| f209c1 | account_type | f212d1 | account_id | f215e1 | participants |
| f218f1 | text_note | f221g1 | record_duration | f224h1 | file_name |
| f227i1 | is_played | f230j1 | is_saved | f233k1 | heart_ts |
| f236l1 | text | f239m1 | message_id | f242n1 | is_read |
| f245o1 | is_spam | f248p1 | is_suspiciousSpam | f251q1 | has_name |
| f254r1 | spam_type | f257s1 | url | f260t1 | time_ms |
| f263u1 | provider | f266v1 | cis | f269w1 | is_play |
| f272x1 | is_seen | f275y1 | total_time_played_ms | f278z1 | type |
| f135A1 | server_id | f138B1 | alias | f141C1 | key |
| f144D1 | date_ms | f147E1 | no_read | f150F1 | body |
| f153G1 | sender | f156H1 | sender_name | f159I1 | json_key |
| f162J1 | server_retry_count | f165K1 | db_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 dir | Real pkg | Files | KB | Module | One-line responsibility | § |
|---|---|---|---|---|---|---|---|
| 1 | p516r5 | r5 | 36 | 696 | MainScreen | Core contact model + AddressBook (ContactsProvider) engine + SocialManager. r5.e is the single 386 KB class that reads ContactsContract.CommonDataKinds.Phone. | §2 |
| 2 | p444o0 | o0 | 111 | 391 | MainScreen | Third-party: Amplitude SessionReplay internals (not MICO logic). | §20 |
| 3 | am | am | 76 | 187 | Contacts | Third-party: Firebase Messaging / GMS worker pools. | §20 |
| 4 | p493q6 | q6 | 55 | 171 | MainScreen | Generic RecyclerView widget kit (adapters, view-holders, decorations, span-size lookups) used by the contact lists/grids. | §3 |
| 5 | p360j7 | j7 | 20 | 144 | MainScreen | Fragment base classes + j7.t (66 KB) ContactListInfo-area mega-class. | §4 |
| 6 | p541s5 | s5 | 19 | 139 | Contacts | DBContacts task-runner pool — every DB mutation lambda + removecontacts.jsp uploader (s5.s). | §5 |
| 7 | p475p6 | p6 | 16 | 103 | MainScreen | Generic app glue: fragment lifecycle hooks, Handler.Callback, page-change callbacks, content://sms readers, updateuserdetails.jsp, cantalk.jsp. | §6 |
| 8 | p236d5 | d5 | 5 | 101 | MainScreen | Remote-config / A-B flag catalogue (d5.d is a 300-entry Map<String,String>), incl. a cleartext HTTP config URL. | §7 |
| 9 | p364jc | jc | 14 | 97 | MainScreen | Fragment/dialog base + inset/layout utilities used by the contact screens. | §8 |
| 10 | p574u6 | u6 | 17 | 72 | MainScreen | Top-calls / recents adapter set (u6.a model, u6.u adapter, u6.d 20 KB refresh runnable). | §9 |
| 11 | p032b5 | b5 | 8 | 67 | MainScreen | Photo storage / cache-path helper (b5.e, b5.d, b5.f) + handler loop. | §10 |
| 12 | p246dh | dh | 17 | 64 | ContactReminder | Google Credential Manager / Play-services-auth internals (dh.i = CredentialManagerCallback). Not reminder logic despite the TSV owner. | §11 |
| 13 | p046c6 | c6 | 16 | 63 | MainScreen | ViewPager2 / animation / touch helpers (c6.a = BroadcastReceiver). | §12 |
| 14 | p315h7 | h7 | 14 | 53 | MainScreen | Executor factory (h7.i scheduler, h7.h ThreadFactory naming threads FLTask-). | §13 |
| 15 | p001a | a | 1 | 51 | ContactReminder | Single 51 KB class a.a — a Kotlin/coroutines or R-independent utility; inspected below. | §14 |
| 16 | p555t6 | t6 | 17 | 48 | MainScreen | Ad/banner + grid decoration helpers (t6.i extends AdListener). | §15 |
| 17 | p492q5 | q5 | 9 | 46 | ContactReminder | Add-contact / caller-identity sheet (q5.k extends c7.j, q5.g static entry point). | §16 |
| 18 | p474p5 | p5 | 5 | 33 | MainScreen | cantalk.jsp / getcantalkstatus.jsp client. | §17 |
| 19 | p304g6 | g6 | 5 | 29 | MainScreen | ViewPager1 PagerAdapter + page-change plumbing (legacy main-screen tabs). | §18 |
| 20 | p528rk | rk | 3 | 26 | MainScreen | rk.b enum + rk.a static holder — small tab/layout descriptor. | §19 |
| 21 | p702z6 | z6 | 5 | 26 | Contacts | z6.a = per-number NOTE record (Comparable), z6.e dialog. | §19 |
| 22 | p556t7 | t7 | 2 | 22 | MainScreen | Generic callback adapter (2 classes). | §19 |
| 23 | p238d7 | d7 | 4 | 17 | MainScreen | Base Fragment for the contact screens (d7.b = Fragment implements i71.n0) + back-press callback + custom RuntimeException. | §19 |
| 24 | p517r6 | r6 | 2 | 7 | MainScreen | ItemTouchHelper.Callback (swipe-to-delete / drag) + its interface. | §19 |
| 25 | p009a8 | a8 | 7 | 5 | MainScreen | Tiny ViewModels (a8.b/e/g extend ViewModel) + helpers. | §19 |
| 26 | p392kq | kq | 2 | 5 | Contacts | Interface + abstract base (2 classes, 5 KB). | §19 |
| 27 | p542s6 | s6 | 3 | 4 | MainScreen | Small 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 file | real | kind | purpose / key members |
|---|---|---|---|
C19846a.java | r5.a | model | AccountWithData — (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.java | r5.a0 | Observer | synthetic Rx-style onChanged(Object) bridge |
C19848b.java | r5.b | Fragment | EyeSearch account-picker fragment. static final AuthenticatorDescription[] f51112J = AccountManager.get(MyApplication.f12163h).getAuthenticatorTypes(); mo4120k0, onCreateView, onViewCreated; fields ViewGroup f51113H, LinearLayout f51114I |
RunnableC19850c.java | r5.c | Runnable | 7 branches (a..g): delete-contact / delete-call flows; calls DBContacts.f11707J.m10135X(...), m10134W(), m10123E() |
RunnableC19851c0.java | r5.c0 | Runnable | 4 branches (a..d); case 0 account-selection post-processing |
RunnableC19852d.java | r5.d | Runnable | 4 branches (a..d); contacts-refresh kick-off |
ViewOnFocusChangeListenerC19853d0.java | r5.d0 | OnFocusChangeListener | NewContactActivity — toggles hint R.string.enter_name / R.string.enter_valid_number |
C19854e.java | r5.e | static utility | ”AddressBook” — the ContactsProvider engine. 4 399 lines. See §2.1 |
C19855e0.java | r5.e0 | abstract callback | extends e2.b (async callback) — FB-profile-link probing progress holder; m31518o("NOT_A_FB_PROFILE_ERROR"), m31515h(), m31516i(), m36607q(AbstractActivityC3498e, String) |
C19856f.java | r5.f | model | CallLogInfo — 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.java | r5.g | ContentObserver | Contacts-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.java | r5.g0 | model | SocialInfo — {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.java | r5.h | model | Participant — {String cis; String cli; String name} + static m36613a(String) splitter. Used for group SMS/MMS (participants column) |
EnumC19861h0.java | r5.h0 | enum | SocialType — 47 constants (ordinals 0..46). See §2.2 |
C19862i.java | r5.i | model | ContactInfo (TAG "ContactInfo") — the central contact entity. See §2.3 |
C19863i0.java | r5.i0 | static utility | SocialManager — resolves/launches every social destination. See §2.5 |
C19864j.java | r5.j | model | PhoneNumberInfo — per-contact number record. See §2.4 |
C19865j0.java | r5.j0 | LifecycleEventObserver | lifecycle hook for NewContactActivity |
C19867k0.java | r5.k0 | abstract callback | Social-media share callback (extends e2.b); launches share sheet for a cis |
C19868l.java | r5.l | RecyclerView.ItemDecoration | grid spacing: h1(67) outer, h1(10) inner — ab list decoration |
C19869m.java | r5.m | predicate filter | implements am.b1; int[] {2} + mo31520a(ContactInfo) — multi-select predicate |
C19870n.java | r5.n | state holder | selection-mode controller: Activity, HashSet f51232c of selected contacts, AtomicInteger f51229i, String f51234e/f51235f, predicate f51237h |
C19871o.java | r5.o | RecyclerView.ViewHolder | ab-list row holder: EyeAvatar f51238c, TextView f51239d, C20343m f51240e, ContactInfo f51241f; implements t6.h (mo716Q, mo717U, mo718V, mo721c0, mo722s, mo723w) |
C19872p.java | r5.p | RecyclerView.Adapter | ab-grid adapter; getItemCount, onBindViewHolder, onCreateViewHolder; field o0.l1 f51244e (Amplitude tracking) |
RunnableC19873q.java | r5.q | Runnable | contact-options menu actions: case 0 delete call, case 4 copy number (R.string.clipboard), case 5 schedule call/message, case 6 note |
RunnableC19874r.java | r5.r | Runnable | 21-branch global dispatcher. See §2.6 |
RunnableC19875s.java | r5.s | Runnable | block / unblock confirmation dialog (R.string.block_number, R.string.block_are_you_sure, R.string.block, R.string.cancel) |
RunnableC19877u.java | r5.u | Runnable | report-spam branch |
AbstractC19876t.java | r5.t | abstract utility | contact 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.java | r5.w | model | FreshPic — {String a,b,c,d; int e,f; long g; String h,i; Bitmap j; ContactInfo k; boolean l} |
C19880x.java | r5.x | node/tree node | contact 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.java | r5.v | interface | callback |
RunnableC19882z.java | r5.z | Runnable | NewContactActivity helpers: invite-link validation (R.string.enter_valid_number, R.string.invite_link_and_text), copy to clipboard |
ViewOnClickListenerC19881y.java | r5.y | OnClickListener | NewContactActivity account picker click; compares against R.string.account_phone |
RunnableC19866k.java | r5.k | Runnable | 31 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 executorpublic 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)— queryContactsContract.Data.CONTENT_URIwheremimetype = ? AND display_name = ?andmimetype = "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 inf51144k.static ArrayList m36584e(String)/m36585f(long, String)— call-log queries.static Uri m36586g(ContactInfo)—ContentProviderOperationbatch that WRITES a contact into the device address book:newInsert(RawContacts.CONTENT_URI)+newInsert(Data.CONTENT_URI)withmimetype=vnd.android.cursor.item/nameandmimetype=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 viaopenAssetFileDescriptor(RawContacts/<id>/display_photo, "rw").static void m36582c(C19854e, byte[], long)/m36583d(long)— deletes the photo / the call-log row.static ArrayList m36592m(String)—RawContactsids forcontact_id = <id>.static Bitmap m36594o(int, String, String)— photo decode.static C16470g0 m36595p(Cursor, String)— builds a social row from anemail_v2data 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 butsocial_type = r5.h0.SKYPE.f51206b.static C19846a m36600u()/m36590k()/m36591l()— picks the default sync account; writes{"type":…,"name":…}intof51139f.static long m36597r(String)—RawContacts._idfromRawContacts.CONTENT_URIwhere_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]:
out[0]/out[1]are the out-params:[0]=C16491sof contact rows,[1]=C16491sof social (email/skype) rows.- Builds the projection (note the duplicate
data1three times and thatdata5appears 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], mimetypeaccount_nameis only appended whenSharedPreferences.getString("", "SP_EYECON_DEF_ACCOUNT_V2")is empty (i.e. no user-selected default account). MyApplication.m10348j()->SP_EYECON_DEF_ACCOUNT_V2;AbstractC0062l0.m371z1()(default country ISO).- Spawns
Executors.newFixedThreadPool(1)and submitsRunnableC19318r(hashMap, 5)— a background task inp6that also drivescontent://smsreads (§6). 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.- Per row:
mimetype == "vnd.android.cursor.item/com.skype.android.chat.action"anddata5 == 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, diskp394l5) returnsC18311awithf46071b= normalised/server CLI (cli_in_server) andf46072c= digits-only CLI (cli). Iff46071bis empty (non-mobile / VOIP / service number) the row is skipped for the normalised path and onlycli = <raw data1>+name = display_nameis kept in a secondary list.- dedup:
HashSetkeyed oncontact_id(map8) holding already-seenf46071bvalues. Only the first occurrence of each normalised number per contact becomes a canonical row:name = display_name,cli_in_server = f46071b,cli = f46072c. - otherwise (
f46071bempty) the row goes toarrayListwithcli = data1,name = display_nameand is normalised later, after the cursor is exhausted, usingmap3(the cache filled byRunnableC17219d1).
- The skipped-number path re-checks the same
HashSetand, on success, writesname(falling back to the digits-only CLI),cli_in_server,cliand appends to the sameC16491s.
- Tail:
executorService.shutdown(); awaitTermination(100, SECONDS). - Language detection:
map.get(f51144k.toLowerCase())->Locale;f51134a = locale.getLanguage().toLowerCase(). - Persists
SP_KEY_TOP_CONTACTS_LANGUAGE = f51134aandSP_KEY_AMOUNT_OF_CONTACTS = contacts.size(). - Sets
out[0] = contacts,out[1] = socials, then invokes theRunnable donecallback. - Retry:
moveToFirst() == -1 && i3 == 0-> close and re-enterm36603x(..., 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 "[]").
| constant | id | package | kind | constant | id | package | kind | |
|---|---|---|---|---|---|---|---|---|
| 1 | com.facebook | 1 | SMS | TRANSIT_FRAGMENT_MATCH_ACTIVITY_OPEN (=65536) | sms | 4 | ||
| 2 | com.linkedin.android | 1 | CALL | 4101 | ”call” | 1 | ||
| VKONTAKTE | 4 | com.vkontakte | 1 | 4128 | ”email” | 1 | ||
| 8 | com.instagram | 1 | NAVIGATION | 4129 | ”navigation” | 16 | ||
| BBM..YAHOO_MESSENGER | 4101..4119 | … | 1 | CALENDER (sic) | 4132 | ”calender” | 16 | |
| KIK/IMO/OOVOO/AZAR/… | 4121..4127 | … | 1 | VIDEO_CALL | 4134 | ”video_call” | 1 | |
| 4118 | com.whatsapp (or com.whatsapp.w4b) | 2 | SHARE_MY_LOCATION | 4135 | ”share_my_location” | 16 | ||
| WHATSAPP_CALL | 4130 | com.whatsapp | 2 | CUSTOM_SOCIAL | -1 | "" | 1 | |
| WHATSAPP_VIDEO | 4131 | com.whatsapp | 4 | TRUE_CALLER | -10 | com.truecaller | 1 | |
| TELEGRAM | 4114 | org.telegram.messenger | 4 | TOKI | -11 | "" | 1 | |
| VIBER | 4116 | com.viber | 4 | GOOGLE_PHOTOS | 4222 | "" | 1 | |
| 4109 | com.tencent.mobileqq | 4 | ADD_CONTACT | 4223 | "" | 1 | ||
| SKYPE | 4111 | com.skype.raider | 32 | REPORT_SPAM | 4224 | "" | 1 | |
| WE_CHAT | 4117 | com.tencent.mm | 1 | CAN_TALK | 4225 | "" | 1 | |
| HANGOUTS | 4104 | com.google.android.talk | 1 | CALL_REMINDER | 4226 | "" | 16 | |
| GOOGLE_MESSENGER | 4125 | com.google.android.apps.messaging | 1 | NOTE | 4227 | "" | 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— primaryPhoneNumberInfo(synthesises one fromphone_numberif the list is empty).m36632n():r5.j— same, withcli_in_serverexplicitly 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 withisSub == true.m36635q():r5.j— number matchingl7.b.m34444e(phone_number)(the default number).m36634p(String):z6.a— per-number note;m36636r(String):String— per-numberdefaultSimId.m36637s(r5.h0):r5.g0— social info for a type;m36638t(char[][])— T9 predictive digits ofprivate_name(space->0), memoised onT9Arrayidentity.m36641w()— “has a possible mobile number” (cachedhasPossibleMobileNumber).m36643y()—trueifcontact_idis 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 (andm36630l()returnscli_in_servervscontact_idon 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)— setextraValuesJson/storage_photo_path(the latter also firesRunnableC3460afromb5).
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()= lazilycli_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 viaContactsContract.CommonDataKinds.Phone.getTypeLabel(...)into aSparseArraycache.compareToorders byisDefaultdesc,scoredesc, type-rank, thenclilexicographic.
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); // legacyString url;try { Long.valueOf(socialId); url = "https://m.facebook.com/profile.php?id=" + socialId; } // numeric -> idcatch (NumberFormatException) { url = "https://m.facebook.com/" + socialId; } // usernamelong t0 = SystemClock.elapsedRealtime();act.m3727x0(true); // show blocking progressC19855e0 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:
- 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 inheritsis_viewer,os_type,is_sub,is_can_talk_enable, andvn(copied throughm32032a/…with a clampm32031z(0, vn)). All are collected inarrayList9. - Socials pass — for each incoming social row:
- all local
socialrows matching it getsocial_idoverwritten andsocial_source = 1(meaning “came from the server”), and theircontact_idvalues are recorded intohashSet; - then every contact number of that person that does not yet have a social row gets a
new
socialrow 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 withsocial_type = FB_MESSENGER(4103)— i.e. a Facebook match implicitly creates a Messenger match.
- all local
DBContacts.m10099U(null, null, socialRowsToWrite)persists thesocialtable, thendBContacts6.f11726d = DBContacts.m10092D()reloads it.DBContacts.m10093O(null, null, arrayList9)persists the contact rows, thendBContacts6.f11724b = DBContacts.m10118v()reloads them.dBContacts6.m10135X("getUpdatedContactInfo_")marks the table dirty;m10134W()/m10123E()flush.- 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.vadapter. 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 inMainFragment.f12024jand captures the search text fromMainFragment.f12027m.getText()intof50395i(trimmed); readsarguments.getBoolean("EXTRA_KEY_INIT_WITH_KEYBOARD_OPEN").mo31174n0()— forwardsMainActivity.getIntent()tomo10240A(intent).m36059y0(Rect, View, RecyclerView, int span, boolean)— 1/3-width offset decoration for the 6-column pyramid grid (each cell ism183B(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]:
| real | purpose |
|---|---|
q6.a1 | abstract RecyclerView.OnScrollListener — shared scroll behaviour (infinite scroll for the contact list) |
q6.b | OnScrollListener impl used by m36061v0 |
q6.c | LinearLayoutManager subclass (1 column) |
q6.v0 | LinearLayoutManager subclass (top-calls area) |
q6.d/q6.f/q6.h/q6.x0/q6.y0 | RecyclerView.ItemDecoration for 6-/2-/3-column grids and list |
q6.e/q6.g/q6.i/q6.z0 | GridLayoutManager.SpanSizeLookup — full-span detection for ad/header rows |
q6.k/q6.k0 | abstract RecyclerView.ViewHolder base + a plain holder |
q6.e0 | abstract clickable/long-clickable holder (implements t6.h, am.b1) |
q6.i0 | contact-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.z | contact-grid cell holder: CG_info_area (ContactGridTextArea) + FL_contact_photo_container |
q6.v | the 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.n | RecyclerView.Adapter for call-log cells (R.layout.call_log_cell) |
q6.m | call-log row holder: IV_sim, IV_type, IV_circle, TV_duration, TV_date, TV_time, FL_sim, TV_sim |
q6.s0 | 3-button row holder (EB_left_btn, EB_middle_btn, EB_right_btn) |
q6.o0 | shimmer placeholder holder (SFL_contacts, IV_icon, TV_text) |
q6.c1 | per-adapter cache object (ArrayList cleared by r5.k case 15) |
q6.n0, q6.k0, q6.a0, q6.c0, q6.u0, q6.l | small helpers: diff delegate, popup helper, scroll-state holder, RecyclerView.ItemAnimator subclass, layout-param helper |
q6.w0 | RecyclerView.OnFlingListener |
q6.x | FragmentStateAdapter for the ViewPager2 |
q6.m0 | enum { SINGLE } |
q6.b1, q6.p, q6.d1 | interfaces: 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.
| real | purpose / key members |
|---|---|
j7.m | PermissionStatusTracker. 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.t | the 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.a | concrete ContactListInfoArea-family sheet (extends j7.t) |
j7.c | permissions 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.j | single-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.h | static 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.r | async callback adapters (extends e2.b) |
j7.g | Rx Observer |
j7.p | CompoundButton.OnCheckedChangeListener |
j7.x | singleton (f44155g) holder |
j7.w | DefaultLifecycleObserver |
j7.e, j7.n, j7.q, j7.s, j7.u, j7.v | small 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.
| real | purpose |
|---|---|
s5.a | Comparator 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.f | wrapper 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.j | 54.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.r | inner-dispatch helpers |
s5.s | removal 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]
| real | purpose |
|---|---|
p6.j | 29-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.r | SMS/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.g | e2.b async callback + FragmentManager.FragmentLifecycleCallbacks |
p6.n | Handler.Callback |
p6.o | ViewPager2.OnPageChangeCallback |
p6.s | MotionLayout.TransitionListener |
p6.p | main-screen toolbar click handler (14 branches): R.string.favorites + R.string.organize_favorites, R.string.for_you, R.string.history, R.string.select |
p6.q | View.OnFocusChangeListener |
p6.a, p6.c, p6.e, p6.i | small 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
| key | default | note |
|---|---|---|
eyecon_remote_config_url | http://cloudfront-cdn-all.eyecon-app.com/configs/ab-test.txt | CLEARTEXT 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. |
shouldSendNameToServer | false (bool2) | remote switch to push contact names to the server |
ApiRequestSyncInterval | 20 | seconds |
user_agent | Mozilla/5.0 (Windows NT 10.0; Win64; x64) …Chrome/123.0.0.0 Safari/537.36 | desktop UA used for in-app web requests (WebView fingerprint spoofing) |
facebooke_Extract_URL_Regex | browser_native_sd_url":"([^"]+)" | regex used to scrape Facebook profile URLs from an HTML page |
script_facebook_profile_validate | "" (empty) | remote-injectable JS |
shouldSearchForTwoNamesInFacebook | false | tries first+last name |
regexForAddSocial | JSON blob of 5 regexes for facebookProfileUrlPattern, facebookDigitProfileIdPattern, instagramProfileUrlPattern, instagramImageProfileUrlPattern, instagramMatchesRegex | regexes that parse Facebook/Instagram profile markup — remote-config-overridable |
com_start_page | 1 | default tab (1 = FOR_YOU/Contacts) |
com_title_for_history / com_title_for_you | "history" / "contacts" | |
com_history_default_style | 7 (= LIST_MAIN_CARD_VIEW) | |
caller_id_mode_v2 | 3 | |
show_caller_id_before_data | true | show caller-ID UI before any server data arrives |
show_get_photo_on_contacts | true | |
display_facebook_link | true | |
whatsappProfileBase64Image | [] | remote-pushed WhatsApp profile pictures |
google_photos_link_loader | JSON containing a JS search_results/links_script and a big_url_script injected into a WebView | remote-controlled WebView JS; placeholders [MAX_LINKS], [WEBVIEW_TAG], [ITEM_ID] |
open_whatsapp_intents | JSON with 4 WhatsApp deep-link variants incl. S.jid=[cis]%40s.whatsapp.net | drives 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_url | JSON with a Google-Forms url and dismiss_url | |
call_contest_report_calls_url | https://apps.eyecon-app.com/call-contest/a/report-calls | |
ac_ads_test_internet_connection_url | https://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 blobs | analytics / ad-id resolution |
is_amplitude_enabled, is_amplitude_enabled | Amplitude (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.
| real | kind | note |
|---|---|---|
jc.a | final class | small holder |
jc.b | interface | jc.m implements it |
jc.c | synthetic impl of InterfaceC21910f, InterfaceC21911g | lifecycle glue |
jc.d | large class | base for dialog/fragment overlays |
jc.e | synthetic impl of InterfaceC21910f, InterfaceC17221e, InterfaceC20998d, OnApplyWindowInsetsListener, InterfaceC18495i | window-insets handling (status/nav bars) for the contact screens |
jc.g | impl of InterfaceC17260p1, InterfaceC17569c0, InterfaceC18573b | animation/transition contract |
jc.h, jc.i, jc.l, jc.m, jc.n, jc.o | small helpers (jc.m implements jc.b) | |
jc.j, jc.k | abstract 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]
| real | purpose |
|---|---|
b5.e | static 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.d | block/unblock + DB async ops (mo415j, mo308k, mo309m, mo310p); builds the snackbar strings R.string.unblock / R.string.blocked |
b5.f | photo-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.h | Handler.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.g | runnables (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-caseRunnable:case 0/defaultare Firebase-Messaging helpers (ExecutorC0221v1,FirebaseMessaging+PowerManager.WakeLock), butcase 1is the photo decrypt path (lines 101-217):FileInputStream-> growm7.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.
| real | purpose |
|---|---|
dh.p | abstract base; static final C18984l1 f39041a (Amplitude Logger) |
dh.a | Remote Config personalisation signal parser — keys rolloutId, parameterKey, parameterValue, variantId, templateVersion, singleton f38998a, implements InterfaceC19134c |
dh.b | personalisation value resolver (extends dh.p) |
dh.c, dh.h, dh.j, dh.n, dh.q | config/JSON/holder helpers (dh.h hard-codes Charset.forName("UTF-8")) |
dh.g | extends JSONObject |
dh.f | implements dh.l; static final C22174e f39015c = new C22174e(9) |
dh.i | implements 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.k | extends InputStream (auth-token stream) |
dh.m | Closeable with Logger |
dh.o | implements dh.d; Charset UTF-8 |
dh.d, dh.l | interfaces |
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) — theMenifaFragmentsheet, 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.bcarries(Serializable, Object, Object, Object, int)and is the quick-action / SMS-send completion handler).c6.a—BroadcastReceiverfor 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—OnTouchListenerwith-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.
| real | purpose |
|---|---|
h7.i | C16945i — 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.h | ThreadFactory — names threads "FLTask-" + <prefix> + "-" + n, daemon threads |
h7.f | m32764a() / m32765b() — the two global single-thread executors (the one r5.g/r5.k use for contact refresh) |
h7.c | ThreadPoolExecutor subclass with a PriorityBlockingQueue |
h7.b | FutureTask implements Comparable (priority wrapper) |
h7.j | extends Binder — local Binder stub |
h7.a | e2.b async callback base used by the whole app |
h7.d, h7.e | Kotlin coroutine Continuation (invokeSuspend) |
h7.k, h7.m, h7.n | AdListener adapters (inneractive / Mbridge / AdMob) |
h7.g, h7.l | runnables; 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
| real | purpose |
|---|---|
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.h | click listeners; q5.h builds layout_date_time_dialog, prefixes R.string.today + ", " |
q5.b | SMS-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.f | completion 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 gateif (eventType==6 || eventType==7) // missed-call variants && !remote("shouldShowWhatsappMissedCallReminder")) return;String delays = remote("missedCallReminderDelayInMinutes", false); // CSV of minutesif (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 withw6.C21273a.m38423g(...), usingrecvcli+message(CampaignEx.JSON_KEY_AD_Q="adq") forcantalk.jsp, andnotifiedcliforgetcantalkstatus.jsp.p5.be2.bcallback;p5.c,p5.drunnables (p5.dhas 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.bis ZXingEncodeHintType(TERMINATOR, NUMERIC{10,12,14}, ALPHANUMERIC{9,11,13}, STRUCTURED_APPEND, BYTE{8,16,16}, ECI, KANJI, FNC1_*, HANZI);rk.amerges 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.rgbmap building, andm36780r(String prefKey, boolean, e2.b)= a SharedPreferences boolean setter (used byr5.rcase 17 forSP_KEY_BLOCK_SPAM_CALLS).z6(5 files, Contacts) —z6.a=Note(implements Comparable; fieldsa,b,c; the per-phone-number note attached tor5.j.note);z6.e= the note dialog (R.layout.note_dialog_layout,R.layout.surveys_base_layout,R.string.notes, shares viaIntent.createChooser(…, R.string.note); empty-note guardR.string.note_is_empty);z6.c= its click listener;z6.d= interface;z6.f= singleton.t7(2 files) —t7.b extends d7.dis the Reverse Lookup fragment (super(R.layout.fragment_reverse_lookup));t7.aasync callback.d7(4 files) — base Fragment for the contact screens:d7.b (AbstractC15858b) extends Fragment implements i71.n0withstatic final HashSet f38727hand the fragment lifecycle contract (f0,i0,j0,k0,l0,m0,n0,o0,p0,q0);d7.d (AbstractC15860d)addsi,m0,o0,r0,s0;d7.c=OnBackPressedCallback;d7.a= a customRuntimeException.r6(2 files) —r6.a extends ItemTouchHelper.Callback(swipe-to-delete / drag-reorder for the contact lists) +r6.binterface.a8(7 files) —a8.choldsstatic final ViewModelStore f390a+ViewModelProvider.NewInstanceFactory f391b;a8.b,a8.e,a8.gare plainViewModels (state holders for the contact screens);a8.aholds threeC19290acomparators (ids 8,9,10);a8.d,a8.fsmall helpers.kq(2 files, Contacts) —kq.ainterface +kq.babstract base withstatic final LinkedList f45607a.s6(3 files) —s6.a extends o6.C16146a,s6.b,s6.cinterface. 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.t1default branch buildsnew ThreadPoolExecutor(0,1,30,SECONDS,new LinkedBlockingQueue(), new NamedThreadFactory("firebase-iid-executor")).am.t1case 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-likeio.reactivex.internalcontracts 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 RxJava2io.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
| # | Step | Code |
|---|---|---|
| 1 | Trigger 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. |
| 2 | Trigger 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] |
| 3 | Projection 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] |
| 4 | Query. 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 |
| 5 | Social 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] |
| 6 | Normalisation. 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. | |
| 7a | Mobile 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] |
| 7b | Non-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] | |
| 8 | Language 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] |
| 9 | Address-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] |
| 10 | Storage 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). |
| 11 | Write-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] |
| 12 | Retry. 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 |
| 13 | Table 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 separatorint 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:
contactsdelta rows are not allowed to overwrite the user’s local data — onlyis_viewer,os_type,is_sub,is_can_talk_enable,vnare copied down onto every phone row of the same person (is_viewer= “this device is the viewer”,vn= viewer’s version).socialdelta rows are authoritative: the server’ssocial_idreplaces the local one andsocial_sourceis set to 1 (from server).- Every number of that person that lacks a social row gets a synthetic social row with
social_score = 0,social_source = 1. - A
FACEBOOK(1)social match implicitly creates aFB_MESSENGER(4103)match as well. - Commit order:
DBContacts.m10099U(null,null,socialRows)->m10092D()reload ->DBContacts.m10093O(null,null,contactRows)->m10118v()reload ->m10135X("getUpdatedContactInfo_")->m10134W()->m10123E(). - Trigger [OBS]:
r5.kcase 8 — when prefSP_REQUIRED_SERVER_CONTACTS_REFRESHis true andf11746xis false, a forced refresh is scheduled on thew6.hServer executor (RunnableC21082e0(25, s5.e(2, db, boolean[]), boolean[])) and the pref is cleared on completion.
Part D — related in-cluster producers.
bm.RunnableC3686q0logs"Server", "addContactsList size = %s, addedList = %s"(the blob builder). [OUT]s5.gcase 0 /s5.kpush the per-contact name and photo fetch.r5.i0.m36652hon any social tap writessocial_source-worthy data viaC16945i.m32774f(DBContacts.f11708K, 0, bm.RunnableC3589a(db, contact, socialInfo, cli, 18)).r5.rcase 11 posts a signature blob{"signatures":[…],"cid":…,"random":…}, RSA-encrypted withR.string.pk_enc_2, tohttps://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 initString 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 prefsSecretKeySpec 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/PKCS5Paddingm7.d.f46765a.init(DECRYPT_MODE, k);m7.d.f46767c = ok; // "cipher enabled" flagTherefore: 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]
- Local name first.
r5.i.private_name(devicedisplay_name) is the default; if it is empty or equals the raw number,l7.b.m34437h().m34440a(private_name)is used, elseR.string.private_number.r5.i.m36643y()decides “is this a real device contact?” (contact_idempty or starts with"-1"=> unknown number). - T9 / unicode search keys.
r5.i.m36638t(char[][] keypad)memoises the T9 digit string ofprivate_name(space ->0);DB_COLUMN_UNICODE_SCORE/unicodeScoresupport non-Latin names. - Server name (caller ID). Out of cluster:
getnames.jsp(p599v7\RunnableC20908f:372-382). In-cluster the result lands onr5.i.private_namewithhasNameInServer = true(AbstractC0039a.f189U / f221garea) and onr5.i.shouldFetchName. - Social lookup table. Every social id is one row of the
socialtable (table id 12) with the columnscontact_id(a7.f210d),cli_in_server(f216f),social_type(f164K0=r5.h0.f51206bnumeric 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. - Ingest rules.
r5.i0.m36647c(ArrayList, SocialInfo)— rejectkind==32(Skype), reject when the app isn’t installed unless the type is in{FACEBOOK, INSTAGRAM, VKONTAKTE, TWITTER, LINKEDIN}, reject emptysocialID; merge duplicates by summingscoreand keeping the first non-emptysocialID. Device-sourced seeds:r5.e.m36595p(EMAIL, fromemail_v2data rows) andr5.e.m36599t(SKYPE, fromcom.skype.android.chat.actionwithdata5==3). - Server-sourced seeds:
r5.kcase 12 (§21.2 Part C) — authoritativesocial_id, and the implicit Facebook -> Messenger duplication. - Facebook-ID path (
r5.i0.m36651g+m36649e+m36654j, source quoted in §2.5). Remote gateisTestingFbProfileLinkEnableV2selects the new probe path; the old path callsm36654jdirectly. Probe support:w6.h.f56471g(aHashSetof already-tried profile names — a hit producesNOT_A_FB_PROFILE_ERROR) and theServerexecutorw6.h.f56472a.w6.h.m38436i(name, type, attributes)builds{"name":…,"type":…,"attributes":{…},"timestamp":<epoch-seconds>}and posts it (that is thenotifyaddcontact.jsppayload). AlsoshouldSearchForTwoNamesInFacebook(remote, §7.1) makes the search try first+last name, andfacebooke_Extract_URL_Regex = browser_native_sd_url":"([^"]+)"scrapes the resulting page. - Launch (all social types) —
r5.i0.m36652h(ContactInfo, SocialInfo, PhoneNumberInfo, Activity), a 38-case switch onsocialEnum.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_messagepinned tocom.viber.voip), WhatsApp, SMS, Facebook, LinkedIn, VK, Instagram, Email, Calendar, Navigation, Note, Share-location, Video call, Can-talk, Call reminder, Report spam, Add contact. - Naming helpers.
r5.h.m36613a(String)splits participant strings;r5.i.m36639u()builds the group display text fromr5.hparticipants, falling back to theircli, then toprivate_name.
21.5 Flow 5 — main screen, fragments, adapters, searchh5
[OBS]
MainActivity(wrapper) hostsMainFragment, which ownsf12024j(Map<Class, Fragment>of registeredq6.jchildren),f12025k(theViewPagerwhosegetCurrentItem()is the tab index read byq6.j.mo10082M()),f12027m(the searchEditText).- Tabs =
q6.w=HISTORY(0)/FOR_YOU(1)/FAVORITES(2).FOR_YOUandFAVORITESshareR.layout.fragment_for_you_and_favorites_layout;HISTORYusesR.layout.fragment_history_layout. Default start page = remotecom_start_page = 1. Default list style = remotecom_history_default_style = 7(LIST_MAIN_CARD_VIEW). - Fragment contract:
q6.j extends d7.b (Fragment) implements q6.d1, q6.r.d7.bsupplies the lifecycle hooks (f0,i0,j0,k0,l0,m0,n0,o0,p0,q0) plusstatic final HashSet f38727h. Tab-specific subclasses:HistoryFragment,ForYouFragment, and the favourites fragment (all thin wrappers incom.eyecon.global.MainScreen.Communication.*). - Adapters:
q6.vis the universal contact adapter (list cellR.layout.eye_contact_list_cell, grid cellR.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 byr5.l(67 dp / 10 dp). - Layout managers / decorations chosen by
q6.j.m36060r0from theq6.d0cell enum (1-column list / 2- / 3- / 6-column grids, withq6.e/g/i/z0span-size lookups giving ad and section-header rows full width, andq6.d/f/h/x0/y0decorations). - Diffing:
q6.v.m36079j(RecyclerView, ArrayList)compares old vs new, and when the sizes match issues per-positionnotifyItemChangedonly for the visible window (m36071a..m36072b), then re-anchors scroll. - Search:
q6.j.mo10006k0(Bundle)snapshotsMainFragment.f12027m.getText()intof50395i(trimmed,m31995E) andm36062w0()=f50395i.length() > 0.q6.j.mo10247x0(String, ArrayList)/mo10282Z(String)are the search entry points (query + result list).EyeSearchActivity(wrapper) hostsr5.b(the sync-account picker) andr5.a(the account model);r5.i0.m36653i/m36654jdo a name->Facebook search from there. - Long-press / selection:
r5.t.m36684d(link/merge contacts confirm),r5.n(selection state:Activity+HashSetof selectedContactInfo+AtomicIntegercounter + predicater5.m),r6.a(ItemTouchHelper.Callback— swipe / drag). - Cell actions:
r5.t.m36681abuilds the bottom sheet;u6.a/u6.urender it;c6.ois the menifa (SocialManager) sheet;c6.dregistersEYECON.INTENT_ACTION_REFRESH_DA_UIso 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 flagsnative_ad_dynamic_area,dynamic_area_url,reminderAdmob*,facebook_ads_reminder_*,cards_update_threshold. - Legacy pager:
g6.e extends PagerAdapterfor the notification-reader / reverse-lookup screens;q6.x extends FragmentStateAdapterfor the ViewPager2 version;c6.gpage-change callback.
21.6 Flow 6 — contact-reminder scheduling and notificationsh5
[OBS]
- Entry points.
- Automatic:
q5.g.m36035a(eventType, cli, name, src)is called from the post-call pipeline (the same family thatr5.rcase 9/10 hooks).eventType6/7 = missed call, 8 = un-answered outgoing. - Manual:
q5.g.m36037c(Activity, cli, name, "menifa")from theCALL_REMINDERentry in the menifa sheet (r5.i0.m36652h, ordinal 45);r5.t.m36681aalso offers “Schedule call or message” (r5.qcase 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.
- Automatic:
- Gating —
AbstractC19327e.m35661t()(master enable), then remote flagsshouldShowWhatsappMissedCallReminder,shouldShowUnAnswerCallReminder. - Delay selection — remote
missedCallReminderDelayInMinutesoroutgoingCallReminderDelayInMinutes, parsed as a comma-separated list; oneWorkManagerjob per entry, so a single missed call can arm several reminders. - Persistence — the
ReminderDatais appended to the SharedPreferences arraysp_call_reminder_list(plaintext JSON; keyscallTime,reminderTime,phoneNumber,name,socialName,isAnswered,amountOfShown,reminderMessageText,reminderType). On aJsonParseExceptionthe 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. - Scheduling —
OneTimeWorkRequest(ContactReminderWorker)withsetInitialDelay(minutes, MINUTES),Data\{callTime, type, reminderDelayTime, phoneNumber = l7.b.m34444e(cli), src\},addTag("CallReminderWorker" + phoneNumber + type). - UI —
q5.k(layoutdialog_contact_reminder): channel picker (call/sms/whatsapp/noteReminderEyeIconButtons),ETContentSmsfor the pre-filled reply SMS (FLReplyMsgContainer,LLTimeOption), and four preset delay buttonseyeButtonClock1..4plusEBSetTimeV->R.layout.layout_date_time_dialog(date prefixed withR.string.today + ", "). Title ="Schedule " + <type label>; toastsR.string.cant_set_reminder/R.string.save_reminder. - Worker —
com.eyecon.global.ContactReminder.ContactReminderWorker(thin wrapper; the realdoWorkis outside C4). It reads theDatakeys above and is expected to show the reminder notification / fire the channel action. [INF] on the notification content, becauseContactReminderWorker’s body is not in this cluster. - 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. - Permission UX —
q5.b(notification permission rationale insideContactReminderActivity:R.string.permissions_needed/permission_app_setting_message/go_to_settings) andq5.d(SMS permission pre-prompt).
22. SharedPreferences keys observed in cluster C4 [OBS]h4
| key | type | written by / meaning |
|---|---|---|
SP_KEY_TOP_CONTACTS_LANGUAGE | String | r5.e.m36603x — detected ISO-639-1 language of the address book |
SP_KEY_AMOUNT_OF_CONTACTS | int | r5.e.m36603x — count of imported contacts |
SP_EYECON_DEF_ACCOUNT_V2 | String | r5.e.m36603x — user-selected default sync account |
sp_social_installed_apps_v2 | String (JSON array) | r5.i0.m36645a — cached set of installed social-app numeric ids |
permission_status_json | String (JSON) | j7.m.m33637c — full map of granted/denied permissions incl. DIALER and CALL_SCREENING roles |
cipher_key | String | the Blowfish key component, nextInt(999), stored in PLAINTEXT (p379k5 case 10) |
sp_call_reminder_list | String (JSON array) | q5.g.m36036b — pending reminders incl. phoneNumber + name |
SP_KEY_EYECON2_REMINDERS | String (JSON array) | r5.k case 20 |
SP_REQUIRED_SERVER_CONTACTS_REFRESH | boolean | r5.k case 8 — forces a server contact refresh |
SP_KEY_IS_GIVE_PRE_EYECON_USAGE | boolean | r5.k case 8 — one-shot flag for pre_eyecon_usage distribution |
SP_KEY_DID_UPDATE_V1453 | boolean | r5.r case 11 — anti-fraud signature already sent |
SP_KEY_BLOCK_SPAM_CALLS / SPAM_CALL | boolean | r5.r cases 16/17, rk.a.m36780r |
SP_KEY_RECORD_CALLS_MODE | int | r5.r case 10 |
SP_KEY_PRE_EYECON_AMOUNT_OF_PHOTOS | int | b5.h — photo-acquisition telemetry baseline |
AMOUNT_OF_UI_CONTACTS | int | b5.f — fallback contact count for telemetry |
SP_KEY_FAKE_AUTHENTICATED_CLI | String | used by the “fake authenticated CLI” test path |
SP_KEY_FID__LIST__FOR_NOT_SHOW_DIALOG | String | suppression list for permission dialogs |
SP_KEY_REG_PERMISSIONS_SUMMERY | String | registration permission summary |
SP_KEY_DONT_ASK_ME_AGAIN_TRACKER | String | do-not-ask-again tracker |
SP_MAIN_VIEW_SORTING_MODE | int | main list sort mode |
SP_NEW_FACEBOOK_SOCIAL_CONTACT_ID | String | new-FB-social path flag |
SP_KEY_LAST_SPAM_SYNC, SP_KEY_LAST_TIME_BACKUP, SP_KEY_LAST_TIME_REQUESTED_CARDS | long | |
SP_KEY_JOIN_VERSION, SP_KEY_JOIN_VERSION_CODE, SP_KEY_IS_REJOIN | registration join telemetry | |
SP_KEY_VALIDATION_WITH | String | b5.f — e.g. "Phone number" |
SP_KEY_NOT_REGISTERED_WITH_REAL_CLI_V2 | boolean | registration state |
SP_KEY_MY_VIRAL_ID, SP_KEY_FID__LIST__… | referral / suppress-dialog data | |
SP_KEY_CONTACT_QUICK_GUIDE_JSON_V19 | String | quick-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_FAILED | permission/role-request UX bookkeeping | |
SP_ALLOWED_TO_START_GIVING_PICS_BY_FORCE, SP_INIT_FRESP_PICS_COUNTER | photo 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_v2 | misc UI/feature flags | |
imeis_updated | String | p6.j — IMEI already pushed |
usernameserver | String | r5.r case 12 — last username pushed to updateuserdetails.jsp |
SP_IS_SMS_WINDOW_ENABLE_BY_USER | boolean | p6.j case 13 — SMS-window content-observer opt-in |
SP_IS_OPEN_MENU_E2 | boolean | menu A/B |
analytics_worker_reload_json, singular_events_json, default_recording_mode_json | String (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 withuser_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 byr5.kcase 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>andGET /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.rcase 12 /p6.j];POST /grps/sgnwith an RSA-encrypted{signatures, cid, random}attestation [OBS,r5.rcase 11]. [INF] On the consent question: the app does gate the upload behindAbstractC9039h.m10665v()(“is a validated Eyecon user”) and the reminder flows behindAbstractC19327e.m35661t(), andj7.mrecords 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.gobserver ->r5.eread ->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.
F7 — MEDIUM · Cleartext http:// social deep linksh5
[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.
F10 · LOW-MEDIUM · Consent-prompt UX is remotely steerableh5
[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.ereads the address book on every ContactsProvider change (r5.gobserver) — no visible debounce in C4; the dedupHashSetpercontact_idlimits write amplification.r5.e.m36603xappliesLIMIT 100only when both flags are set — in the general case the full address book is walked on every refresh.q6.v.m36079jdiffs but still callsnotifyItemChangedper visible row.q5.g.m36036bsilently drops every pending reminder if the JSON array is corrupt.- The
r5.jCANTALK_TYPE_*/canTalkStatusflow 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 claim | Real location | Status |
|---|---|---|
w6.h.m38428a builds the addcontacts.jsp payload | p620w6\C21280h.java:112-179 | Not in C4. Claim verified correct out-of-cluster (see §21.2 Part A). |
@z@ record format for addcontacts.jsp | p620w6\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:143 | Not in C4. Verified correct out-of-cluster. |
getcontactslang.jsp returns a 2-letter ISO code | same file, .matches("[a-zA-Z]{2}") | Not in C4. Verified. |
Blowfish/ECB key "Ewu" + nextInt + "sTQHGK" | p379k5\RunnableC17750b.java:401-414 | Not in C4 (key derivation) — but the decrypt sink is in C4 (am\RunnableC0215t1). Verified; keyspace is 999 not 1000. |
a7.AbstractC0039a column constants | fp008a7\AbstractC0039a.java | Referenced from C4, fully transcribed in §0.2. |
contactdb 48 columns / v57 migrations | the 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-339 | Not in C4. Verified verbatim correct. |
getfreshpicsjson.jsp / getoptionalpics.jsp / picapproval.jsp | androidx\fragment\app\RunnableC2598c.java, p607vk\RunnableC21082e0.java:242, p572u4\RunnableC20597o.java:147 | Not in C4. |
getnames.jsp | p599v7\RunnableC20908f.java:372 | Not in C4. |
notifyaddcontact.jsp | as\RunnableC3316o.java:111,116 | Not in C4. |
getupdatedcontactsinfo.jsp / resetupdatedstatus.jsp | p607vk\RunnableC21093i.java:140, p607vk\RunnableC21082e0.java:368 | Not 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 apps | manifest | Not 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_v2 | Partially confirmed in C4 (the enum + the pref); the manifest block itself is out of scope. |
Erratum to §20.2 (
ampackage attribution). On a second pass I checked the imports of theamclasses rather than assuming a library.amis not a coherent third-party namespace: it is a mixed R8 class-merge bucket. Confirmed contents: Firebase-Messaging helpers (am.t1Firebase 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 owne2.basync-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 thatamcontains no contact/reminder logic other thanam.t1case 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 inam.t1itself remains verified.
Erratum to §19 (
rk.c).rk.c’s 40-element static tablenew C19951c(n, C9049r(...), ...)is Facebook Audience Network / Meta placements (C9049r,C17558l, packagecom.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
| item | state |
|---|---|
| 27 / 27 packages listed in the TSV | 100 % 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 from | Yes — §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
pNNNxxxmaps to real packageNNNvia the/* JADX INFO: renamed from: ... */header on each file.
0. Package inventory and first-party / third-party classificationh4
| # | Disk dir | Real pkg | Files | KB | Briefing module | VERDICT | Responsibility (one line) |
|---|---|---|---|---|---|---|---|
| 1 | p703z7 | z7 | 51 | 286 | Toki | FIRST-PARTY MICO | Toki (walkie-talkie) UI Fragment + contact-chooser + adapters + ring/sound assets |
| 2 | p282f5 | f5 | 27 | 194 | Backup | FIRST-PARTY MICO | Google-Drive backup/restore of contactdb |
| 3 | p598v6 | v6 | 22 | 137 | MoreMenuAndSettings | FIRST-PARTY MICO | ”More menu” / Settings entry hub, deep-link router |
| 4 | p432n7 | n7 | 27 | 125 | MoreMenuAndSettings | FIRST-PARTY MICO - but it is the Photos / PhotoPicker module, not a Google-Account sync adapter | PhotoPicker source-chooser, crop, persist, Facebook + Google-Images photo harvesting |
| 5 | p572u4 | u4 | 31 | 120 | AdsIB | THIRD-PARTY (Fyber FairBid) | FairBid interstitial/rewarded mediation layer |
| 6 | p526ri | ri | 22 | 119 | MoreMenuAndSettings | THIRD-PARTY (Google Firebase Cloud Messaging internals) | FCM EnhancedIntentService, topic subscribe, ProcessIntentHelper, GCM task prefs. NOT first-party. See 8.1 |
| 7 | p720zq | zq | 15 | 91 | Permissions | MIXED — zq.g FIRST-PARTY PermissionsHelper; zq.a…zq.o are THIRD-PARTY OkHttp 4 platform/TLS-socket-alias classes | Permissions helper + OkHttp TLS platform |
| 8 | p477p8 | p8 | 6 | 65 | MoreMenuAndSettings | THIRD-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 |
| 9 | p566ts | ts | 7 | 40 | MoreMenuAndSettings | THIRD-PARTY (Moloco tech.crackle) | Crackle rewarded-video ad + OMID viewability |
| 10 | p249dn | dn | 13 | 34 | Ads | THIRD-PARTY (Moloco tech.crackle VAST/advert-session layer) | Ad event + pricing waterfall glue for Moloco |
| 11 | p462ol | ol | 2 | 15 | Photos | THIRD-PARTY (Glide) | Glide Bitmap downsample+EXIF-rotate AsyncTask. NOT photos code. See 4 |
| 12 | p010a9 | a9 | 1 | 15 | MoreMenuAndSettings | MIXED / JADX artefact — one giant Runnable merging ~9 unrelated original classes | Settings-screen WebView + Fyber FairBid banner request |
| 13 | p367jh | jh | 4 | 8 | Menifa | FIRST-PARTY MICO — but not Menifa; it is the “Can You Talk” (SMS/WhatsApp) delivery-status feature | CanTalk status widget + VOIP-ish payload holder |
| 14 | p007a6 | a6 | 1 | 1 | DynamicArea | FIRST-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 -
z751,f527,v622,n727,jh4,a61,zq.g1 - THIRD-PARTY SDK: ~95 files -
ri22 (Firebase Messaging),u431 (Fyber FairBid),ts7 +dn13 (Moloco/Crackle),ol2 (Glide),p86 (Facebook/OkHttp/RxJava),zq14 (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: theeyecon://idPlussettings-consent deep link)
Notable briefing correctionsh5
p462olis NOT the Photos module. It is Glide’s image downsampler (BitmapFactoryinSampleSizedoubling + EXIF orientation parse viapl.C19372b+OkHttpfetch viap407ll.C18399k). Mislabelled in the TSV.p367jhis 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: noMenifastring in any of the 229 files — see §13).p572u4is NOT first-party ad mediation; it is verbatim Fyber FairBid SDK code (com.fyber.fairbid.g00,FairBidAdRequest, waterfall types).p566tsandp249dnare Moloco, not first-party.- Toki does NOT stream the microphone continuously. Proof in §2.
f5backup: “the ENTIRE contactdb file is uploaded” is CORRECT but needs qualification — see §3.- The briefing omits
com.eyecon.global.Photos.PhotosTrackerWorkerentirely. 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 instrings.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 invisibleJS_ENABLEDWebView 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.comis 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-cis NOT a cryptographic challenge.C21273a.m38421E()merely sums the numeric values of the first characters of the--separatede-authsegments. 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.
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) holdsf60181e(aLifecycleOwner, base of the ViewPager2FragmentStateAdapterchain) and wiresInterfaceC21859n(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.comand 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 file | Real class | Kind | Purpose |
|---|---|---|---|
C22122v0.java (128 KB) | z7.v0 | Fragment + wl.InterfaceC21451n impl | The Toki call Fragment. Owns UI, state machine, session, PTT button |
C22107o.java | z7.o | RecyclerView adapter-ish | Participant row binder (m39366q(C22113r)) |
C22101l.java | z7.l | Adapter | Grid adapter (D/E renamed members) |
ViewOnTouchListenerC22089f.java | z7.f | View.OnTouchListener | Swipe-up/down detector (FlingAnimation, SpringAnimation, SpringForce) |
C22111q.java | z7.q | Dialog | Session-error / press_and_hold bubble (m4117o0, m4130r0, m4132t0, m4133u0, m4115m0, m4130r0) |
C22125x.java | z7.x | Listener | — |
RunnableC22092g0.java | z7.g0 | Runnable | RECORD_AUDIO never_ask_again_mode gate |
C22109p.java | z7.p | Model | Remote Toki invitation state (f60360l, f60353e) |
ViewOnClickListenerC22095i.java | z7.i | Click listener | — |
C22081b.java | z7.b | FloatPropertyCompat | Animated property helper |
C22103m.java, C22104m0.java | z7.m, z7.m0 | Handler callbacks | — |
C22086d0.java, C22128y0.java | z7.d0, z7.y0 | Internal views | — |
C22090f0.java | z7.f0 | View holder | Participant cell |
C22093h.java, RunnableC22094h0.java | z7.h, z7.h0 | Listener / Runnable | Retry buttons |
ViewOnClickListenerC22098j0.java | z7.j0 | Click listener | — |
C22096i0.java | z7.i0 | View holder | — |
C22091g.java, RunnableC22079a.java | z7.g, z7.a | Holder / Runnable | — |
ViewOnClickListenerC22126x0.java | z7.x0 | Click listener | Opens https://docs.google.com/forms/d/17QjnHJanVGPt0V_AkVj_baVmE2xZZGOyjAKjB22oeOU/viewform?edit_requested=true (user-feedback Google Form, hardcoded) |
C22099k.java, ViewOnClickListenerC22100k0.java | z7.k, z7.k0 | — | — |
C22110p0.java | z7.p0 | BroadcastReceiver | com.eyecon.global.isonear (near-peer/live-cluster broadcast) |
ViewOnClickListenerC22117t.java | z7.t | Click listener | — |
C22113r.java | z7.r | Model | Per-participant row model: f60377c name, f60378d cli, f60379e number, f60382h bitmap, f60383i adapter, f60384j status enum |
RunnableC22114r0.java | z7.r0 | Runnable | — |
C22115s.java | z7.s | Registry | f60387b.f60388a = Map<…, z7.v0> of live Toki fragments |
EnumC22118t0.java | z7.t0 | Enum | Talk-status per participant |
C22119u.java, EnumC22120u0.java | z7.u, z7.u0 | Enum + factory | Sound-effect / talk modes (.REJECT, .CLICK_TALK, m39377a(), m39378b(float)) |
C22121v.java | z7.v | — | — |
C22112q0.java | z7.q0 | Callback | CHANNEL_JOIN callback |
C22127y.java, C22116s0.java | z7.y, z7.s0 | AudioFocus listener | z7.s0 = AudioManager.OnAudioFocusChangeListener |
C22083c/85d/87e.java | z7.c/d/e | DynamicAnimation.OnAnimationEndListener | Animation completion |
ViewOnClickListenerC22082b0 / C22084c0 | z7.b0, z7.c0 | Click listeners | Both also open the same hardcoded Google Form feedback URL |
ViewOnTouchListenerC22102l0.java | z7.l0 | Touch listener | — |
AbstractC22088e0.java | z7.e0 | Builder | AudioFocusRequest.Builder (API 26+), m39345a() |
C22080a0.java | z7.a0 | Model | Contacts-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):
- The single
AudioRecordin the whole APK isp599v7/RunnableC20908f.java:508(v7.fswitchcase 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;case 11is posted from exactly two call sites, both insidez7.v0.m39404R0()(the press-talk handler):p703z7/C22122v0.java:1170→runnableC21441d.m38602b(new RunnableC20908f(11, runnableC21441d, c21450m));followed byTALK_STARTp703z7/C22122v0.java:1186→ same, followed byCHANNEL_JOIN_START_TALK
m39404R0()is thez7.fOnTouchListenerACTION_DOWN /CLICK_TALKpath. 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”).- Stop path (OBSERVED,
p633wl/RunnableC21440c.java=wl.c):case 0→m38601a()(stop + release) thenf57144f = false(kills the thread)case 1→f57148j = true;then flushes the bufferedDatagramPacketlist — this is “answer started, send what I said while the invite was ringing”.
wl.d.m38601a()callsaudioRecord.stop(), flushes queued packets only iff57148jwas never set to true (i.e. never released → discard), thenaudioRecord.release(),f57145g = false,Process.setThreadPriority(0),System.out.println("Recorder Stopped").- App-wide search for
new AudioRecordreturns exactly one hit;MediaRecorderreturns zero hits in first-party code;AudioRecordis also guarded byz7.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 buffershort[] sArr = new short[2400]; // 2400 samples @ 8 kHz = 300 msint iEncode3 = AmrEncoder.encode3(this.f57149k /* mode = 7 */, sArr, 0, this.f57147i.read(sArr, 0, 2400), bArr, 16); // payload starts at offset 16bArr[16] = (byte) (bArr[16] | 2); // flag "AMR" on the frameDatagramPacket datagramPacket = new DatagramPacket(bArr, iEncode3 + 16);if (this.f57148j) { this.f57146h.m38611f(datagramPacket); } // live → send nowelse { this.f57143e.add(datagramPacket); } // pre-answer → bufferio.kvh.media.amr.AmrEncoder(kvhk AMR port) — mode index 7 ⇒ AMR-NB frame;AmrEncoder.init(0)= single-frame encode API.f57149kis assigned7at the top ofrun()and never changed (OBSERVED) — a single fixed AMR mode.- Buffer ring:
Runnable[1000]command ring +ArrayListDatagramPacket 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 iddata[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 transportDatagramSocket sock = k.f59289b;DatagramPacket parity = k.f59306s;byte[] data2 = datagramPacket.getData();data2[1] = 1; // PKT TYPE = AUDIOk.f59285A++; AbstractC19124b.m35480z(data2, 2, k.f59285A); // 4-byte seq @ offset 2k.f59312y++; AbstractC19124b.m35480z(data2, 7, k.f59312y); // 4-byte control-seq @ offset 7data2[6] = 1; // flags bytedatagramPacket.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):
| Offset | Size | Meaning |
|---|---|---|
| 0 | 1 | (unused/0 in all observed constructions) |
| 1 | 1 | packet type: 0=CONTROL, 1=AUDIO, 2=XOR-FEC-parity |
| 2 | 4 | packet_seq (int, AbstractC19124b.m35480z/m35471q) |
| 6 | 1 | flags: 0x80=SYN, 0x08=MSG/keepalive-class, 0x40=carries NACK list |
| 7 | 4 | control_seq / media timestamp (for audio: the Toki session id) |
| 11 | 4 | NACK / lost-packet seq list (present iff flags & 0x40) |
| 15 | 1 | 0 for audio (differentiates from a 15-byte CONTROL packet) |
| 16.. | n | AMR 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"); // :1994Port: 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" rangesreturn ((Integer) arrayList.get(new Random().nextInt(arrayList.size()))).intValue();} catch (Exception e) { … return 5620; } // hard defaultSecurity-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:
z7.v0.m39382O0(...)(push-notification entry) readsSharedPreferencesC16497y.m32082b(null, "authenticatedCli")— pref keyauthenticatedCli(C22122v0.java<508>508>). If present it builds a session:
int iM39737C = AbstractC22312g.m39737C(); // UDP port from remote cfgC21450m c21450m = new C21450m(new C22112q0(c21450mArr, z10, zM34703c, i3, i10), MyApplication.f12163h, C18312b.m34437h().m34444e(str3)); // authenticatedCli -> clic21450m.m38608b(str /* server host */, iM39737C); // -> InetAddress.getByName -> TalkiServerwl.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();- The
e-authcredential 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 JsonObjecto.m14611q("cli", cli);o.m14611q("vn", "4.2026.04.23.1145"); // hardcoded app versiono.m14611q("vc", String.valueOf(749)); // hardcoded VOIP/protocol version 749o.m14611q("e-auth-v", "e1"); // auth scheme versiono.m14611q("e-auth", e_auth); // <-- THE CREDENTIALo.m14611q("e-auth-c", e_auth_c); // challenge/nonce answerCORRECTED (see 21.1 and 22.2).
p620w6/C21273a.m38421E()was later read in full: splite-authon ”-”, sumCharacter.getNumericValue(first char)of each segment, return the decimal string. Soe-auth-cis NOT a signature or challenge response - it is a trivially derivable checksum ofe-auththat any passive observer of a single frame can recompute, and it provides zero additional authentication.e-authitself 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 everyCHANNEL_*JSON frame over the same signalling channel.
Signalling verbs sent by wl.m (all as Gson objects, C3349u2.m3503v(type, json, callback, runnable)):
| Verb | Payload keys | Method |
|---|---|---|
CHANNEL_JOIN | cli, vn, vc, e-auth-v, e-auth, e-auth-c, channel_id, invitation_id | m38609d(int channelId,int invitationId,String auth,String authC) |
CHANNEL_JOIN_START_TALK | same + channel_id, invitation_id | z7.v0.m39404R0() line 1190 |
CHANNEL_INVITE | same + invited_clis (array) | m38607a(C11856m invited,String auth,String authC) |
TALK_START | channel_id, session_id | z7.v0 line 1174 |
TALK_DONE | channel_id, session_id | m38612g() |
CHANNEL_BROADCAST_COMMAND | sound_fx_id, cli, channel_id, session_id, type="playSoundFX" | m38613h(int) |
SESSION_STATUS | channel_id, cli, session_id, status | m38614i(int) |
INVITE_REJECT | channel_id, invitation_id, cli, type | m38610e(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
| Sev | Finding | Evidence |
|---|---|---|
| HIGH | Microphone 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 |
| MEDIUM | Toki 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 |
| MEDIUM | e-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 |
| MEDIUM | Destination 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 |
| LOW | Hardcoded Google-Form feedback URL in 3 click handlers. | ViewOnClickListenerC22082b0:39, ViewOnClickListenerC22084c0:45, ViewOnClickListenerC22126x0:51 |
| LOW | System.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 file | Real class | Kind | Purpose |
|---|---|---|---|
AbstractC16422f.java | f5.f | abstract base | Backup/restore session base. Owns the six Drive object names, Drive handler, thread slots, progress |
C16434r.java | f5.r | f5.f subclass | RESTORE (backup reader). Lists appDataFolder, downloads, merges into a temp DB, then swaps |
C16442z.java | f5.z | f5.f subclass | UPLOAD (backup writer). Creates folders, zips/copies/uploads the five payloads |
C16418b0.java | f5.b0 | final class (DriveHandler) | Google Drive REST wrapper (Drive, Drive.Files, SSLContext, ZIP unzip) |
C16426j.java | f5.j | Fragment | ”Upload to Drive” screen |
C16437u.java | f5.u | Fragment | ”Restore from Drive” screen; static f40753C = active f5.z upload handle |
RunnableC16439w.java | f5.w | Runnable | Upload worker, 6 cases (0=orchestrator, 1=TTM zip, 2=recordings, 3=address book, 4=database, 5=call log) |
RunnableC16429m.java | f5.m | Runnable | Restore worker, 6 cases |
RunnableC16431o.java | f5.o | Runnable | Download one call-record file (m31908c) + progress PROGRESS_RECORDS |
RunnableC16432p.java | f5.p | Runnable | Download address_book / call_logs |
RunnableC16433q.java | f5.q | Runnable | Diff-and-upload single file (skips if a file with the same name already exists remotely) |
RunnableC16438v.java | f5.v | Runnable | pending_temp_<name> → rename, and fail_safe_<name> fallback / cleanup |
RunnableC16441y.java | f5.y | Runnable | Upload one AMR recording into eyecon_backup_for_calls_records |
RunnableC16423g.java | f5.g | Runnable | ACTION_RESTART_AFTER_RECOVERY broadcast after restore |
RunnableC16428l.java | f5.l | Runnable | Looper.quitSafely() |
C16416a0.java | f5.a0 | Thread | Cancellable worker thread (f40660a = cancel flag) |
C16417b.java | f5.b | Handler.Callback | Fans cancel to all 5 worker threads; what==2 → start work after 5 what==2 messages |
C16419c.java | f5.c | Exception | code 1 = NO_BACKUP, 2 = MISSING_FILES |
C16420d.java | f5.d | Exception | Network-to-Eyecon error (opaque) |
C16427k.java | f5.k | InterfaceC20992b | Drive ProgressRequestInitializer adapter → f5.e.mo267g(int) |
InterfaceC16421e.java | f5.e | interface | void mo267g(int) progress callback |
ViewOnClickListenerC16424h/i.java | f5.h, f5.i | click listeners | signOut / clear SP_KEY_STARTING_RESTORE_PROCESS / m31923u0() |
ViewOnClickListenerC16435s/t.java | f5.s, f5.t | click listeners | signOut / m31944t0() |
C16430n.java (15 KB) | f5.n | JADX artefact | Merged ~15 unrelated library classes (Vungle, BottomSheet, Billing, f5 progress callback is only the tail) |
C16440x.java (34 KB) | f5.x | JADX artefact | Merged: 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 name | Case | Source | Sink | MIME |
|---|---|---|---|---|
BU_ttm_thread | 1 | gf.AbstractC16733c.m32601u() (TTM dir) → AbstractC0060k0.m331u(...) ZIP | <filesDir>/ttm.zip | application/zip |
BU_records_thread | 2 | e5.AbstractC16098a.m31460c(0) = Documents/Eyecon/RecordedNotes, m31460c(1) = Documents/Eyecon/RecordedCalls (MediaStore) | one Drive object per AMR file | audio/amr |
BU_address_book_thread | 3 | af.C0125a.m559k(...) (getAddressBookContacts) | <filesDir>/address_book | application/json |
BU_database_thread | 4 | MyApplication.f12163h.getDatabasePath("contactdb") → AbstractC0085x.m456g0(dbPath, <filesDir>/database, progress) | <filesDir>/database | application/x-sqlite3 |
BU_call_log_thread | 5 | gf.AbstractC16733c.m32593k(...) → CallLog.Calls query | <filesDir>/call_logs | application/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):
- The database payload is produced by
p008a7/AbstractC0085x.java:183— real namea7.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 keyCipher 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 pubkeyCipher 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_encpublic key is pinned instrings.xml— confirmed.
3.3 UPLOAD (OBSERVED)h5
f5.z.m31962h(f5.z) (static h):
- Create/resolve the six folders:
C16418b0.m31906a("pending_temp_eyecon_backup_for_<X>")for each of the six names.m31906acreates a Drive folder withparents=["appDataFolder"],mimeType="application/vnd.google-apps.folder", and returns itsid. - Progress 1, then 6 parallel
RunnableC16438v(..., case 1)tasks: delete stalepending_temp_<name>andfail_safe_<name>objects. - Progress 2, then 6
case 0tasks: if the canonical object is missing butfail_safe_<name>exists, rename the fail-safe back (m31910e). - Progress 3, then start the 5 worker threads (
BU_*). f5.z.mo31914b()at 95 %: renamepending_temp_<name>→ canonicaleyecon_backup_for_<name>for all six (parallelRunnableC3589a(…, 9)).- Delete local staging files
f40677j/f40679l/f40678k/f40680m(database / call_logs / address_book / ttm.zip). - 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-mailC21280h.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):
f40747F = getDatabasePath("contactdb");f40677j = new File(<db parent>, "eyecon_restore_temp_database").- Local dirs prepared:
<filesDir>/notes(wiped),<filesDir>/calls(wiped),<filesDir>/call_logs,<filesDir>/address_book. - List
appDataFolder(setSpaces("appDataFolder").setFields2("nextPageToken, files(id, name, size, parents)"), paginated). - If empty →
throw new C16419c("Selected account has no files", 1)⇒ UI stringbackup_restore_no_backup_v2, reasonNO_BACKUP, bucket4. - 5 ×
RunnableC3589a(…, 8)→m31934h(r, name)resolvesfail_safe_<name>; if absent ⇒C16419c(…, 2)⇒backup_restore_missing_files, reasonMISSING_FILES, bucket4. - Resolve three subfolders:
m31937k("database","eyecon_backup_for_database"),("address_book", …),("call_logs", …). - Start 5 threads:
BR_ttm_thread,BR_records_thread,BR_address_book_thread,BR_call_log_thread,BR_database_thread. f5.r.mo31914b()at 95 %: openseyecon_restore_temp_databaseas a DB and merges row-by-row into the live DB viaDBContacts.m10108g(...), matching onAbstractC0039a.f213e/f212d1/f216f(number / name / lookup key) and bump-revisioning viaDBContacts.m10109h(...) + 1.0; recency/timestamp fieldsf154H,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 copyFile file2 = new File(databasePath.getParentFile(), "eyecon_temp_db"); // current DBboolean 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:
- The complete
contactdbSQLite file — every contact: names, numbers, e-mail, physical addresses, birthdays, notes, caller-ID/spam tags, revision history. - The full call log — every number called/received, direction (
type), epochdate,duration,countryiso,geocoded_location,subscription_component_name/subscription_id(SIM),data_usage,new. - Every recorded call (
RecordedCalls) and voice note (RecordedNotes) as raw.amraudio uploaded individually. - All TalkiToMe (IVR) recordings as a
ttm.zip. - The full address book as JSON.
- The Google account e-mail address (
"account"key inbackup_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
| Sev | Finding | Evidence |
|---|---|---|
| HIGH | Backup 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 |
| HIGH | Documents/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 |
| MEDIUM | The 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 |
| MEDIUM | Two-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 |
| MEDIUM | Restore 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 |
| LOW | Google 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 file | Real class | Kind | Purpose |
|---|---|---|---|
C19143a.java | ol.a | immutable result | (Bitmap bitmap, nl.C18870a orientation) or (Exception). Mirrors Glide Downsampler.DecodeResult |
AsyncTaskC19144b.java | ol.b | AsyncTask | Glide 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); }→ GlideBitmapLoadCallback.nl.C18870a=nl.a={int orientation; int rotationDegrees; boolean flipHorizontally;}→ GlideOrientation.- EXIF parsed by
pl.C19372b(InputStream).m35701a()→ returnsOrientationconstants 2/3/4/5/6/7/8; maps to 180/90/270 rotations — GlideExifInterface. - Http client:
p407ll.C18399k.f46325blazyObjectinitialised top466oq.C19227c0(OkHttpOkHttpClient) — Glide’sHttpUrlFetcher/Internalholder. Requests wrapped incom.google.firebase.perf.network.FirebasePerfOkHttpClientandandroidx.webkit.ProxyConfig.MATCH_HTTPused as the “http” scheme constant. - 100 MB pixel budget:
if (bitmap.getByteCount() > 104857600) { options.inSampleSize *= 2; }— GlideDownsampler.DOWNLOAD_BUFFER_SIZE/MAX_SIZEbehaviour. - 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 file | Real class | Kind | Purpose |
|---|---|---|---|
C17626a.java | jh.a | immutable | 3 booleans. m33860a() returns (c || b) && a → AND-of-OR gating for a CYT eligibility decision |
C17627b.java | jh.b | immutable 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.java | jh.c | interface | C17627b mo33861n(C21851f request, JSONObject response) |
C17629d.java | jh.d | widget controller | The “Can you talk” bubble UI: status text + live timer |
jh.d details (OBSERVED):
- Constructor
(View, p516r5.C19862i contact, p034b7.AbstractActivityC3498e activity), bindsR.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.WHATSAPPifm36616c()elseEnumC19861h0.SMS; - target =
C19862i.m36633o()(preferred) elsem36631m(); - 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).
- picks
m33863b(int status)— delivery status → UI text, with local wall-clock timer rendered asHH:mmfromCalendar.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.bcarries twodoublefields (f44553d,f44554d) that are populated alongside a contact and a timestamp. Fromjh.dalone 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 file | Real class | Kind | Purpose |
|---|---|---|---|
C18716e.java (36 KB) | n7.e | p262e7.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.java | n7.w | C3878j (DialogFragment) | Source-chooser sheet: new C3877i(R.string.Camera, R.drawable.ic_camera_colored, …) + R.string.gallery + R.string.facebook |
C18730s.java | n7.s | RecyclerView.Adapter | Photo grid; m35060b() returns a WeakReference<PhotoPickerActivity> |
RunnableC18728q.java | n7.q | Runnable | Crop/transform result → EyeAvatar.setPhotoAndRescaleWhenNeeded(bitmap) |
ViewOnClickListenerC18729r.java | n7.r | RecyclerView.ViewHolder + OnClickListener | Single photo cell (R.id.EA_photo), pager setCurrentItem, m10424I0(1) |
RunnableC18718g.java | n7.g | Runnable | Applies a selected list (f12467v0) into PhotoPickerActivity.f12438Q0 |
RunnableC18720i.java | n7.i | Runnable | Writes the postal address of every chosen contact into the system Contacts provider |
C18714c.java | n7.c | ml.a (Glide callback) | mo34915a(bitmap, orientation, src, dst) → persist + notify |
C18730s/RunnableC18713b.java | n7.b | Runnable | intent.setData(Uri.fromParts("package", pkg, null)); startActivityForResult(intent, 123) → takePictureFromCamera(null) |
RunnableC18715d.java | n7.d | Runnable | Permission gate: AbstractC22312g.m39774g0(activity, strArr) then CAMERA check → takePictureFromCamera(null) |
C18711a.java | n7.a | AbstractC3872d (DialogFragment) | 5-second auto-dismiss confirmation dialog, R.layout.confirmation_dialog, Lottie LAV_anim max frame 45 |
C18721j.java | n7.j | RecyclerView.ItemDecoration | Grid spacing |
ViewOnTouchListenerC18724m.java | n7.m | OnTouchListener | Zoom rect getGlobalVisibleRect |
C18722k.java | n7.k | androidx.lifecycle.Observer | LiveData of fetched photo bitmaps (p009a8.C0089a.f385a) → RunnableC3380e(…, 3) |
RunnableC18726o.java | n7.o | Runnable | Finish + m10416C0(activity, strArr) + adapter refresh |
C18725n.java | n7.n | ViewPager.OnPageChangeListener | Page tracking |
C18719h.java | n7.h | FileFilter | file.getName().startsWith("survey_activity_temp_pic_") / file.isDirectory() |
C18723l.java | n7.l | GridLayoutManager.SpanSizeLookup | Span sizing |
C18727p.java | n7.p | RecyclerView.OnScrollListener | Scroll paging |
C18712a0.java | n7.a0 | Comparable | Photo item: {boolean a, String b..f, EnumC18737z g} |
C18730s/EnumC18737z.java | n7.z | enum holder | Photo-source / result discriminator (f47503a = default) |
DialogInterfaceOnDismissListenerC18717f.java | n7.f | OnDismissListener | Dialog cleanup |
C18735x.java | n7.x | AbstractC16130b | Second presenter stub (empty bodies m35055q/r/s) |
C18736y.java | n7.y | class | 1-field holder |
RunnableC18731t/32u/33v.java, RunnableC18715d.java | n7.t/u/v | Runnables | Sheet / 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 mergedn7.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 prefixhttps://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, thenC16945i.m32774f(DBContacts.f11708K, 0, new RunnableC20062p(dBContacts, contactKey, RunnableC18375q(7,…), arr, bitmap, list, null))— asynchronous DB write on the contacts executor. Failure → toastR.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); // ENCRYPTCipher cipher = Cipher.getInstance("Blowfish");cipher.init(2, secretKeySpec); // DECRYPTc18517d.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 byv6.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 fieldif (!AbstractC9039h.m10665v()) return -1; // must be a logged-in Eyecon userFile 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 headersDataOutputStream 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;// UPDATEContentProviderOperation.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 existsContentProviderOperation.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
| Sev | Finding | Evidence |
|---|---|---|
| HIGH | Contact 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" |
| HIGH | On 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 |
| MEDIUM | Photo 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 |
| MEDIUM | uploadpic.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 |
| LOW | Facebook 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 file | Real class | Kind | Purpose |
|---|---|---|---|
C20875a0.java (31 KB) | v6.a0 | p262e7.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.x | multi-interface hub | Implements InterfaceC9123k, OnAdManagerAdViewLoadedListener, InterfaceC10638a, InterfaceC20639p, InterfaceC22228k, InterfaceC21855j, ActivityResultCallback, InterfaceC22216a — the ad-manager/activity-result glue for the settings screen |
C20881g.java | v6.g | C3878j (DialogFragment) | Consent dialogs, incl. R.string.ask_me_for_every_call (CustomCheckbox) and R.string.sms_permission_pre_prompt_msg |
RunnableC20888n.java | v6.n | Runnable | ”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.java | v6.f | Runnable | ”No face” avatar-animation tutorial (R.string.no_face_anim_text5, R.id.goSocial, R.id.copy_facebook, R.id.copy_instagram) |
RunnableC20899y.java | v6.y | Runnable | R.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.java | v6.b | click listener | Delete-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.java | v6.c | click listener | R.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.java | v6.o | click listener | Opens https://www.facebook.com/eyeconapp, https://www.tiktok.com/@eyecon.app (×2 paths: ACTION_VIEW + AbstractActivityC3498e.m3705y0) |
ViewOnTouchListenerC20891q.java | v6.q | touch listener | Scroll/gesture |
RunnableC20892r.java | v6.r | Runnable | Caller-ID explainer (same strings as v6.n) |
C20883i.java | v6.i | ListAdapter | Open-source-licenses screen: R.id.TV_artifact / TV_license / TV_title |
C20884j.java | v6.j | DiffUtil.ItemCallback | License-list diffing |
RunnableC20874a.java | v6.a | Runnable | Navigates the license screen |
C20885k / C20886l / C20887m.java | v6.k / l / m | AbstractC21478j coroutine scopes | Async ad/banner lifetimes (InterfaceC16320o) |
C20897w.java | v6.w | TextWatcher | Name/label field |
C20878d.java | v6.d | InterfaceC18794b | Lifecycle hook |
C20882h.java | v6.h | empty class | Placeholder (133 bytes) |
RunnableC20896v / RunnableC20893s.java | v6.v / s | Runnables | Misc 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 file | Real class | Kind | Identification evidence (OBSERVED) |
|---|---|---|---|
AbstractServiceC19926g.java | ri.g | abstract Service | Log.isLoggable("EnhancedIntentService", 3) → EnhancedIntentService; unconfigurableExecutorService(threadPoolExecutor); field f51430b = ri.v Binder |
C19925f.java | ri.f | final class | extends-style wrapper over com.google.firebase.messaging.FirebaseMessagingService; remote flag "gcm.n.noui"; KeyguardManager.inKeyguardRestrictedInputMode() |
C19937r.java | ri.r | final class | "SERVICE_NOT_AVAILABLE", "INTERNAL_SERVER_ERROR", "TOO_MANY_SUBSCRIBERS", bundle.putString("gcm.topic", "/topics/" + str) → InstanceId/TopicManager subscribe |
RunnableC19939t.java | ri.t | Runnable | newWakeLock(1, "wake:com.google.firebase.messaging") + ConnectivityManager → FCM heartbeat |
ServiceConnectionC19943x.java | ri.x | ServiceConnection | ConnectionTracker.getInstance().bindService(..., 65); if (iBinder instanceof BinderC19941v) |
BinderC19941v.java | ri.v | Binder | if (Binder.getCallingUid() != Process.myUid()) → same-process guard |
AbstractC19940u.java | ri.u | abstract | context.startService(intent) → ProcessIntentHelper |
C19930k.java | ri.k | Closeable | Blocking queue close |
C19935p.java | ri.p | final class | SharedPreferences + ScheduledThreadExecutorService → FCM task scheduling prefs |
C19938s.java | ri.s | BroadcastReceiver | Boot/package-replace receiver |
C19920a/b/c.java | ri.a/b/c | InterfaceC19134c impls | TaskCompletionSource completions |
C19929j.java | ri.j | OnSuccessListener | — |
C19932m.java (45 KB) | ri.m | InterfaceC20435w1 + ViewBinding + MBSplashShowListener | JADX merge artefact: also contains KeyGenParameterSpec/GCMParameterSpec/Keystore and Mintegral MBSplashShowListener code |
C19927h/C19933n/C19934o/C19942w/C19931l/C19923d/C19924e | ri.h/n/o/w/l/d/e | helpers | Task/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 file | Real class | What it actually is |
|---|---|---|
AbstractC19327e.java (40 KB) | p8.e | JADX 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.java | p8.d | Facebook 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.java | p8.b | Facebook task/FetchResult helper |
C19328f.java | p8.f | InterfaceC18606a-like/observer |
C19325c.java | p8.c | Small builder |
C19323a.java | p8.a | Graphics 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 categoriesaddress,healthdeclared. p8.e.m35634C/p8.eis also the base class ofzq.b, OkHttp’s platform/SSL trust-manager helper — confirmingp8andzqshare one third-party namespace.- Only first-party-ish content in the package: pref key
SP_KEY_IS_MISSED_CALL_NOTIFICATION_ENABLED(referenced fromp8scope).
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:
- Post-registration flow (
com.eyecon.global.Registration.RegistrationActivity):m10528P0(…, R.string.oops_, …),m10544e1(false),m10506B0(),m10550k0(),f12997x0.m34274K(activity). - Settings consent toggles (
com.eyecon.global.MoreMenuAndSettings.SettingActivity):SettingActivity.m10331H0(key, !checkboxValue)+MyApplication.m10347i().m32077d(key, !checkboxValue)+m31543W(activity, key)→ deep linkeyecon://idPlus?source=<key>. - FairBid banner (
MediationRequest,BannerWrapper,InternalBannerOptions,AdDisplay,FetchOptions,SettableFuture). - 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 file | Real class | Kind | Purpose |
|---|---|---|---|
AbstractC15989e.java | dn.e | abstract base | Ad-request builder: ArrayList f39223a, int f39224b, boolean f39225c — an ORTB/Vast request accumulator |
AbstractC15990f.java | dn.f | abstract | implements InterfaceC19424c0 — ad-provider contract |
C15997m.java | dn.m | dn.f | Provider impl A |
C15986b.java | dn.b | dn.f | Provider impl B |
C15996l.java | dn.l | InterfaceC20758d, InterfaceC21472d | Listener pair (impression / completion) |
C15995k.java | dn.k | InterfaceC21472d, InterfaceC20758d | Second listener pair |
C15988d.java | dn.d | final class | Small request/response value |
C15987c.java | dn.c | extends Throwable | Ad error |
C15985a.java | dn.a | AbstractC21471c (coroutine Job) | Job wrapper |
C15994j.java / C15992h.java / C15993i.java | dn.j/h/i | extends AbstractC16733c | Small helpers / constants |
AbstractC15991g.java | dn.g | abstract | Marker |
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 file | Real class | Kind | Purpose |
|---|---|---|---|
C20542a.java | ts.a | AbstractC21478j + InterfaceC16320o | Owns a tech.crackle.cracklertbsdk.vast.ShowFullScreenVideo; builds an IAB OMID AdSession + AdEvents (InteractionType, MediaEvents) to certify viewability of the rewarded video |
C20547f.java | ts.f | final class | com.iab.omid.library.crackletech.Omid, AdSessionConfiguration, AdSessionContext, CreativeType, ImpressionType, Owner, Partner, VerificationScriptResource — OMID partner/verification-resource plumbing |
ViewOnClickListenerC20544c.java | ts.c | OnClickListener | Rewarded-video close button; pushes a C19399k(now, "closeLinear") event to the VAST tracker (C20444m0) |
C20545d.java / C20546e.java | ts.d / ts.e | AbstractC21471c | Coroutine jobs (play/close) |
C20548g.java | ts.g | AbstractC17896r + Function1 | Suspend builder |
AbstractC20543b.java | ts.b | abstract | Marker |
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 file | Real class | Kind | Purpose |
|---|---|---|---|
C20608z.java (27 KB) | u4.z | final class | The 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.java | u4.s | final class | In-flight load record (Context, u4.z, u4.w, p484po.C19404p) + CancellationException handling |
C20604v.java | u4.v | final class | Per-container binding: (ViewGroup, u4.e unit, EnumC20594l format, String adKey, WeakReference host, attach listener); m37684a() = tear-down |
C20605w.java | u4.w | AbstractC21478j + InterfaceC16320o | Load coroutine |
C20606x.java / C20607y.java | u4.x / u4.y | same | Render coroutines |
C20600r.java | u4.r | same + Function1 | Dispatch coroutine |
C20591i.java / C20592j.java | u4.i / u4.j | same | Init / ensure-is-initialised coroutines |
AbstractC20588f0.java | u4.f0 | abstract Comparable | Ad placement/format descriptor |
AbstractC20585e.java | u4.e | abstract | Base ad unit (u4.b, u4.c, u4.d derive from it) |
C20584d0.java | u4.d0 | extends AbstractQueue | FairBid request queue |
C20587f.java | u4.f | extends HashMap | Waterfall map |
C20580b0.java | u4.b0 | Comparable | Sortable unit |
C20586e0.java | u4.e0 | final class | Loader entry |
C20578a0.java | u4.a0 | final class | Placement registry |
C20579b/C20581c/C20583d | u4.b/c/d | u4.e | Ad-type variants |
EnumC20577a.java | u4.a | enum holder | Format enum |
EnumC20589g/g/h/k/l.java | u4.g/h/k/l | enums | u4.l is the only real enum — placement formats (INTERSTITIAL, REWARDED, BANNER, …) |
InterfaceC20582c0.java / InterfaceC20595m.java | u4.c0 / u4.m | interfaces | IB host callbacks |
AbstractC20585e/C20598p/C20599q | u4.p, u4.q | AbstractC21471c | Coroutine scopes |
RunnableC20596n.java / RunnableC20602t.java | u4.n / u4.t | Runnables | Callbacks |
RunnableC20597o.java (14 KB) | u4.o | synthetic class Runnable | First-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<= 0the loader refreshes once viaC20608z.m37685a(unit), otherwise it schedules aC20604vre-bind into theMap<ViewGroup, u4.v>. - Init:
u4.v.m37687c(Context)→ ifuse_IB_ad_loadingand 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.a0referencesBaseAfterCallActivity), DynamicArea (p033b6), SMS window (remote key **loadAdTimeOutSmsMS**), and Toki (banner inz7`).
11.2 Ad networks present (classification — depth not required)h5
| Network | Evidence |
|---|---|
| Google AdMob / GAM | R.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 Network | com.facebook.appevents.AbstractC9039h, com.google.ads.mediation.facebook.FacebookMediationAdapter, facebook_app_id = 1716660955257637, p8.d AppEvents |
| Fyber FairBid / Inneractive | com.fyber.fairbid.* everywhere, com.fyber.inneractive.sdk.external.InneractiveMediationDefs/MediationNameConsts, fb_ts_sdk_version = "FairBid SDK %s", MintegralAdapter |
| Pangle / ByteDance | com.bytedance.sdk.openadsdk.TTAdConstant, com.bytedance.sdk.component.pglcrypt.PglCryptUtils, com.google.ads.mediation.pangle |
| InMobi | com.inmobi.sdk.InMobiSdk, com.inmobi.media.*, com.inmobi.commons.core.configs.AdConfig |
| Amazon APS | R.string.amazon_app_key = 905c0238-13dd-4cf9-af3d-90f64672a4a9, com.fyber.fairbid.adapters.APSAdapter, com.amazon.aps.shared.metrics.model.ApsMetricsDataMap |
| Vungle / Liftoff | com.vungle.ads.*, com.vungle.mediation.VungleInterstitialAdapter, com.vungle.ads.internal.ui.VungleWebClient |
| Mintegral (Mbridge) | com.mbridge.msdk.*, MintegralAdapter, MintegralMediationAdapter, ~30 mbridge_* strings |
| AppLovin | R.string.applovin_key = UZvHsQBqsDCf0qv_yVDKn0UeTmRknnQSgTT3HP7HFikqs8IxdD-7uWdloUQVGTn09CQ0LThxLm6tfMnKDFS9xa |
| Moloco / Crackle | tech.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 |
| Singular | com.singular.sdk.Singular, com.singular.sdk.internal.Constants (used in v6.a0) |
| Amplitude | https://api2.amplitude.com/2/httpapi / https://api.eu.amplitude.com/2/httpapi |
| Firebase Performance / Crashlytics | FirebasePerfOkHttpClient, FirebasePerfUrlConnection.instrument(...), com.google.firebase.crashlytics prefs |
| Play Billing | com.android.billingclient.api.Purchase, BillingClient bundle handling |
| Adjust | R.string.adjust_app_token = m23t9uwrtvy8 |
| AppVestor | R.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,a9orp8. 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), andir.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,Singularevents,Amplitudeevents, and MICO’s own analytics tags built fromC3465f(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 -> thePermissionsHelperin 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 file | Real class | OkHttp class it is | Proof (OBSERVED) |
|---|---|---|---|
C22306a.java | zq.a | okhttp3.internal.platform.Android10Platform / Android10SocketAdapter | SSLSockets.isSupportedSocket, SSLSockets.setUseSessionTickets, isSupported() -> Build.VERSION.SDK_INT >= 29 |
C22307b.java | zq.b | okhttp3.internal.platform.Android10CertificateChainCleaner | extends p477p8.AbstractC19327e (which is okhttp3.internal.tls.CertificateChainCleaner); X509TrustManagerExtensions.checkServerTrusted(chain, "RSA", hostname) |
AbstractC22308c.java | zq.c | okhttp3.internal.platform.Platform | CopyOnWriteArraySet f61169a of loggers, Log.isLoggable, 4000-char log chunking; linkedHashMap.put(C19227c0.class.getName(), "okhttp.OkHttpClient") |
C22309d.java | zq.d | okhttp3.internal.platform.OkHttpLogHandler | extends java.util.logging.Handler, maps INFO -> 4, above -> 5 |
C22310e.java | zq.e | okhttp3.internal.platform.Jdk9Platform / Jdk9SocketAdapter | reflective getDeclaredMethod("setUseSessionTickets", Boolean.TYPE), getAlpnSelectedProtocol, setAlpnProtocols |
C22311f.java | zq.f | okhttp3.internal.platform.BouncyCastlePlatform | mo38653a returns false; delegates to zq.h |
C22313h.java | zq.h | BouncyCastleJssePlatform / BouncyCastleJsseSocketAdapter | org.bouncycastle.jsse.provider.BouncyCastleJsseProvider, BCSSLSocket, BCSSLParameters |
C22314i.java | zq.i | ConscryptPlatform | Conscrypt.isConscrypt(sSLSocket) |
C22316k.java | zq.k | ConscryptSocketAdapter | Conscrypt.setUseSessionTickets, Conscrypt.setApplicationProtocols, version gate >= 2.1.0 |
AbstractC22315j.java | zq.j | Conscrypt.version() shim | major()/minor()/patch() comparison to 2.1.0 |
C22318m.java | zq.m | AndroidPlatform base | synchronized InterfaceC22319n m39800d(SSLSocket) lazy adapter selection |
InterfaceC22317l.java | zq.l | SocketAdapter | boolean mo38653a(SSLSocket), SocketAdapter mo38654b(SSLSocket) |
InterfaceC22319n.java | zq.n | Platform.SocketAdapter | mo39731a/b/c + isSupported() |
C22320o.java | zq.o | AndroidSocketAdapter | empty 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):
| Method | Real sig | Purpose |
|---|---|---|
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() | Intent | Notification-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 .. m39797X | kotlin-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 Settingselse 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 : 120Note 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":
// m39774G0if (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 branchint 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);}// m39753Sif (SP.getInt("PS-Counter-" + p, 0) > 4) return true; // >=5 attempts -> never ask againif (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
| Sev | Finding | Evidence |
|---|---|---|
| MEDIUM | The 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 |
| MEDIUM | The 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 |
| LOW | OEM 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() |
| NONE | Consent 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 |
| NONE | OkHttp 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
| File | Kind | Size | Role |
|---|---|---|---|
TokiActivity.java | Activity extends AbstractActivityC3498e | 3.1 KB | Host activity. Two intent actions only: INTENT_ACTION_START_TOKI, INTENT_ACTION_REFOCUS_TOKI. |
TokiService.java | Service implements SensorEventListener | 5.2 KB | Foreground service + proximity-sensor monitor that emits com.eyecon.global.isonear. |
TokiContactsChooserActivity.java | Activity extends AbstractActivityC3498e | 11.4 KB | Multi-select contact picker used by Toki (and other “share/can-talk/toki” flows). |
ClearFocusOnBackEditText.java | CustomEditText subclass | 1.2 KB | onKeyPreIme 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>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_TOKIor intent flagFLAG_ACTIVITY_LAUNCHED_FROM_HISTORY (0x100000)-> tryC22122v0.m39384W0(activity, null, null)(resume the existing fragment). If that fails: log"onNewAction canceled, unable to resume the toki"(+ the_after_launched_from_historyvariant) thenfinishAndRemoveTask(), and for the history case additionallystartActivity(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(...); elseC22122v0.m39386G1(this, intent.getExtras()).onNewIntentre-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 notificationSensorManager sm = (SensorManager) MyApplication.f12163h.getSystemService("sensor");Sensor prox = sm.getDefaultSensor(8); // TYPE_PROXYIMITYif (prox != null) f13078a = prox.getMaximumRange();sm.registerListener(this, prox, 3); // SENSOR_DELAY_NORMALm10586a() (TokiService.java:44-61):
if (26 <= Build.VERSION.SDK_INT) AbstractC18372n.m34531s("toki_service", 2, "Eyecon Toki", false); // create notification channelNotificationCompat.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 != 9stopSelf(); 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 indexIntentFilter 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):
| Caller | Trigger |
|---|---|
z7.l0 (ViewOnTouchListenerC22102l0) case default | R.id.IV_talk ACTION_DOWN (0) -> m39404R0(), ACTION_UP (1) -> m39403Q0() - classic press-and-hold |
z7.p0 case default | Single tap on the incoming-Toki notification’s answer button - no press-and-hold |
p457t9.C20641q | the “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-41int action = motionEvent.getAction();if (action == 0) { if (z7.v0.f60438R) z7.v0.m39403Q0(); z7.v0.m39404R0(); } // DOWN -> startelse if (action == 1) { z7.v0.m39403Q0(); } // UP -> stopreturn true; // consumes the eventSo 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 / member | Type | Purpose |
|---|---|---|
f13052q0, f13053r0 | static 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. |
f13054U | double | INTENT_KEY_REQUEST_KEY (default -1.0) - the lookup key into f13053r0. |
f13055V | String | INTENT_KEY_CONTACT_CLI - pre-selected CLI. |
f13056W | int | INTENT_KEY_REQUEST_CODE. |
f13057X | InterfaceC16133e | The caller’s filter; null -> RuntimeException("originalFilter is null") + finish(). |
f13063d0 / f13064e0 | C22093h / C22097j (z7.h/z7.j) | Two LruCache-backed adapters (contacts + selected). evictAll() in onDestroy. |
f13065f0/f13066g0 | RecyclerView | RV_contacts_selector, RV_selected_contacts. |
f13071l0 | C21867v (y5.v) | Progress/sharing dialog; receives onActivityResult for request codes 89 and 100 (C21867v.m39116t(intent)). |
f13072m0 | C3892x | Blocking progress dialog; if a "mWaitingDialog" fragment survives a config change it is removed and the resume runnable is re-posted. |
f13073n0 | HashSet | INTENT_KEY_PRE_SELECTED_CONTACTS (serializable). |
f13074o0 | boolean | Insets applied once. |
f13058Y / f13060a0 | ArrayList | full contact list / filtered-visible list. |
f13062c0 | String | current lower-cased search text. |
f13075p0 | int | monotonically 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
| Sev | Finding | Evidence |
|---|---|---|
| MEDIUM | The 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)) |
| MEDIUM | Answering 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 |
| LOW | AbstractC16468f0.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 file | Real class | Kind | Purpose |
|---|---|---|---|
C21295o.java (13 KB) | w7.o | final class (singleton f56545b, f56546a=true) | The share engine. Endpoint construction, provider detection, WebView HTML builders, job entry point. |
C21293m.java | w7.m | model | One 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.java | w7.p | w7.m subclass | TikTok-specific item (extra HTML field f56547l, f56548m = hasRawEmbed). |
C21294n.java | w7.n | singleton f56542c | Local DB layer. Thread named "ShareVideoDB". Table table_share_media (in contactdb). |
C21284d.java | w7.d | singleton f56488g | ”first card” cache refresher; log tag "MediaShareFirstCardC..". |
C21289i.java (32 KB) | w7.i | DialogFragment | The “ShareVideoHelper” modal (WebView + 3 CTAs). |
DialogC21290j.java | w7.j | Dialog | Small confirm dialog. |
RunnableC21281a.java | w7.a | Runnable | Injects the provider-specific JS video-URL scraper into the WebView. |
C21282b.java | w7.b | AbstractC16134f + @JavascriptInterface | JS bridge: close(), onLoadedMetaData(), onVideoUrlFound(String), onPageSizeChanged(int). |
C21283c.java | w7.c | holder | WebView + w7.m + provider + mediaType + generation for one card render. |
EnumC21291k.java | w7.k | enum | TIKTOK("tiktok"), SHARE_CHAT("share_chat"), FACEBOOK("facebook"), INSTAGRAM("instagram"). |
EnumC21292l.java | w7.l | enum | VIDEO("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 userif (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 POSTERString content = j.getString("content"); // <-- provider-specific payload blobString 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):
| Provider | What is rendered |
|---|---|
TIKTOK | First 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. |
FACEBOOK | Runs 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+ everyiframe.contentDocument, queriesvideo, audio, returnsvideos[0].src(or''). - TikTok: walks the same iframe set, queries
[data-testid=play-video], returnsvideos[0].src. - Fallback (case default): dumps
document.documentElement.outerHTMLintow7.o.m38461C(text)with remote-config regexfindFirstMp4UrlForInstagramRegexto extract the first.mp4URL.w7.o.m38466H(url, isPost)then wraps the resolved CDN URL in a generated HTML5<video>page that callsEyeconWebView.onLoadedMetaData()fromonloadedmetadataand scrolls toclientHeight*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
| Field | Source | Reaches third parties? |
|---|---|---|
url | server | Yes - 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 blob | server | Yes - Facebook uses the fb_mobile_* AppEvents path (§8.2). |
cli (poster’s Eyecon id) | server | Stays local (table_share_media.cli). |
share_ts, seen, type, id, alias | server | Stays local. |
| The user’s contacts | local | Only handed to ContactsChooserActivity for the OS/share-sheet flow; not sent to facebook/instagram/tiktok by any code in w7. |
| The user’s Eyecon CLI | e-auth header on GET /app/share | Sent 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
| Sev | Finding | Evidence |
|---|---|---|
| HIGH | Server-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 |
| MEDIUM | The 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 |
| MEDIUM | MenifaFragment.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 |
| LOW | The 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 |
| LOW | w7.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
| File | Kind | Role |
|---|---|---|
CustomThemePreviewActivity.java (13 KB) | Activity extends AbstractActivityC3498e | Custom-theme paywall + preview + purchase |
CustomThemeView.java (4.5 KB) | FrameLayout | Phone-frame preview widget with a theme enum |
ThemeException.java | Exception | Marker only |
CustomThemePreviewActivity (OBSERVED):
- Extras:
"source"(default"Missing source"), used as the analyticsSourcedimension. - 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 ofEB_premium/EB_productis visible). - Purchase flow:
new C16708j(productId, "inapp")->C16707i.f41454h.m32534M(list, new C19867k0(18, ...))(Play BillingBillingClient); the listener is stored inf13042Xand torn down withC16707i.f41454h.m32535O(...)inonDestroy. - Photo swap:
EB_change_photo->ViewOnClickListenerC21874b(...,1); the resultingBitmapis held in the staticf13038c0and applied viaCustomThemeView.setThemeBackgroundBitmap(bitmap)->RunnableC20908f(27, view, bitmap)(writes into the theme cacheC21884l.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 inCustomThemeView.m10582c; a pending purchase forces the cached custom background). - Theme enum
y7.j(EnumC21882j):SYSTEM_DEFAULT,CUSTOM_THEME, plus the built-in ones (fieldf59016f= 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
onClickhandlers on distinctView.OnClickListeneranonymous classes (f54614bcaptured 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))-> enablesEnumC19861h0.TOKIon 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 valueAbstractC15804d.m31086g("...")the screen requests theCALL_SCREENINGrole (line 7084-7096). - Shared-media WebView:
onDestroysendsACTION_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), andRunnableC17219d1-> the Dynamic-Area carousel of §15.5 is embedded in this screen.
Security findings (Menifa):
| Sev | Finding | Evidence |
|---|---|---|
| MEDIUM | The 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) |
| LOW | The 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 |
| LOW | C19859g0 “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 theblock_list/spam_listtables incontactdb(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/ linkm10415D0((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 putMyApplication.m10347i().m32074a(null); // commitnew 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):
| Sev | Finding | Evidence |
|---|---|---|
| LOW | The 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 |
| LOW | The 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 APIif (code != 200) throw new EyeconException();m33503W(response.byteStream(), listA, listB, listC, out); // multi-part stream decodeC21856k.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):
| File | Kind | Role |
|---|---|---|
BaseCardViewLayout.java | CardView | dispatchTouchEvent forwards to a setCustomTouchListener(...) hook so the carousel can intercept swipes. No logic. |
CardsException.java | Exception | Marker. |
SwipeRefreshLayout.java (10.8 KB) | androidx.swiperefreshlayout-style | Drop-in pull-to-refresh used by Menifa and the cards list. No MICO logic. |
Security findings (cards):
| Sev | Finding | Evidence |
|---|---|---|
| MEDIUM | The 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 |
| MEDIUM | Card 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) |
| LOW | Impression/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 OBTAINEDMolocoLogger.setLogEnabled(false);AdRegistration.getInstance(getString(R.string.amazon_app_key), ctx); // Amazon APSAdRegistration.setMRAIDSupportedVersions(new String[]{"1.0","2.0","3.0"});AdRegistration.setMRAIDPolicy(MRAIDPolicy.CUSTOM);AudienceNetworkAds.buildInitSettings(ctx).withInitListener(new C19052e()).initialize(); // Meta FANMBridgeSDKFactory.getMBridgeSDK().setConsentStatus(ctx); // Mintegral/Mbridgem35358p(consent == 3, consent == 2 || consent == 3); // Fyber/FairBid consentCrackleMediationAdapter.crackleInitialize(ctx); // MolocoMobileAds.initialize(ctx, new C19053f()); // Google AdMob / GAMAudienceNetworkAds.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).
16.2 Consent handling - what is actually implementedh5
| Mechanism | OBSERVED detail |
|---|---|
| Google UMP | C19064q.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 heuristic | C19064q.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 AdMob | AbstractC19054g.m35355M(builder):<br>if ((status == REQUIRED || (status == UNKNOWN && gdprBySim)) && status != OBTAINED) addNetworkExtrasBundle(AdMobAdapter.class, {npa:"1"}) |
| FairBid / Fyber network extras | addNetworkExtrasBundle(FyberMediationAdapter.class, {mute_video:false}) |
| Mbridge | MBridgeSDKFactory.getMBridgeSDK().setConsentStatus(ctx) (IAB TCF read) |
| Mintegral COPPA | p274eo/C16303b:976 MintegralMediationAdapter.f21500b.setCoppaStatus(ctx, MobileAds.getRequestConfiguration().getTagForChildDirectedTreatment() == 1) |
| Pangle | ir/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):
| Network | Evidence | Role |
|---|---|---|
| Google AdMob / GAM | R.string.admob_app_id = ca-app-pub-2959190743815944~6092014645; 26 ca-app-pub-… unit ids in remote-config defaults; MobileAds.initialize | Primary mediation |
| Meta / Facebook Audience Network | AudienceNetworkAds.buildInitSettings(...).initialize(); com.google.ads.mediation.facebook.FacebookMediationAdapter; facebook_app_id = 1716660955257637; p8.d AppEvents | Adapter + own SDK init |
| Fyber FairBid / Inneractive | com.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 APS | R.string.amazon_app_key = 905c0238-13dd-4cf9-af3d-90f64672a4a9; com.fyber.fairbid.adapters.APSAdapter; com.amazon.aps.shared.metrics.model.ApsMetricsDataMap | FairBid adapter |
| Moloco / Crackle | tech.crackle.*, com.moloco.sdk.*, CrackleMediationAdapter, R.string.crackle_key = fai=1:917722272265:android:af67d… | Adapter + own rewarded-video/OMID SDK (ts, §10) |
| Pangle / ByteDance | com.bytedance.sdk.openadsdk.TTAdConstant, com.bytedance.sdk.component.pglcrypt.PglCryptUtils, com.google.ads.mediation.pangle | GAM adapter only |
| Mintegral / Mbridge | com.mbridge.msdk.*, MintegralAdapter, MintegralMediationAdapter | GAM + FairBid adapter |
| InMobi | com.inmobi.sdk.InMobiSdk, com.inmobi.media.* (≈25 classes absorbed into v6.f) | Standalone SDK init |
| Vungle / Liftoff | com.vungle.ads.*, com.vungle.mediation.VungleInterstitialAdapter, com.vungle.ads.internal.ui.VungleWebClient | Adapter + standalone |
| AppLovin | R.string.applovin_key = UZvHsQBqsDCf0qv_… | Adapter |
| Singular | com.singular.sdk.Singular, com.singular.sdk.internal.Constants (used in v6.a0) | Attribution |
| Adjust | R.string.adjust_app_token = m23t9uwrtvy8 | Attribution |
| AppVestor | app_vestor_api_token = 980d4e3f-…, app_vestor_app_key = 8c830f45-… | Attribution |
| Amplitude | https://api2.amplitude.com/2/httpapi, https://api.eu.amplitude.com/2/httpapi | Analytics |
| Firebase Performance / Crashlytics | FirebasePerfUrlConnection.instrument(...), com.google.firebase.crashlytics | APM |
| Play Billing | com.android.billingclient.api.Purchase | Billing (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):
| Trigger | Key | Behaviour |
|---|---|---|
| In-banner (“IB”) | use_IB_ad_loading, ac_ib_refresh_time_ms | u4.z re-binds the unit into Map<ViewGroup, u4.v> on the cadence (§11.1) |
| Morning pre-load | morning_ads_refresh_job.is_enable, .when (default "7:00") | com.eyecon.global.Ads.MorningAdRefreshJob (§16.5) |
| Ad-free window | duration_without_ads_v2 | m35351I(): if now < installTs + duration -> no ads |
| Ad-free window | duration_without_popup_and_aftercall | m35353K(): same for popups + after-call |
| Fullscreen caller-ID feed | incall_full_screen_enable_ads, incall_fs_feed_enable | w0.a.m38324I pre-load, gated also on CallStateService.m10199X() |
| In-call feed | CallerIdHelper.m32645G() | gp.a.m32647J pre-load |
| Lock-screen no-video fallback | use_no_video_ad_only_when_not_paid | m35356N swaps to lock_screen_no_video_ad_unit_id when the last ad went unpaid and the screen is locked |
| MediaShare fetch cadence | share_video_list_sync_interval | §14 |
| Remote config cadence | RemoteConfigUpdater (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 callbackpremium == TRUE -> log "source = %s, premium user 1" ; presenter.m31516i(); // show adpremium == FALSE -> log "source = %s, NOT premium user 1"; presenter.m31515h(); // suppress16.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
| File | Kind | Role |
|---|---|---|
Ads/CustomMediaView.java (2.6 KB) | extends com.google.android.gms.ads.nativead.MediaView | Rounded-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) | Activity | Fullscreen 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) | FrameLayout | FairBid/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
| Sev | Finding | Evidence |
|---|---|---|
| MEDIUM | Eight-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 |
| MEDIUM | Ad 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) |
| MEDIUM | addCustomTargeting sends SIM country, device manufacturer, Android release, current network type and account-identity status on every AdManager request. | m35346D |
| LOW | Ad 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 |
| LOW | FullscreenNativeAdsActivity 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
| File | Kind | Role |
|---|---|---|
PhotoPicker/PhotoPickerActivity.java (44 KB) | Activity | The photo picker host. R.layout.activity_photo_picker. |
PhotosTrackerWorker.java (12 KB) | androidx.work.Worker | Automated Facebook profile-photo harvester - see 17.3 |
PhotoRevealAnimationView.java (15 KB) | custom View | Reveal animation for the photo grid/pager. No logic, no I/O. |
ZoomImageView.java (25 KB) | custom View | Pinch-zoom for the full-screen photo. No logic, no I/O. |
PhotoPickerActivity intent contract (OBSERVED):
| Extra | Meaning |
|---|---|
name, cli | Contact name + Eyecon CLI being decorated |
deepLink (bool) | launched from a deep link |
EXTRA_KEY_IS_FROM_NEW_CONTACT_ACTIVITY | NewContactActivity mode -> no disk write, return via m10425J0 |
AbstractC0039a.f210d | contact id |
contact_cis_list (ArrayList from c19862i.m36624E()) | the contact’s CLI list |
contact_image_tag | picLastApproveTag |
AbstractC0039a.f207c | primary_raw_id |
EXTRA_SOURCE, EXTRA_IS_EYECON_USER, change_pic | analytics + “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-enablesEBapprove(R.string.set_photo) after a savem10423H0(String, int, EnumC18737z)- the “no results / switch tab” callback (then7.epresenter tail, §6.1)m10425J0(int, Bundle)- return path toNewContactActivity(NewContactActivity.f11749G0 = bitmap)m10426K0()-R.string.permissions_needed+R.string.permission_app_setting_message+R.string.go_to_settingsdialogm10418G0(String)- analyticscontact_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.hactivity.f12464s0 = fb;if (fb.f46788e == null) return;fb.f46791h = -1;fb.f46786c = new C18519e(fb, fb.f46784a, 0); // m7.e WebViewClientfb.f46787d = new C18519e(fb, fb.f46784a, 1);fb.m34719B(fb.f46788e, fb.f46784a[0]); // <-- the actual searchm7.h.m34719B (C18525h.java:107-110):
webView.setWebViewClient(f46786c);webView.loadUrl("https://m.facebook.com/search/people/?q=" + str); // str = the CONTACT NAMEand m7.h.m34718D configures each WebView (C18525h.java:95-103):
webView.setAlpha(0.0f); // invisiblewebView.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 switchWorkManager.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 baselineeyeconRoomDB.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
| Sev | Finding | Evidence |
|---|---|---|
| CRITICAL | A 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 |
| HIGH | Background 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 |
| HIGH | Hidden 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 |
| HIGH | Remote-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 |
| HIGH | Contact 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 |
| MEDIUM | uploadpic.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 |
| MEDIUM | fct 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 |
| LOW | PhotoRevealAnimationView / 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
| Check | Command / scope | Result |
|---|---|---|
Any new AudioRecord(...) | whole sources tree | 1 hit - p599v7/RunnableC20908f.java case 11 (Toki, §2.1) |
MediaRecorder | p* packages | 0 hits |
MediaProjection | p* packages | 0 hits |
VOICE_DOWNLINK / VOICE_UPLINK / VOICE_CALL | p* packages | 0 hits (only an unrelated constant inside androidx.core.content.ContextCompat) |
AudioRecord guarded by RECORD_AUDIO | z7.v0.m39395G0 | yes |
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 aboveand 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 / notifyItemInsertedOP 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, orif (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.
| Method | Purpose |
|---|---|
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
| Sev | Finding | Evidence |
|---|---|---|
| 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 |
| HIGH | Every 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 |
| MEDIUM | The 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 |
| MEDIUM | Recordings 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 |
| LOW | m9991A0() reports raw free-space; m10001H0 shows an exact remaining-quota count to the user. | RecordingsFragment.m9991A0 |
| LOW | File 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
| File | Kind | Role |
|---|---|---|
BackupActivity.java (3 KB) | Activity | Host for the f5.j upload / f5.u restore dialogs |
ProgressTracker.java (2.2 KB) | FrameLayout | Two RoundedCornersFrameLayout children; setProgress(float) sets the front bar’s width to getWidth()*f. Pure UI. |
BackupOrRestoreException.java | Exception | Marker. |
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
| File | Kind | Role / findings |
|---|---|---|
MoreSettingsFragment.java (47 KB) | Fragment | Settings 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) | Activity | Toggle 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) | Activity | Own 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) | Worker | doWork(): 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) | Activity | R.string.version + the social/FAQ/legal links already listed in §7. No secrets. |
LicensesActivity.java (4.4 KB) | Activity | Renders LicenseItem rows. No logic. |
LicenseItem.java (23 KB) | model | One 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. |
Finding (LOW): the eyecon://idPlus?source=<key> deep link emitted on every settings toggle is a cross-surface tracking/telemetry channel - any handler of that scheme can observe which settings a user flips. The scheme is not registered as exported in this cluster; handler-side exposure is outside C5 scope.h2
20. Remote config - verification of the briefing’s claims (OBSERVED)h4
20.1 Transport: CLEARTEXT HTTP, unauthenticated - CONFIRMEDh5
| Item | Value / evidence |
|---|---|
| Default URL | http://cloudfront-cdn-all.eyecon-app.com/configs/ab-test.txt - AbstractC15804d.java:300 (key eyecon_remote_config_url) |
| Fetcher | p005a4/RunnableC0031c.java case 17 -> C21273a.m38423G(new ArrayList(), url, "GET", code, /*accept json*/false, "getUpdatedEyeconRemoteConfig", null, false, false, null, "") |
| Client | w6.a = C21273a (/* JADX INFO: renamed from: w6.a */, TAG = "NetworkHelper"), HttpURLConnection wrapped by FirebasePerfUrlConnection.instrument(...) |
| Auth | NONE. 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. |
| Headers | User-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 |
| Scheduler | com.eyecon.global.Analytics.RemoteConfig.RemoteConfigUpdater (WorkManager Worker); blocked entirely if dev_remote_config_tester == 1 |
| Store | SP EyeconRemoteConfigVersion (int) vs remoteConfigVersion; d5.c.m31073A performs the A/B bucketing (isNotMatchTargetExposure / isNotMatchTargetCountry / isNotMatchTargetLanguage / isNotMatchTargetVersion, random_result, _variant_chance) |
| Package-local wrapper | com.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 key | Effect if flipped by a cleartext-HTTP attacker |
|---|---|
eyecon_remote_config_url | Point 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_ports | Redirect 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_agent | Change the UA the Google/Facebook scrapers present. |
facebooke_Extract_URL_Regex, findFirstMp4UrlForInstagramRegex, regexForAddSocial, notSupportedKeys | Change which social content is extracted / blocked (§14, §15.2). |
isWalkieTalkieEnabled | Add/remove the Toki button app-wide (§15.2). |
isPhotosTrackerEnabled | Enable/disable the background Facebook photo harvester (§17.4). |
shouldSendNameToServer | Toggle whether the user’s contact name is transmitted to the backend (RunnableC16004e:399, set in AbstractC15804d:420). |
fct | Swap 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 ids | Retarget the ad stack (§16.4). |
duration_without_ads_v2, duration_without_popup_and_aftercall, free_rec_calls_actions | Remove 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_id | Force a specific ad on the lock screen. |
share_video_list_sync_interval, is_media_share_cache_enable | Control the social-share feed (§14). |
is_gam_key_value_enable, custom_theme_purchase_options, custom_background_theme_product_id | Ad targeting + theme pricing/purchase options (§15.1). |
dev_remote_config_tester, should_show_caller_id_denied_dialog | Disable the consent dialog / suppress the “caller ID denied” nag (§12.1). |
open_whatsapp_intents | Rewrite 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_url | Ad 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_historyblock_list, spam_listsocial (+ column added in upgrade)names_photos_cachefresh_picsrecorded_notessms_tablettm (TalkiToMe)statistics_questionsdynamic_area_tickets, dynamic_area_subjects <-- §15.5table_share_media <-- §14table_api_requestsdb_table_notification_reader, db_table_reverse_lookup_idshared_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
| Sev | Finding | Evidence |
|---|---|---|
| CRITICAL | The 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 |
| HIGH | The 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 |
| MEDIUM | shouldSendNameToServer 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 |
| MEDIUM | The 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) |
| LOW | EyeconRemoteConfigVersion 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 claim | Verdict | Proof |
|---|---|---|
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
| Sev | Area | Finding |
|---|---|---|
| CRITICAL | Remote config | Whole 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) |
| CRITICAL | Photos | Facebook app secret 1716660955257637|afa3f38c55f24c73b177afc962969b15 hardcoded in strings.xml and used as a Graph access_token; also remotely overridable via fct. (§17.5) |
| HIGH | Photos | PhotosTrackerWorker 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) |
| HIGH | Photos | Hidden Facebook mobile-web people-search in an invisible JS WebView with a spoofed Pixel 2 / Chrome 79 UA, once per contact name. (§17.2) |
| HIGH | Photos | Remote-config-controlled arbitrary JS executed in a JS_ENABLED WebView carrying an addJavascriptInterface bridge. (§17.3) |
| HIGH | Photos | Contact photos at rest: Blowfish/ECB, no IV, <=1000 keys + shared external storage. (§6.4, §17.5) |
| HIGH | Toki | Microphone (AMR) streamed over plaintext UDP with OAR/XOR FEC only, no DTLS/SRTP/AES/HMAC; host + port partly server-controlled. (§2.2, §2.5) |
| HIGH | Toki | The 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) |
| HIGH | Backup | The 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) |
| HIGH | Recordings | RECORD_AUDIO is requested and a premium “actions” counter is decremented for a call-recording feature that does not exist in this build. (§18.5) |
| HIGH | Social share | Server-driven arbitrary-URL rendering in a JS_ENABLED WebView with a @JavascriptInterface bridge (GET /app/share -> table_share_media -> iframe). (§14.4) |
| MEDIUM | Ads | 8+ ad SDKs initialised unconditionally at app start; consent (npa=1) reaches only the AdMob extras bundle, not InMobi/Vungle/Pangle/Moloco/Singular. (§16.7) |
| MEDIUM | Ads | Every AdManager request carries SIM country, manufacturer, Android release, network type and account-identity status as custom targeting. (§16.3) |
| MEDIUM | Photos | The photo flow writes the chosen postal address into the user’s system Contacts via applyBatch("com.android.contacts"). (§6.3) |
| MEDIUM | Permissions | Runtime.exec("am start -n com.huawei.systemmanager/...") shell-out; OEM probing across 14 vendor package names. (§12.5) |
| MEDIUM | Toki | Foreground notification is VISIBILITY_PUBLIC; FGS type 29 (microphone). (§13.3) |
| MEDIUM | Backup | Restore is a blind renameTo swap with rollback only on rename failure - no integrity gate. (§3.4) |
| LOW | Settings | Every settings toggle emits an eyecon://idPlus?source=<key> deep link (cross-surface telemetry channel). (§19.2) |
| LOW | Permissions | Consent is a single writable SP boolean, and the consent text can be hidden by an intent flag. (§12.5) |
| LOW | Recordings | Recording 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, §2z7, §3f5, §4ol, §5jh, §6n7, §7v6, §8ri/p8, §9dn, §10ts, §11u4, §12zq, andp010a9/a9in §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:
z751,f527,v622,n727,jh4,a61,zq.g1 = 133 files - THIRD-PARTY SDK:
ri22 (Firebase Messaging),u431 (FairBid),ts7 +dn13 (Moloco/Crackle),ol2 (Glide),p86 (Facebook/OkHttp/RxJava),zq14 (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: theeyecon://idPlussettings-consent deep link),zq.g(~1 class of ~30 in the file)
- FIRST-PARTY MICO:
- 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
| Topic | Canonical 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.jsp | cluster C1 / C2 |
f7.f0.m32019N, R.string.pk_enc, error-report scrubbing | cluster C1 (§3.2 here states the correction) |
contactdb schema in full, Room DB eyecon_room | DATA-LAYER.md (§20.3 here gives the table list) |
Dialer / SMS / call state, CallStateService, IncomingSMSDTO | cluster C3 |
MainActivity, MainFragment, ForYouFragment, HistoryFragment | cluster C4 |
Manifest, exported components, network_security_config, permissions declared | MANIFEST-AND-SECURITY.md |
All api.eyecon-app.com/app/*.jsp endpoints, request/response shapes | BACKEND-API.md |
21.5 Things this cluster explicitly did NOT find (negative results, so later clusters do not re-search)h5
- No
MediaRecorder, noMediaProjection, noAudioManager.MODE_IN_CALLcapture, noVOICE_DOWNLINK/UPLINK/CALLaudio source. No call recording. - No
TrustManager/HostnameVerifier/certificate-pinning override, noALLOW_ALL, no OkHttpCertificatePinnerbypass. 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) andrc_key = "PgdtSBeR0MumR7fO"(static request key).
22. Appendix - full endpoint / key / secret inventory for cluster C5h4
22.1 Endpoints touched by C5 codeh5
| Endpoint | Transport | Where |
|---|---|---|
https://api.eyecon-app.com/app/uploadpic.jsp | HTTPS | own profile picture, msisdn = clientId, multipart/form-data, raw JPEG body (6.2) |
https://api.eyecon-app.com/app/addcontacts.jsp | HTTPS | contacts incl. photo bytes in the contacts JSON (6.2) |
https://api.eyecon-app.com/app/join.jsp / getcontactslang.jsp | HTTPS | signup + language negotiation (6.2) |
https://api.eyecon-app.com/app/share | HTTPS | GET ?source=mysettings, social-share list (14.3) |
https://voice2text.eyecon-app.com/start-ttm | HTTPS | TTM / voice2text {mycli,acli,calltag,purchases} (11, u4.o case 6) |
https://cards.eyecon-app.com/getCards? | HTTPS | Dynamic-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.txt | HTTP (CLEARTEXT), unauthenticated | remote config / A-B flags (20.1) |
ptt.eyecon-app.com:<toki_ports> | plaintext UDP | Toki voice channel + signalling (2.2) |
https://graph.facebook.com/<socialId>/picture?width={50,200,600}&access_token=<fct> | HTTPS | Facebook 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.js | HTTPS (iframe) | share card (14.3) |
https://www.tiktok.com/oembed?url=<url> | HTTPS | share card (14.3) |
Google Drive appDataFolder | HTTPS (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/ | HTTPS | settings/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/UCYI84aNWZ6Q0TxaCVTjiaFQ | HTTPS | social links (7) |
https://docs.google.com/forms/d/17QjnHJanVGPt0V_AkVj_baVmE2xZZGOyjAKjB22oeOU/viewform?edit_requested=true | HTTPS | Toki in-app feedback (2.5) |
https://docs.google.com/forms/e/1FAIpQLSczgi6BudeGMX_kHxfkxQHRlrNS08Ib5NcTVHhhHjySIbJMCA/{viewform,formResponse} | HTTPS | welcome_back_notification_url (20.2) |
https://apps.eyecon-app.com/call-contest/a/report-calls | HTTPS | call_contest_report_calls_url (20.2) |
https://api2.amplitude.com/2/httpapi, https://api.eu.amplitude.com/2/httpapi | HTTPS | Amplitude (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.js | HTTPS | mobitech_*_html_script remote-config ad creative (20.2) |
https://www.google.com | HTTPS | ac_ads_test_internet_connection_url (20.2) |
https://youtu.be/2lxRSxgbxmg | HTTPS | invite_youtube_link (20.2) |
22.2 Hardcoded secrets / identifiersh5
| Constant | Where | Note |
|---|---|---|
R.string.fct = 1716660955257637|afa3f38c55f24c73b177afc962969b15 | res/values/strings.xml | Facebook app-id | app-secret, used as a Graph access_token. Remote-overridable via fct. CRITICAL. |
R.string.facebook_app_id = 1716660955257637 | strings.xml | Meta app id |
rc_key = PgdtSBeR0MumR7fO | p620w6/C21273a.java | static request-key header e-auth-k |
R.string.pk_enc = MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A… (RSA-2048 SPKI) | strings.xml | error-scrubber public key (§3.2, not backup) |
R.string.cipher_key = Ewu | strings.xml | Blowfish contact-photo key prefix (§6.2) |
R.string.admob_app_id = ca-app-pub-2959190743815944~6092014645 | strings.xml | GAM app id (26 unit ids in remote-config defaults) |
R.string.amazon_app_key = 905c0238-13dd-4cf9-af3d-90f64672a4a9 | strings.xml | Amazon APS |
R.string.applovin_key = UZvHsQBqsDCf0qv_… | strings.xml | AppLovin SDK key |
R.string.crackle_key = fai=1:917722272265:android:af67d7515574ed8c689ed8~~~fak=AIzaSyBxXQfYa0khnAZ2-NiK3WQV-4JbMWCw6ls~~~fpi=eyecon---eyecon-caller-id~~~s5=417687~~~s6=234363 | strings.xml | Moloco/Crackle (also embeds a Firebase API key) |
R.string.adjust_app_token = m23t9uwrtvy8 | strings.xml | Adjust |
R.string.app_vestor_api_token / _app_key | strings.xml | AppVestor |
e-auth-c derivation: C21273a.m38421E(e-auth) = sum( Character.getNumericValue(first char of each "-"-separated segment) ) | p620w6/C21273a.java | not 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:99 | spoofed, 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_PICSP_KEY_LAST_TIME_BACKUPSP_KEY_STARTING_RESTORE_PROCESSSP_KEY_ORIGALY_INSTALL_VERSIONSP_KEY_WAITING_FOR_CUSTOM_THEME_PENDING_PURCHASESP_KEY_IS_MISSED_CALL_NOTIFICATION_ENABLEDSP_KEY_LEGAL_MESSAGE_SHOWN_V2SP_KEY_PREMIUM_ACTIONS_LEFTSP_KEY_CALL_RECORDING_MENU_BUBBLE_SHOWN_V1SP_KEY_SHOW_TIME_LIMIT_BANNERSP_KEY_RECORD_CALLS_MODESP_KEY_CALL_RECORDING_MODES_BUBBLE_SHOWNSP_KEY_DID_MIGRATE_TO_SCORE_STORAGESP_KEY_FIXED_RC_TIMESTAMPSP_KEY_IS_CALL_RECORDING_ACTIVITY_ENABLESP_KEY_DID_LOVE_SURVEY_SHOWNSP_KEY_IS_TOKI_ENABLEDSP_KEY_RV_APPEARANCES_BEFORE_CALLER_ID_ROLE_REQUESTSSP_SCHEDULER_POPUP_SHOWN_AMOUNTSP_KEY_DID_EYECON_CONSENT_APPROVEDSP_KEY_DA_LAST_TAGSSP_KEY_DA_LAST_BUNDLE_IDSP_REG_AUTO_START_SHOWNSP_KEY_CLICKED_ON_AUTO_START_GO_BTNSP_KEY_ORIGINALY_INSTALL_VERSIONEyecon.isNeedAutoStartcipher_keyclientIdauthenticatedClifpt_last_tracked_cisptw_tracked_beforeclicked_toki_btn_v2fb_notification_cash_v2_*fb_notification_EYECON_TYPES_v2fb_notification_timestamp_five_starsSP_KEY_SELECTED_QUOTES_LANGUAGE_v1SP_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
pNNNpackages 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:
| Bucket | Packages | Notes |
|---|---|---|
| Third-party libraries / SDKs | 177 | ExoPlayer (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 MICO | 9 | b6 (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 SDK | 2 | xs, ys — MICO wrappers around Moloco tech.crackle RTB |
| Cannot be classified confidently | 4 | p293fh, gj, g8, yi (see section 5) |
| TOTAL | 192 | 2,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 storedObject objInvoke3 = objInvoke.getClass().getMethod("getId", null).invoke(objInvoke, null);this.f39537a = (String) objInvoke3; // <-- GAID always harvestedThe 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.javaholds anAudioRecordinstance (audioRecordfield) and aRecorder Stoppedstate, alongsideamr_decoded.length,packet_jitter_avg_100, and signalling keyschannel_id,invitation_id,session_id,CHANNEL_INVITE,CHANNEL_JOIN,CHANNEL_BROADCAST_COMMAND.p633wl/C21450m.javahard-codes the app version4.2026.04.23.1145in the signalling client.p687ylimplements 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 propertiesadid,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-RBICtay1g24MGXWy8XF3vIml3_MfnmSmyuKBpT3B0dWbHRR_4cgq-gAdev-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.
2.9 LOW — Cleartext fallback for the Google Maps directions deep linkh4
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 retryIf 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 datawebView2.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_TASKcontext.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 dir | Real pkg | Files | Class | Responsibility / SDK | Notable classes & markers |
|---|---|---|---|---|---|---|
| 1 | p627wd | wd | 48 | 3P | ExoPlayer UI — RecyclerView PlayerView adapters | AbstractC21367d0 extends RecyclerView.Adapter; listens on p344ic/p365jd |
| 2 | p708zd | zd | 16 | 3P | ExoPlayer display/AVC/HEVC (Surface.setFrameRate, avc1.%02X%02X%02X) | already known (skip-list) |
| 3 | p693yr | yr | 56 | 3P | Moloco / tech.crackle SSP adapter wrappers (MICO-authored glue) | C21986f1 implements CrackleAdListener; import tech.crackle.core_sdk.ssp.SSP |
| 4 | p652xc | xc | 29 | 3P | Mintegral’s bundled ExoPlayer2 extractors (TS/ADTS/WebVTT/FLAC) | import com.mbridge.msdk.playercommon.exoplayer2... |
| 5 | p324hg | hg | 80 | 3P | Kotlin stdlib collections (Iterable, Map, List internals) | hg.a/hg.a2; uses ir.AbstractC17448b = Intrinsics |
| 6 | om | om | 69 | 3P | Ktor HTTP client core + plugins | io.ktor.client.plugins.HttpCallValidator, HttpPlainText, HttpTimeout |
| 7 | gq | gq | 79 | 3P | kotlinx.serialization-json | implements KSerializer, kotlinx.serialization.encoding.Decoder |
| 8 | ao | ao | 53 | 3P | RxJava 2 operators (PublishProcessor, Flowable, prefetch queues) | rx2.buffer-size, Prefetch queue is full?! |
| 9 | p547sp | sp | 74 | 3P | kotlinx.coroutines Flow/Channel operators | Buffer size should be non-negative, BUFFERED, or CONFLATED |
| 10 | p240d9 | d9 | 10 | 3P | AndroidX AppCompat R.attr ID table + RecyclerView/ViewGroup internal listeners | RecyclerView.OnScrollListener, ViewGroup.OnHierarchyChangeListener; the android_id hit here is an AndroidX resource-ID constant (ViewStubCompat_android_id, Fragment_android_id), not device-ID access |
| 11 | p590ur | ur | 47 | 3P | kotlinx.serialization core (descriptors, SerializersKt) | must include generic type, Dalvik/RoboVM checks |
| 12 | p653xd | xd | 30 | 3P | ExoPlayer MediaSession / playback-state (PlaybackStateCompat) | android.support.v4.media.session.PlaybackStateCompat |
| 13 | p707zc | zc | 19 | 3P | ExoPlayer MediaCodec async adapter + device quirks (baffin, c2.android.aac.decoder) | MediaCodec.Callback, ExoPlayer:MediaCodecAsyncAdapter: |
| 14 | p709ze | ze | 21 | 3P | Material Components shape drawables / tint / ripple | class C22176g extends Drawable implements TintAwareDrawable |
| 15 | p311h0 | h0 | 30 | 3P | Lottie JSON parser / composition | Lottie doesn't support expressions. / ... 3D layers. |
| 16 | p548sq | sq | 25 | 3P | OkHttp internal RealCall, connection pool, TLS | HTTP/1.1, bytes but received, CONNECT tunnels |
| 17 | p278f0 | f0 | 10 | 3P | Lottie content model (__container, PRE_COMP, images/fonts) | data URL did not have correct base64 format. |
| 18 | p532rp | rp | 22 | 3P | kotlinx.coroutines buffered channels | Buffered channel capacity must be at least 1 |
| 19 | p680yd | yd | 24 | 3P | ExoPlayer util (GL, MIME, XML pull, Exif) | com.google.android.exoplayer2.util.GlUtil$GlException |
| 20 | p229ct | ct | 32 | FP | MICO ad-mediation glue — Mintegral/Moloco RTB + banner/interstitial/native wrappers | ssp=s5&core=1.2.0&sv=, ssp=s12&..., CrackleAdListener, onShowFailRewarded= |
| 21 | p610vq | vq | 25 | 3P | OkHttp internal HTTP/2 frames | PROTOCOL_ERROR padding, :authority, |PADDED |
| 22 | p695z | z | 20 | 3P | Lottie content model | __container (2nd half of Lottie model) |
| 23 | p710zf | zf | 25 | 3P | OpenCensus / OkHttp instrumentation | Cannot initialize default OpenCensus HTTP propagation text format. |
| 24 | p257e2 | e2 | 29 | 3P | Glide downsampler, bitmap pool, video-frame decode | Downsampler.DownsampleStrategy, DfltImageHeaderParser |
| 25 | p508qq | 8 | 3P | OkHttp cache (Cache is corrupt:), interceptor, DiskLruCache | consumes MICO p033b6.C3475h HTML-text config | |
| 26 | p543s7 | s7 | 5 | FP | MICO Reminders card — persists reminder JSON in SP_KEY_EYECON2_REMINDERS | RemindersDataHandler, reminder_text, creationDate, da_card |
| 27 | p642x1 | x1 | 29 | 3P | Glide core Request/Engine/Resource | You cannot modify locked T, consider clone() |
| 28 | p452o8 | o8 | 14 | 3P | Facebook SDK core (install referrer, app events, ad attribution) | advertiser_id, advertiser_tracking_enabled, anon_id, fb_mobile_activate_app |
| 29 | p029b2 | b2 | 37 | 3P | Glide model loaders + LazyHeaders | Failed to find any ModelLoaders for model:, LazyHeaders{headers= |
| 30 | p608vm | vm | 32 | 3P | Ktor HTTP Url, media types, headers | Fail to parse url:, Transfer-Encoding, TRACE |
| 31 | p659xj | xj | 10 | 3P | Google libphonenumber (PhoneNumberMatch, country code, Externalizable) | PhoneNumberMatch [, Country Code Source: |
| 32 | p626wc | wc | 11 | 3P | iVorbis / Ogg decoder + Moloco media request glue | No ogg page can be found., adRequestId |
| 33 | p401ld | ld | 11 | 3P | ExoPlayer subtitles (application/cea-608, application/ttml+xml) | Unexpected decode error |
| 34 | p615w1 | w1 | 4 | 3P | Singular SDK device-attribution handler + ad DB | SingularDeviceAttributionHandler, kind = 1 AND image_id = ? |
| 35 | p580ud | ud | 10 | 3P | ExoPlayer cue / ASS / WebVTT styling | A style block was found after the first cue. |
| 36 | p033b6 | b6 | 26 | FP | MICO remote-config “card” schema — the JSON model that defines every on-call UI card | incall_fs_feed_admob_first_ad_id, balwan_bg_color, pre_defined_action, shouldSendName-style flags, eyecon://url?link=… |
| 37 | p253dt | dt | 21 | FP | MICO Inneractive/Fyber (DT Exchange) SSP adapter glue | InneractiveAdSpot.RequestListener, ssp=s6&core=1.2.0&sv= |
| 38 | p634wm | wm | 14 | 3P | Ktor HTTP client (multipart, header limits) | Header line length limit exceeded |
| 39 | p603vd | vd | 22 | 3P | ExoPlayer device/track selection | BRAVIA, Sony, sys.display-size, audio/ac4 |
| 40 | p293fh | p293fh | 1 | ? | Firebase/encoder-side model with address / arch / baseAddress / binaries / buildId (binary/ABI model) | only 1 class, fully read, no behavioural code |
| 41 | p396l1 | l1 | 25 | 3P | Amplitude identify + disk-backed event store | amplitude-identify-intercept-disk-queue, .tmp |
| 42 | p588up | up | 24 | 3P | kotlinx.coroutines core (Dispatchers, continuations) | Module with the Main dispatcher is missing. |
| 43 | p375k | k | 27 | 3P | Coil Compose image loading | coil.compose.requestOf (Utils.kt:21), Success(painter= |
| 44 | p635wn | wn | 3 | 3P | Moloco/Mintegral shared media-request glue | adRequestId, $adUnitInfo |
| 45 | p564tp | tp | 30 | 3P | kotlinx.coroutines Flow/Channel internals | onBufferOverflow=, AbstractSharedFlowSlot |
| 46 | p641x0 | x0 | 14 | 3P | SQLiteOpenHelper (Mintegral/Glide ad cache) + ExoPlayer glue | extends SQLiteOpenHelper, Attempt to re-create existing legacy database file |
| 47 | p002a0 | a0 | 18 | 3P | Lottie keyframes / interpolators | This animation does not support split dimensions! |
| 48 | p265eb | eb | 18 | 3P | AdMob mediation adapter: DT Exchange / Fyber Marketplace | com.google.ads.mediation.dtexchange, OnFyberMarketplaceInitializedListener |
| 49 | p272em | em | 11 | 3P | OkHttp HTTP/2 connection + HPACK | FRAME_SIZE_ERROR, Header index too large, :authority |
| 50 | p488q0 | q0 | 21 | 3P | Amplitude analytics core (internal) | heavy Amplitude dep graph |
| 51 | p030b3 | b3 | 15 | 3P | ExoPlayer core player + MediaPlayer compat + subtitle controller | CSJ_VIDEO_MEDIA, setSubtitleAnchor |
| 52 | p483pj | pj | 14 | 3P | Firebase Performance sessions + HTTP instrumentation | app_quality, sampling_rate, firebase_sessions_*, HttpsURLConnection |
| 53 | p433n8 | n8 | 9 | 3P | Mintegral (MTS) reporting parameter builder | _audiencePropertyIds, _eventName_md5, _MTSDK_Default_ |
| 54 | p334i0 | i0 | 3 | 3P | okio JsonReader (Nesting too deep, Expected a boolean but was) | okio JSON |
| 55 | p308gj | gj | 4 | ? | Small plugin/loader glue (Moloco↔Mintegral bridge), 4 classes, no behavioural logic | PackageManager use only |
| 56 | p625wb | wb | 36 | 3P | Amazon 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 |
| 57 | p373jr | jr | 19 | 3P | Joda-Time core (chronology, time zones) | centuryOfEra, Africa/Addis_Ababa |
| 58 | p003a1 | a1 | 6 | 3P | IAB 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 |
| 59 | p624wa | wa | 11 | 3P | Fyber / DT Exchange Marketplace SDK (Inneractive* listeners, initOmidSdk) | Firing Event 801 - AdLoadFailed - errorCode, IAConfigManager |
| 60 | p544s8 | s8 | 7 | FP | MICO spam-report flow classifier — regex-logger/suggested-event detector + shareSpam preference | SUGGESTED_EVENTS_HISTORY, pref_key_share_spam_time_ms, (?i)(confirm.*password)|(password.*confirm) |
| 61 | p426n | n | 13 | 3P | Coil Android resource/asset fetchers (+ SHA-256 header hash) | Invalid android.resource URI:, HTTP , SHA-256 |
| 62 | p436nd | nd | 9 | 3P | ExoPlayer Mp4Webvtt / cue parsing | Incomplete Mp4Webvtt Top Level box header found. |
| 63 | p443o | o | 10 | 3P | Coil core (disk-cache key, fetcher factory, request interceptor) | coil#disk_cache_key, coil#is_sampled |
| 64 | p459og | og | 9 | 3P | Firebase core (ComponentDiscovery, BackgroundDetector, StartupTime) | Make sure to call FirebaseApp.initializeApp(Context) first. |
| 65 | p369jj | jj | 13 | 3P | Firebase Remote Config (fetch/ETag/backoff, /namespaces/) | abt_experiments_key, last_fetch_etag, Fetch is throttled. |
| 66 | p306g8 | g8 | 5 | ? | 5 tiny classes, no strings, no APIs — glue/model only | depends on p358j5, p675y8 |
| 67 | p331hq | hq | 24 | 3P | kotlinx.serialization-json internals (JsonLiteral, primitives) | Expected input to contain a single valid number, but got |
| 68 | p579uc | uc | 8 | 3P | ExoPlayer ID3 TLEN frame reader | Searched too many bytes. |
| 69 | p666xs | xs | 9 | FP-glue | MICO Moloco RTB wrapper (banner/interstitial/rewarded listeners) | CrackleRtbInterstitialAdListener, errMsg |
| 70 | p467or | or | 11 | 3P | Joda-Time tz data provider | org/joda/time/tz/data, Corrupt zone info map |
| 71 | p667y0 | y0 | 6 | 3P | Amplitude core + session-replay plugin bridge | plugin-session-replay-android/0.20.12, 9774d56d682e549c, adid |
| 72 | p687yl | yl | 13 | 3P | Voice/RTP audio transport — jitter buffer, FEC, keep-alive UDP socket | send Keep Alive, socket connected:, fec none, close UDPSocket |
| 73 | p633wl | wl | 14 | 3P | Opus codec + voice-channel signalling; holds an AudioRecord | channel_id, invitation_id, CHANNEL_INVITE, amr_decoded.length, 4.2026.04.23.1145 |
| 74 | p354j0 | j0 | 9 | 3P | Lottie offscreen renderer + animator | lottie-…-thread-, Cannot nest start() calls |
| 75 | p719zp | zp | 10 | 3P | okio segments/buffers/sinks | only owner can write, Source is closed. |
| 76 | p721zs | zs | 12 | 3P | Google Mobile Ads custom-format load callbacks (AppOpen/Interstitial/Rewarded/AdManager) | extends AppOpenAd.AppOpenAdLoadCallback |
| 77 | p439nk | nk | 8 | 3P | ZXing barcode encoders (EAN/UPC/CODE_128) | Can only encode EAN_13, but got |
| 78 | p353j | j | 11 | 3P | Coil image cache + request interceptors | image_cache |
| 79 | p411lp | lp | 19 | 3P | kotlinx.coroutines sequences | This sequence can be consumed only once. |
| 80 | p335i1 | i1 | 11 | 3P | Amplitude event pipeline (storage, retry, enrichment) | Event storage file not found:, #!maxRetryAttemptReached |
| 81 | p243dc | dc | 9 | 3P | Firebase JobScheduler transport backend (uploads metrics/logs) | SELECT next_request_ms FROM transport_contexts, FirebaseCrashlytics |
| 82 | p513r2 | r2 | 13 | 3P | AndroidX core (ArrayMap, IoUtils, executors) | extends ArrayMap, File too large to map into memory |
| 83 | p549t | t | 15 | 3P | Coil request/lifecycle (GlobalLifecycle) | coil.request.GlobalLifecycle |
| 84 | p498qd | qd | 5 | 3P | ExoPlayer ASS/SRT cue parsing | [V4+ Styles], Failed to parse color expression: |
| 85 | p413m | m | 8 | 3P | DiskLruCache (okhttp-style) | cache is closed, directory == null |
| 86 | p310go | go | 18 | 3P | RxJava 2 schedulers/plugins | rx2.computation-threads, RxCachedThreadSchedulerShutdown |
| 87 | p450o6 | o6 | 3 | FP | MICO navigation-app chooser (Google Maps / Waze / Sygic) | EyeBottomNavigateDialog, google.navigation:q=, com.sygic.aura://, cleartext http://www.google.com/maps/dir/ fallback |
| 88 | p289fd | fd | 3 | 3P | mp4/ID3 metadata writer (TXXX, image/jpeg) | AbstractC16257t1 |
| 89 | p028b1 | b1 | 7 | 3P | Amplitude core — device/user properties incl. android_id, adid, carrier, city | amplitude-android-, amplitude-kotlin- |
| 90 | p551t1 | t1 | 4 | 3P | Glide GIF header parser (NETSCAPE2.0) | You must call setData() before parseHeader() |
| 91 | p428n2 | n2 | 4 | 3P | Glide Resource / RequestManager | To indicate failure return a null Resource object |
| 92 | p480pc | pc | 8 | 3P | ExoPlayer AVI extractor | AVI Header List not found, video/mp42 |
| 93 | p346ig | ig | 9 | 3P | Guava BaseEncoding + ByteStreams | Illegal alphabet length |
| 94 | p454ob | ob | 13 | 3P | AdMob mediation adapter: Pangle (ByteDance) | PAGAppOpenAdLoadListener, MediaOpenAd |
| 95 | p336i2 | i2 | 9 | 3P | Glide GIF drawable | BufferGifDecoder, StreamGifDecoder |
| 96 | p520rb | rb | 8 | 3P | AdMob bidding adapter: Liftoff Monetize (Vungle) | getBidResponse(...), getWatermark(...) |
| 97 | p593v1 | v1 | 13 | 3P | Glide data fetchers — incl. ContactsContract photo fetcher and HttpUrlFetcher | ContactsContract.Contacts.openContactPhotoInputStream, contacts/lookup/*, phone_lookup/* |
| 98 | p617w3 | w3 | 7 | 3P | Glide scheduling/CronField helpers | CronField does not belong to day of week |
| 99 | p679yc | yc | 5 | 3P | ExoPlayer WAV reader + block alignment | Unsupported or unrecognized wav file type. |
| 100 | p323hf | hf | 16 | 3P | Google Play Core In-App Review / App Update / request monitor | error requesting in-app review for %s, AppUpdateService |
| 101 | p537s1 | s1 | 6 | 3P | Glide disk cache (glide-disk-lru-cache-thread) | LineReader is closed |
| 102 | p451o7 | o7 | 4 | FP | MICO feature config — cantalk_c/cantalk_n/cantalk_q, authentication, new_photo, share_media, call-log card | n_pics, callLog, eyecon://share, received |
| 103 | p503ql | ql | 5 | 3P | Glide animated AppCompatImageView / GifDrawable | Animation duration cannot be negative value. |
| 104 | p629wg | wg | 11 | 3P | AndroidX / Guava SequentialExecutor, ScheduledExecutorService | Shutting down is not allowed. |
| 105 | p042c2 | c2 | 7 | 3P | Glide HttpGlideUrlLoader + MediaStore fetcher | HttpGlideUrlLoader.Timeout, ACCESS_MEDIA_LOCATION |
| 106 | p521rc | rc | 4 | 3P | ExoPlayer MP3/ID3 (filepositions, keyframes) | Audio format not supported: |
| 107 | p565tq | tq | 7 | 3P | OkHttp interceptor (100-continue, date parsing) | 100-continue, EEE dd MMM yyyy HH:mm:ss z |
| 108 | p276eq | eq | 12 | 3P | kotlinx.serialization descriptors | SerialDescriptor, descriptor, serialName |
| 109 | p374js | js | 3 | 3P | Moloco banner WebView — custom WebView + WebViewClient for click/video tracking | CLICK_BANNER_EXCEPTION, clickEndCard, beginVideo( |
| 110 | p287fb | fb | 14 | 3P | AdMob mediation adapter: InMobi (banner/native, marketplace init) | InMobi SDK failed to download native ad image assets. |
| 111 | p663xp | xp | 11 | 3P | kotlinx.coroutines sync (Mutex, Semaphore) | Semaphore should have at least 1 permit |
| 112 | p349ij | ij | 6 | 3P | Firebase Installations (FID: last_fetch_status, fetch_timeout_in_seconds, _fpc) | firebaseinappmessaging-adjacent FID store |
| 113 | p651xb | xb | 17 | 3P | Firebase Crashlytics metrics proto (appNamespace, logSourceMetrics, eventsDroppedCount) | Base64.encodeToString(bArr, 2) |
| 114 | p618w4 | w4 | 4 | FP | MICO AdMob revenue wrapper — maps ad types to AdRevenueEvent$AdType | getAdType$app_googleRelease()Lcom/eyecon/global/AdsIB/AdRevenueEvent$AdType;, gam_ad_types_loading_mode |
| 115 | p440nm | nm | 8 | 3P | OkHttp Android engine bridge | couldn't send a body with the [Android] engine. |
| 116 | p296fq | fq | 4 | 3P | kotlinx.serialization Decoder/Encoder interfaces | 'null' is not supported by default |
| 117 | p041c1 | c1 | 8 | 3P | Facebook SDK Core + ConnectivityManager.NetworkCallback monitor | AndroidNetworkListener, onNetworkAvailable., com.facebook.AccessTokenManager.SharedPreferences |
| 118 | p255e0 | e0 | 16 | 3P | Lottie model (ShapePath, MergePaths, TrimPath) | Animation contains merge paths but they are disabled. |
| 119 | p589uq | uq | 6 | 3P | OkHttp HTTP/1 codec + chunked writer | Duplex connections are not supported for HTTP/1 |
| 120 | p378k2 | k2 | 11 | 3P | Meta Audience Network RM plugin + fragment (SupportRMFragment, ConnectivityMonitor) | You cannot start a load for a destroyed activity |
| 121 | p583ui | ui | 6 | 3P | Firebase Performance FrameMetricsAggregator / fragment monitor | Failed to record frame data for %s., _tsns |
| 122 | p377k1 | k1 | 5 | 3P | Amplitude identify interceptor ($identify, $groupidentify) | Event is invalid for missing information like userId and deviceId. |
| 123 | p658xi | xi | 2 | 3P | Amplitude prefs-backed logger/SuccessContinuation multiplexer | amplitude-events- |
| 124 | p712zh | zh | 15 | 3P | Firebase In-App Messaging display (FirebaseInAppMessagingDisplay) + metrics | FIAM.Display, BANNER_PORTRAIT/CARD_PORTRAIT |
| 125 | p355j1 | j1 | 9 | 3P | Amplitude event builder ($clearAll, $set, $unset) | $groupidentify |
| 126 | p655xf | xf | 4 | 3P | Mintegral / Google API-client consent plumbing | com.google.api.client |
| 127 | p668y1 | y1 | 10 | 3P | Glide bitmap pool (LruBitmapPool, array pools) | Cannot pool recycled bitmap |
| 128 | p595v3 | v3 | 2 | 3P | Small preconditions/interval utility (Glide-ish) | The end instant must be greater or equal to the start |
| 129 | p497qc | qc | 2 | 3P | ExoPlayer FLAC extractor | Failed to read FLAC stream marker. |
| 130 | p694ys | ys | 4 | FP-glue | MICO Moloco SSP wrapper (ssp=s13) | ssp=s13&av=1.2.0.0 |
| 131 | p692yq | yq | 5 | 3P | OkHttp AndroidPlatform (SSL socket factory, trust roots, CloseGuard) | buildTrustRootIndex, .OpenSSLSocketFactoryImpl |
| 132 | p039bp | bp | 11 | 3P | okio Path/FileSystem | Check failed. |
| 133 | p657xh | xh | 8 | 3P | Firebase In-App Messaging click/view metrics logger | message click to metrics logger, FIAM.Display |
| 134 | p409ln | ln | 15 | 3P | OpenCensus tracing (SpanContext, TraceComponentImpl) | Couldn't load lite implementation for TraceComponent |
| 135 | p568u0 | u0 | 4 | 3P | Amplitude session-replay UI element/screen tracking (GestureDetector) | [Amplitude] Element Interacted, [Amplitude] Hierarchy |
| 136 | p407ll | ll | 11 | 3P | Glide animated drawable / GIF plumbing | consumes p503ql |
| 137 | p600v8 | v8 | 5 | 3P | Facebook Core Diagnostics (analysis_log_, /instruments) | app_version, callstack, device_model |
| 138 | p242db | db | 5 | 3P | AdMob mediation adapter: Meta Audience Network (banner/interstitial/native) | Ad from Meta Audience Network doesn't have all required assets. |
| 139 | p239d8 | d8 | 4 | 3P | Fyber / Inneractive marketplace SDK internals | InneractiveMediationDefs.GENDER_FEMALE |
| 140 | sk | sk | 2 | 3P | zlib/PNG CRC + scanline helper (media image decoder) | Invalid mask pattern, 0 polynomial |
| 141 | p660xm | xm | 6 | 3P | Ktor CIO buffers/char-array pooling | ktor.internal.cio.disable.chararray.pooling |
| 142 | p000 | p000 | 2 | 3P | Amplitude Session Replay (rrweb screen recorder) | com.amplitude.android.sessionreplay.config.ConfigManager, rrweb events. |
| 143 | p368ji | ji | 13 | 3P | Firebase In-App Messaging rate limiting / campaign store | APP_FOREGROUND_ONE_PER_DAY_LIMITER_KEY |
| 144 | p329ho | ho | 4 | 3P | RxJava 2 core reactive interfaces | 2.12 violated: onSubscribe must be called at most once |
| 145 | p294fl | fl | 7 | 3P | Fyber/Inneractive identity token client — AES/GCM/NoPadding + HttpsURLConnection | Refresh token expired, optout, Identity refreshed |
| 146 | p051cj | cj | 7 | 3P | Firebase component registry / provider discovery | Firebase Background/Blocking/Lite/Scheduler |
| 147 | p628we | we | 8 | 3P | Material Components text-input / theme attrs | Theme.MaterialComponents theme error |
| 148 | p509qr | qr | 3 | 3P | SLF4J ServiceLoader provider lookup | does not implement SLF4JServiceProvider interface |
| 149 | p613w | w | 13 | 3P | Glide util / device-config tables | A3_Pro, A30ATMO, A6L-C, /proc/self/fd |
| 150 | p682yg | yg | 4 | 3P | Firebase Crashlytics thread-check / ANR monitor | FirebaseCrashlytics, interestedThreads |
| 151 | p256e1 | e1 | 3 | 3P | Singular SDK device-info collector — GAID (reflection, ignores LAT), App Set ID, GPS-derived country, TelephonyManager | getAdvertisingIdInfo, getAppSetIdInfo, getLastKnownLocation, isLimitAdTrackingEnabled (result discarded) |
| 152 | p535s | s | 5 | 3P | OkHttp cache-control (Date, Expires, If-None-Match) | Unexpected header: |
| 153 | p525rh | rh | 7 | 3P | Firebase protobuf @Protobuf annotation encoders | @com.google.firebase.encoders.proto.Protobuf(tag= |
| 154 | p501qh | qh | 5 | 3P | Firebase encoders core (ValueEncoderContext) | Cannot encode a second value in the ValueEncoderContext |
| 155 | p533rq | rq | 3 | 3P | Guava TaskRunner | Check failed. |
| 156 | p350ik | ik | 6 | 3P | ZXing Code128 writer | Can only encode CODE_128, but got |
| 157 | p326hi | hi | 9 | 3P | Firebase In-App Messaging API client | firebaseinappmessaging.googleapis.com, X-Goog-Api-Key, X-Android-Cert |
| 158 | p559te | te | 10 | 3P | Material bottom-sheet behaviour + animator | OnBackAnimationCallback |
| 159 | p271ek | ek | 5 | 3P | Firebase In-App Messaging protobuf campaign messages | campaignId_, triggeringConditions_, experimentPayload_ |
| 160 | p384kb | kb | 4 | 3P | AdMob mediation adapter: Mintegral (AppOpen, native w/ code) | Mintegral SDK failed to return a native ad. |
| 161 | p299g1 | g1 | 8 | 3P | Amplitude session store (last_event_id, opt_out) | app_build, previous_session_id |
| 162 | p612vs | vs | 10 | 3P | AdMob native mediation adapter (mintegral/moloco family) | NativeAdMapper, NativeAd.Image |
| 163 | p228cs | cs | 6 | 3P | Viewability/OMID measurement — WebChromeClient, ViewabilityRule, HTTP fetch | persistent-storage-current-file-uri, HttpURLConnection |
| 164 | p530rm | rm | 9 | 3P | Ktor server engine attributes (HttpRequestData, HttpResponseData) | io.ktor.util.AttributeKey |
| 165 | p496qb | qb | 2 | 3P | AdMob adapter base: Liftoff Monetize (Vungle) | Failed to load bidding banner ad from Liftoff Monetize. |
| 166 | p536s0 | s0 | 5 | 3P | Compose UI test / view-hierarchy finder | Error while locating target in view hierarchy: |
| 167 | p227cq | cq | 9 | 3P | kotlinx.serialization polymorphic serializers | kotlinx.serialization.PolymorphicSerializer(baseClass: |
| 168 | p713zi | zi | 5 | 3P | Firebase Performance trace + URL sampling | getResultUrl throws exception, firebase_performance_whitelisted_domains |
| 169 | p505qn | qn | 15 | 3P | RxJava 2 core (Flowable, Observable, Scheduler) | rx2.buffer-size, rx2.scheduler.drift-tolerance |
| 170 | p297fs | fs | 2 | 3P | Ad impression/creative model (imp_url, creative_width, q1_url) | ad-server response model |
| 171 | p403lf | lf | 9 | 3P | Play Core install-time service binding (linkToDeath, binder death) | PhoneskyVerificationUtils |
| 172 | p602vb | vb | 3 | 3P | Firebase CCT/Crashlytics backend with obfuscated endpoint table + TelephonyManager | hts/frbslgiggolai.o/0clgbthfra=snpoo, LegacyFlgDestination |
| 173 | p245dg | dg | 5 | 3P | org.json (JSONTokener, JSONStringer) | Token is not a number |
| 174 | p292fg | fg | 3 | 3P | OkHttp canonicalisation / Escaper | Alphanumeric ASCII characters are always 'safe' |
| 175 | p270ej | ej | 7 | 3P | AndroidX Compose frame metrics (_fr_fzn, _fr_slo, _fr_tot) | ViewTreeObserver.OnDrawListener |
| 176 | p328hl | hl | 5 | 3P | Fyber/Inneractive identity store | uid2_identity.json, euid_identity.json, identity_status |
| 177 | p522rd | rd | 1 | 3P | ExoPlayer TTML cue parser | Skipping invalid index:, <br> |
| 178 | p701z5 | z5 | 1 | FP | MICO card-impression RecyclerView adapter | onCardSeen position = %s, data = %s, lifecycle-gated attach/detach logging |
| 179 | p361j8 | j8 | 2 | 3P | Google Play services Measurement (gps_ara_* outcome receiver) | Failed to get measurement manager |
| 180 | p382k8 | k8 | 2 | 3P | Mintegral user-info / protected-audience collection | /privacy_sandbox/pa/logic, ?daily&app_id=, ?trusted_bidding |
| 181 | p461oj | oj | 5 | 3P | Firebase Crashlytics session + component dependency graph | Enum: CRASHLYTICS / MATT_SAYS_HI / PERFORMANCE |
| 182 | p370jk | jk | 3 | 3P | Android graphics Rect/Matrix + a BaseEncoding charset enum | Num bits must be between 0 and 32 |
| 183 | p049cb | cb | 4 | 3P | AdMob adapter: Meta Audience Network (init + rewarded) | AudienceNetworkAds.InitListener |
| 184 | p456od | od | 2 | 3P | ExoPlayer ID3 frame | extends AbstractC18336f |
| 185 | p684yi | yi | 3 | ? | OkHttp method-set wrapper over Firebase-Perf/Amplitude classes; no distinct behaviour | CONNECT/DELETE/HEAD/OPTIONS |
| 186 | p387kf | kf | 3 | 3P | Play Core in-app review + hardcoded Play cert fingerprints | 8P1sW0EPJcslw7UzRsiXL64w-O50Ed-RBICtay1g24M |
| 187 | p616w2 | w2 | 7 | 3P | ExoPlayer SurfaceView/TextureView wrappers | SurfaceHolderCallback, TextureViewSurfaceTextureListener |
| 188 | p507qp | qp | 4 | 3P | kotlinx.coroutines Dispatchers.Main | The main looper is not available |
| 189 | p298g0 | g0 | 5 | 3P | Lottie network cache (gzip/zip decoding) | lottie_cache_, lottie_network_cache, application/gzip |
| 190 | p248dl | dl | 5 | 3P | Fyber/Inneractive identity package model | advertising_token, refresh_token, Phone(phone= |
| 191 | p495q8 | q8 | 4 | 3P | Facebook Core ReceiverService / custom events | fb_mobile_purchase, CUSTOM_APP_EVENTS |
| 192 | p569u1 | u1 | 11 | 3P | Glide 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:
| Class | Card type | Notable remote keys |
|---|---|---|
C3468a | Ad feed card | disabled_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 |
C3469b | Image/lottie card | background, foreground_lottie, isNewUserCard |
C3470c | Caller/date card | balwan_bg_color, balwan_color, dateCall, pre_defined_action |
C3475h | HTML text block | is_html_text, text_color, horizontal_frame_percent, headers, referrer, size |
C3478k | Deep-link card | eyecon://url?link=https://faq.eyecon-app.com/, eyecon://url?link=https://player.vimeo.com/video/7462615…?h=ee63579ec3 |
C3479l | Yes/no question card | question, yes_button, no_button, use_photo_button, change_photo |
C3483p | Reminder card | add_first_reminder_button, content_reminder, bell_lottie |
C3484q/C3485r | Incoming-call answer screen | nameText, phoneText, messageText, bottomMessageText, first_place_bar_color |
C3486s, C3492y | Top-3 callers / podium | first_place_name, first_place_number, second_place_number, third_place_name |
C3487t | Contact stat | contact_name, value_name |
C3491x | Pie chart | loser_bubble_color, pie_winner_color, loser |
C3493z | Free-form HTML | change_display_style, contentHtml, cors |
EnumC3480m | Card trigger enum | autostart, 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
C20076aholds 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/C20079dpersist predicted events intoshardPreferenceskeySUGGESTED_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 NAMEarrayList.add(new C21872a("cli", buttonText)); // reported CALLER IDC21273a.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; implementsCrackleAdListener,CrackleUserRewardListener,BidListennning,NewInterstitialListener,MBSplashLoadListener,BannerAdListener,NativeListener.NativeAdListener.dt:ssp=s6&core=1.2.0&sv=; implementsInneractiveAdSpot.RequestListener,InneractiveAdViewEventsListenerWithImpressionData,InneractiveFullscreenAdEventsListenerWithImpressionData.C15763d0/C15767f0/C15777n/C15778osurface Moloco errors asonShowFailInterstitial=,onShowFailRewarded=,onShowFailRewardedInterstitial=.C15785vreads a bid token (p0.bidToken) and logsHas 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 tocom.google.android.apps.maps - Waze:
https://waze.com/ul?navigate=yes&q=…scoped tocom.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 tohttp://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_qo7.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:
| Field | Source |
|---|---|
f39537a | Google Advertising ID via reflection; on Build.MANUFACTURER == "Amazon" uses Settings.Secure advertising_id |
f39538b | Country code — Geocoder reverse-geocode of getLastKnownLocation(), else TelephonyManager.getNetworkCountryIso() when getPhoneType()==2, else Locale.getCountry() |
f39539c | PackageManager.getPackageInfo(...).versionName |
f39540d..f39543g | Build.VERSION.RELEASE, BRAND, MANUFACTURER, MODEL |
f39544h | TelephonyManager.getNetworkOperatorName() |
f39545i | device Locale.getLanguage() |
f39546j | Google 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:
isLimitAdTrackingEnabled()is read and then thrown away (§2.1).- The location branch only executes when
e1.b.f39549bis true, butf39550c(collect GAID) andf39551d(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 countThere 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 outC16654g : ResponsePackage(identity=…)EnumC16652e : success | unauthorized | invalid_token | expired_token | optout | client_errorfl.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
| Package | WebView role |
|---|---|
a1 | Loads 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). |
cs | Viewability/OMID measurement: WebChromeClient, ViewabilityRule(eventId=…, requiredPercentage=…, requiredDurationMs=…), and a raw HttpURLConnection fetch. |
js | Moloco’s banner WebView: custom WebView subclass + WebViewClient implementing CLICK_BANNER_EXCEPTION, clickEndCard, beginVideo(. Note adData, changedView, bannerWebViewListener are exposed as fields. |
p000 | Amplitude 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:
omincludesio.ktor.client.plugins.HttpPlainTextandDefaultRequest— the Ktor stack is capable of cleartext HTTP. Whether MICO’s own code ever selectsHttpPlainTextcannot 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.omalso containsDefaultResponseValidation/HttpCallValidatorwith" because of security downgrade"— again a capability, not a call site.
5. Packages I could not classify confidently, and whyh3
| Package | Why | What 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) andp707zc(zc) are in the briefing but are already-classified ExoPlayer packages (they were named in the skip list). I re-verified them:zd=Surface/Displayvideo-output and AVC/HEVC config parsing;zc=MediaCodecasync 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 + rawHttpURLConnection+ 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:
- fully enumerated for file count and
renamed from(real package name), - scanned for its complete external import graph (all non-
android*/java*/kotlin*/pNNNimports — this names the owning SDK unambiguously for most packages), - 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), - 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/hashCodeboilerplate,Parcelableproxies) 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) andlq(ShortcutBadger launcher-badge adapters, parameterised oncom.eyecon.global.Others.MyApplication).
| Bucket | Packages | Notes |
|---|---|---|
| Third-party SDK / library code | 176 | see the classification table (§4) for the owning SDK per package |
| FIRST-PARTY MICO | 2 | i5 (IAP purchased-product cache, called from a7), lq (ShortcutBadger badge adapters, typed on MyApplication) |
| Dead code / unreachable stubs | see §6 | dominated by throw new UnsupportedOperationException() synthetic abstract /* synthetic */ classes that R8 failed to inline |
| Cannot be classified confidently | 3 | uh, un-adjacent orphans and p471p2 — see §5 |
| TOTAL | 193 | 472 .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; \}thenMyApplication.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.javacase 18 — iteratesc16708j3.f41475j == 1rows and callsc17151a.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 keySo 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) -> GZIPThe 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 pkgpi) — Firebase common (called fromp459og/C19129g.javaandC19125c.java, i.e. the Firebase/GMS common component used bytiFirebase-Perf andxgCrashlytics). ReadsContextCompat.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 thefirebase_data_collection_default_enabledflag from prefs, falling back toPackageManager.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")plusm34899b()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.f47109cis the{"*","FCM","GCM",""}mime array. Auth/refresh tokens live inmi.a’sPersistedInstallationEntry\{firebaseInstallationId, registrationStatus, authToken, refreshToken, expiresInSecs, tokenCreationEpochInSecs, fisError\}.bk(p037bk) — Glide disk-cache metadata reader.C3582b.m3798a(InputStream)builds anew ObjectInputStream(inputStream)andreadExternal()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 aObjectInputStreamsink and worth recording given theObjectInputStream.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 sSocketTimeoutExceptionceiling.i(p333i) —AbstractC17126ais a dead stub: both methods (execSQL(SupportSQLiteDatabase,String,Object[]),isOpen) were reduced by R8 tothrow new UnsupportedOperationException()/return false. Live SQLite work is inandroidx.sqlite/ Room elsewhere.g2(p300g2) — Glideandroid.resource:/// asset loaders; callscontext.createPackageContext(authority, 0)on a URI authority taken from an image URL (Glide’s known behaviour), andResources.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, viaNsdManager.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 ifC9075b0.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()walksActivityManager.getProcessesInErrorState(), filterscondition == 2 && uid == Process.myUid(), then serialises the entire main-threadStackTraceElement[]into aJSONArrayand ships{shortMsg, JSONArray-of-stacks}viaAbstractC16137c.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/Lenovocom.zui.launcher(content://com.android.badge/badge), and an AOSPcontent://com.android.launcher3.cornermark.unreadbadgesetAppUnreadCountvariant whosegetSupportedLaunchers()returnsnew ArrayList(0)(see §6 — unreachable). All four takecom.eyecon.global.Others.MyApplicationas the context parameter, so this is the one package where R8 narrowed a third-party API signature to MICO’s ownApplicationsubclass.rn(p531rn) — RxJava2 main-threadScheduler(HandlerScheduler-equivalent,C19964c/C19966e/RunnableC19965d), used broadly.yn(p689yn) — RxJava2Disposables(EmptyDisposable,DisposedDisposable), plusOnErrorNotImplementedExceptionrouting.zn(p717zn) — RxJava2CompositeDisposable-equivalent (C22291a extends AtomicIntegerwith a compositeC21168c),Runnable/CallableTaskwrappers, andCompletable-esque sources.rn/zn/j9/s9/k9/b9/on/d1-styleswitch(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 pkg | Disk dir | Files | Class | One-line responsibility | Notable classes |
|---|---|---|---|---|---|---|
| 1 | oc | p455oc | 1 | TP Mintegral/ExoPlayer fork | AMR-NB / AMR-WB Extractor (magic #!AMR / #!AMR-WB, frame-size tables) | C19118a |
| 2 | fe | p290fe | 5 | TP Google Material | AppBarLayout.BaseBehavior / ScrollingViewBehavior inner classes + a11y delegate | C16533a, AbstractC16535c, AbstractC16536d |
| 3 | vj | p606vj | 3 | TP Gson 2.10.1 | ReflectAccessFilterHelper + java.lang.reflect Record support (isRecord/getRecordComponents) | AbstractC21068c, C21067b |
| 4 | kl | p390kl | 2 | TP Liftoff/Vungle | AdMob mediation BaseAdListener shims (interstitial / app-open / banner / rewarded) | C17854a, C17855b |
| 5 | g2 | p300g2 | 2 | TP Glide | ResourceUriLoader (android.resource://) + drawable→bitmap resource wrapper | C16694a, C16695b |
| 6 | uj | p584uj | 1 | TP util | ISO-8601 yyyy-MM-dd'T'HH:mm:ss[.SSS]X date parser (UTC fixed) | AbstractC20748a |
| 7 | ib | p343ib | 6 | TP InMobi | AdMob mediation adapters (banner / interstitial / native / rewarded) — setKeywords(""), load() | C17199a…C17204f |
| 8 | m1 | p415m1 | 7 | TP Google Measurement | Analytics transport error payloads: BAD_REQUEST (events_with_invalid_fields, silenced_devices), TOO_MANY_REQUESTS (throttled_devices/users/events), PAYLOAD_TOO_LARGE, FAILED | C18474a, EnumC18476c |
| 9 | a2 | p004a2 | 3 | MERGE | One Thread class with a 5-way switch: Glide executor (priority 9 + StrictMode.detectNetwork().penaltyDeath()), Coil, ExoPlayer:SimpleDecoder, Amplitude timeline, androidx.work | C0026a, ExecutorServiceC0028c, ThreadFactoryC0027b |
| 10 | le | p402le | 8 | TP Google Material | CarouselLayoutManager ItemDecoration + LinearSmoothScroller + card-size metrics | C18342a…C18349h |
| 11 | mi | p424mi | 2 | TP Firebase Installations | PersistedInstallationEntry (FID, auth/refresh tokens, expiry) + GCM RSA pubkey→SHA-1 cert prefs | C18592a, C18593b |
| 12 | he | p322he | 2 | TP Google Material | SwipeDismissBehavior ViewDragHelper.Callback + AccessibilityViewCommand | C16966a, C16967b |
| 13 | ya | p677ya | 4 | TP Fyber FairBid | SDK request wrapper for OfferWall ("ofw") and video-caching ("vcs") flows | AbstractC21893d, C21891b, EnumC21892c |
| 14 | ip | p352ip | 5 | TP Kotlin stdlib | kotlin.random.Random bridge onto java.util.Random + writeReplace serialization proxy | AbstractC17413a, C17416d |
| 15 | yb | p678yb | 6 | TP Google DataTransport | CCT backend registry (BackendResponse{status,nextRequestWaitMillis}, CreationContext) | C21894a, C21895b, C21898e |
| 16 | vf | p604vf | 4 | TP Mintegral | HTTP request builder (POST, 10 MB cap, Range: bytes */*, 5xx-retrying ServerCallback) | C20993c, C20994d |
| 17 | j9 | p362j9 | 4 | TP Fyber FairBid | Synthetic switch dispatchers for Banner/Interstitial/Rewarded/OfferWall + Android-8 IAB intent fallback | C17542a…C17545d |
| 18 | z3 | p699z3 | 2 | TP Digital Turbine Ignite | AIDL IIgniteServiceCallback.Stub subclasses: resolve data.token; decrypt & persist one DT to prefs "odt" | BinderC22067a, BinderC22068b |
| 19 | wj | p632wj | 3 | TP Gson | java.sql.Date/Time/Timestamp TypeAdapter ("MMM d, yyyy", "hh:mm:ss a") | C21435a, AbstractC21437c |
| 20 | kd | p386kd | 2 | TP ExoPlayer / Mintegral | AdPlaybackState + AdGroup (ad-loader bookkeeping) | C17837a, C17838b |
| 21 | pb | p479pb | 5 | TP Liftoff/Vungle | Vungle AdMob adapter init queue; sets VungleWrapperFramework.admob integration 7.7.0.1_7_7_0_1; COPPA mapping | C19334c, C19336e, InterfaceC19333b |
| 22 | w8 | p622w8 | 3 | TP Facebook | getProcessesInErrorState() main-thread stack-trace uploader + 5× ANR-trace uploader ("anr_reports") | AbstractC21297a, AbstractC21299c |
| 23 | dr | p251dr | 1 | TP OkHttp | HttpUrl.canonicalize percent-encoding (%XX, +) | AbstractC15999a |
| 24 | e3 | p258e3 | 2 | TP Pangle/CSJ (ByteDance) | Video preload manager — HTTP RANGE pre-warm of the ad video file | AbstractC16094a, C16095b |
| 25 | v0 | p592v0 | 5 | TP Amplitude Session Replay | TargetLocator registry — reflective probe for androidx.compose.ui.node.Owner | C20846a, C20848c |
| 26 | rg | p524rg | 3 | TP Google Measurement | zzlm interface adapter + GRANTED/DENIED and AD_STORAGE / ANALYTICS_STORAGE / AD_USER_DATA / AD_PERSONALIZATION enums | C19912c, EnumC19910a, EnumC19911b |
| 27 | u | p567u | 7 | TP Coil | Size/Dimension/SizeResolver (screen-size resolver) | C20549a, C20551c, C20554f |
| 28 | ks | p393ks | 2 | TP Moloco | AES/GCM/NoPadding + GZIP + Base64(NO_WRAP) request codec, 12-byte SecureRandom IV | C18251a, C18252b |
| 29 | ub | p578ub | 7 | TP Amplitude | Typed Event{code,payload,priority,productData} + ProductData{productId} + Encoding{name} + Priority enum | C20710a, EnumC20713d |
| 30 | r | p510r | 8 | TP Coil | MemoryCache (LruCache subclass, Value(bitmap,extras), low-RAM % heuristics) | C19805a, C19809e, InterfaceC19812h |
| 31 | zn | p717zn | 4 | TP RxJava2 | CompositeDisposable (AtomicInteger) + Task/Runnable/Callable wrappers + no-op task | C22291a, C22292b, C22293c |
| 32 | wr | p639wr | 25 | TP Retrofit 2 | The complete @interface annotation set (see §2.5) | InterfaceC21493a…InterfaceC21517y |
| 33 | o1 | p445o1 | 4 | TP Amplitude | Identity(userId,deviceId) + IdentityConfiguration(instanceName, apiKey, experimentApiKey, identityStorageProvider, storageDirectory, fileName) | C19042a, C19043b |
| 34 | h1 | p312h1 | 4 | TP Amplitude | $identify event model + $set/$setOnce/$add/$append/$clearAll/$prepend/$unset/$preInsert/$postInsert/$remove | C16908a, EnumC16910c |
| 35 | mj | p425mj | 5 | TP Firebase In-App Messaging | FIAM Rollouts: RolloutAssignment{rolloutId,variantId,parameterKey,parameterValue,templateVersion} | C18594a, C18596c, C18597d |
| 36 | z1 | p697z1 | 6 | TP Glide | ArrayPool-style cache + disk-cache key digest (MessageDigest→uppercase hex, 1000-entry LRU) + low-RAM pool sizing | C22059f, C22057d |
| 37 | s3 | p539s3 | 7 | MERGE (TP) | Quartz-style cron field value model (L, W, LW, a#b, ?) hoisted into a shared package; consumed by Google Measurement, Firebase, Facebook and WorkManager | AbstractC20038e, C20039f, C20037d |
| 38 | lq | p412lq | 9 | TP ShortcutBadger (+ FP types) | Launcher app-badge adapters for OPPO / vivo / ZUI / AOSP cornermark, all parameterised on MyApplication | C18439d…C18444i |
| 39 | di | p247di | 1 | TP Google Material | com.google.android.material.internal.ModalLayout (max width/height % from ModalLayout attrs) | AbstractC15951a |
| 40 | e9 | p264e9 | 3 | TP recyclerviewfastscroll | SectionIndexer/AbstractSectionIndexer base + animator-end listener | AbstractC16147b, C16146a |
| 41 | d0 | p231d0 | 5 | TP Lottie | Path keyframe Keyframes/KeyframeParser data objects (merged with a Facebook holder) | C15791a…C15794d |
| 42 | ti | p563ti | 4 | TP Firebase Performance | Perf component factory (reads metaData, Remote Config + SessionManager wiring) | C20461b, C20463d |
| 43 | c3 | p043c3 | 1 | TP Pangle/CSJ | MediaDataSource for CSJ media download-and-play (wait(5) ×4000 busy-wait) | C3838a |
| 44 | vr | p611vr | 3 | TP Retrofit + Moshi + Fyber | Retrofit RequestBodyConverter / ResponseBodyConverter adapters (UTF-8 BOM "EFBBBF" skip) | C21202a, C21203b, C21204c |
| 45 | kk | p389kk | 2 | TP ZXing | GenericGF (GF(2^k) tables for 4 predefined polys) + GenericGFPoly (Reed–Solomon) | C17852a, C17853b |
| 46 | n1 | p427n1 | 4 | TP Singular | Event(eventType, eventProperties, userProperties, groups, groupProperties) + 512-slot ArrayBlockingQueue ingest buffer | C18690a, C18691b |
| 47 | t8 | p557t8 | 2 | TP Facebook | mDNS/NSD registration of fbsdk_android-18.0.3_<hash> _fb._tcp. on port 80 (see §3.5) | C20364a, C20365b |
| 48 | d3 | p234d3 | 1 | TP Pangle/CSJ | Video cache file handler (RandomAccessFile("rw"), .tmp → final rename, RANGE resume) | C15799a |
| 49 | an | p016an | 2 | TP Ktor | RandomAccessFile-backed suspending writer (setLength(remaining)) | AbstractC0232a, C0233b |
| 50 | op | p465op | 2 | TP Android (android.graphics) | Pair-equivalent value class with Externalizable proxy (UUID-like formatting) | C19220a, C19221b |
| 51 | xg | p656xg | 3 | TP Firebase Crashlytics | Crashlytics component factory ("clx"/"crash" store lookups) + component lookup holder | C21760a, C21761b |
| 52 | ih | p347ih | 6 | TP Firebase Crashlytics | Interleaved-string constants (Crashlytics batchlog URL + API key, §2.2) + internal metric throttle sampler | C17303a, C17304b |
| 53 | rn | p531rn | 5 | TP RxJava2 | Main-thread Scheduler (Handler-backed) + Disposable | C19964c, C19966e, RunnableC19965d |
| 54 | nb | p435nb | 5 | TP 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 |
| 55 | h8 | p316h8 | 2 | TP Facebook | App-Events “deprecated event / deprecated_param” remote-config applier | C16951b |
| 56 | os | p468os | 2 | TP Moloco | AdError constants (0 “SDK is not initialized” … 5 “Internal error”) + impression-URL / request counters | AbstractC19281a, AbstractC19282b |
| 57 | k9 | p383k9 | 2 | TP Fyber FairBid | BannerView load/show synthetic dispatchers (SettableFuture.Listener) | C17795b, RunnableC17794a |
| 58 | r8 | p519r8 | 2 | TP Facebook | App-Events “restrictive_param” remote-config applier | C19894b |
| 59 | s9 | p545s9 | 2 | TP Fyber FairBid | 30-arm synthetic dispatcher over C9501d + MediationManager accessors | C20083a, C20086d |
| 60 | ab | p012ab | 2 | TP Fyber FairBid | FairBid SDK config string accessor (C9901tb) + "Fyber"-tagged threaded logger | C0098a, C0099b |
| 61 | hb | p319hb | 4 | TP InMobi | Bidding AdMob adapters — pass getBidResponse().getBytes() straight to InMobi*.load() | C16956a…C16959d |
| 62 | u3 | p571u3 | 3 | MERGE (TP) | Cron-field typed values (Integer / EnumC20575b) shared with s3 | C20574a, C20576c, EnumC20575b |
| 63 | bk | p037bk | 2 | TP libphonenumber | Metadata resource reader — getResourceAsStream + ObjectInputStream.readExternal() on /com/google/i18n/phonenumbers/data/*.Proto | C3581a, C3582b |
| 64 | p4 | p473p4 | 3 | TP Amazon APS | DTBAdCallback → AdManagerAdRequest param injection; 320×50 / 300×250 slot table | C19293a, C19294b, AbstractC19295c |
| 65 | gk | p309gk | 2 | TP ZXing | Encoder hint enum (ERROR_CORRECTION … GS1_FORMAT) | EnumC16771a, InterfaceC16772b |
| 66 | q | p487q | 1 | TP Glide | Model loaders for byte[], file://, resource-id Uri, android.resource://…, String | C19511a |
| 67 | yn | p689yn | 3 | TP RxJava2 | Disposables (EmptyDisposable, DisposedDisposable) + OnErrorNotImplementedException sink | AbstractC21960b, C21961c |
| 68 | j2 | p356j2 | 3 | TP Glide | ByteBuffer → byte[] transcoder with ArrayBuffer fast path | C17479b, C17480c |
| 69 | s2 | p538s2 | 8 | TP Glide | Pools.Pool / FactoryPools (ArrayPool pool of pools, ObjectPool release guard) | C20029d, AbstractC20032g, C20033h |
| 70 | oh | p460oh | 6 | TP FIAM / protobuf | FieldDescriptor{name,properties} + the 6-method field-setter builder interface | C19133b, InterfaceC19135d |
| 71 | m2 | p416m2 | 4 | TP Glide | Decode-options Registry data objects (ArrayMap keyed on 3-type tuple) | C18481a…C18484d |
| 73 | t0 | p550t0 | 2 | TP Amplitude | FragmentLifecycleCallbacks auto-track "[Amplitude] Fragment Viewed" (class, id, screen name, tag) | C20305a |
| 74 | hp | p330hp | 3 | TP Kotlin stdlib | ObservableProperty / ReadWriteProperty (beforeChange/afterChange hooks) | AbstractC17097a |
| 75 | p0 | p470p0 | 2 | TP Amplitude | Identity(userId, deviceId, userProperties) + identity storage singleton | C19287b, C19286a |
| 76 | t2 | p552t2 | 3 | TP Pangle/CSJ | Video-config singleton ("v_config", 10 s timeouts) + download tracker interfaces | AbstractC20311a, InterfaceC20312b |
| 77 | ed | p266ed | 1 | TP ExoPlayer | IcyInfo metadata parser ((.+?)='(.*?)';, streamurl/streamtitle) | C16179a |
| 78 | i4 | p337i4 | 1 | TP Digital Turbine Ignite | BroadcastReceiver for com.dt.ignite.service.action.PROPERTY_CHANGED watching the DTID property | C17150a |
| 79 | y3 | p670y3 | 2 | TP Digital Turbine Ignite | IDTAuthenticator wrapper — authenticate(), getOdt(), isAuthenticated(), isConnected() | C21842a, C21843b |
| 80 | m3 | p417m3 | 2 | TP Joda-Time | DayOfWeek arithmetic (7-day and 6-day variants) | C18486b |
| 81 | qi | p502qi | 1 | TP Koin/DI | Qualified Executor provider lookup (Qualified.qualifiea, Executor::class.java) | C19727a` | |
| 82 | xa | p650xa | 1 | TP Fyber FairBid | Install-reporter (InstallReporter) posting a C9284cv request in a new Thread | AbstractC21677a |
| 83 | c4 | p044c4 | 3 | TP Digital Turbine Ignite | IgniteEventDispatcherWrapper + error enums 1100-1106 (ENCRYPTION_EXCEPTION, ONE_DT_*) | C3839a, EnumC3840b, EnumC3841c |
| 84 | ym | p688ym | 4 | TP OkHttp | RequestBody/TextContent(text, contentType) with byte-length + charset | AbstractC21956b, C21958d |
| 85 | yk | p686yk | 1 | TP Moloco / AdMob | OnPaidEventListener bridge — forwards AdValue CPM to Moloco with ca-app-pub / GAM source tagging | C21941a |
| 86 | z2 | p698z2 | 7 | TP Pangle/CSJ | Video player control + download-manager interfaces | InterfaceC22060a…InterfaceC22066g |
| 87 | y6 | p673y6 | 1 | TP OkHttp | BasicNameValuePair(name, value) | C21872a |
| 88 | fm | fm | 1 | TP OkHttp | Host/port resolution (http→80, https→443) + hex char parser | C16655a |
| 89 | hc | p320hc | 1 | MERGE (TP) | Priority→int mapper shared by androidx.transition, Facebook AppEvents and Google DataTransport | AbstractC16960a |
| 90 | z8 | p704z8 | 1 | TP Mintegral | error_log_<epochSeconds>.json writer ({"timestamp":…,"error_message":…}) | C22130a |
| 91 | f2 | p280f2 | 1 | TP Glide | DataFetcher factory for ByteBuffer / ParcelFileDescriptor (one arm is UnsupportedOperationException("Not implemented")) | C16413a |
| 92 | pi | p482pi | 1 | TP Firebase common | firebase_data_collection_default_enabled resolver on device-protected storage (§3.2) | C19354a |
| 93 | wi | p631wi | 1 | TP Firebase (Dagger) | Provider for the Firebase C19129g app context / trace / clock deps | C21434a |
| 94 | oe | p457oe | 1 | TP Google Material | MaterialColors helpers (calculateLuminance > 0.5, compositeColors) | AbstractC19121a |
| 95 | i9 | p342i9 | 1 | TP Fyber + Ktor | APSAdapter request builder + a Ktor HttpClient call on a background dispatcher | C17198a |
| 96 | wq | p638wq | 1 | TP Ktor | WindowCounter(streamId,total,acknowledged,unacknowledged) flow-control window | C21492a |
| 97 | u8 | p576u8 | 1 | TP Amplitude | GateKeeper(name, value) feature flag model | C20706a |
| 98 | x8 | p649x8 | 1 | TP Amplitude | UncaughtExceptionHandler that walks the cause chain looking for a fatal marker and reports it | C21676a |
| 99 | if | p345if | 1 | TP Google Play Core / Install Referrer | Play error-code→message maps (ERROR_APP_NOT_OWNED, ERROR_INSTALL_NOT_ALLOWED, ERROR_PLAY_STORE_NOT_FOUND) | AbstractC17293a |
| 100 | p9 | p478p9 | 1 | TP Fyber FairBid | OnScreenAdTracker SettableFuture.Listener dispatcher | C19330b |
| 101 | v2 | p594v2 | 1 | TP Amplitude | MD5 hex digest helper (shared MessageDigest, uppercase hex) — weak hash, used for cache/ids | AbstractC20864a |
| 102 | ul | p585ul | 1 | TP Firebase Perf (Dagger) | Double-check scoped Provider (FirebasePerformanceRegistrar) | C20750a |
| 103 | z0 | p696z0 | 1 | TP Amplitude Session Replay | InternalOptions override: pack=true, transform=true, logPayloads=false, maxStorageBytes=10 MB (§2.6) | C22053a |
| 104 | en | p273en | 1 | TP Kotlin reflect | TypeInfo(KClass, KType?) | C16301a |
| 105 | g3 | p301g3 | 1 | TP Mintegral | Progress %, hh:mm:ss formatter, immersive setSystemUiVisibility(3846) | AbstractC16696a |
| 106 | ff | p291ff | 1 | TP Google Material | materialThemeOverlay / ContextThemeWrapper resolution | AbstractC16538a |
| 107 | ef | p267ef | 1 | TP Google Material | MaterialTextView textAppearanceLineHeight support | C16183a |
| 108 | b0 | p027b0 | 1 | TP Glide | Per-request resource cache (Callback+base-URL+extras → bitmap) | C3399a |
| 109 | xe | p654xe | 1 | TP Google Material | ElevationOverlayProvider state colour/alpha helpers (state_pressed, state_focused, …) | AbstractC21755a |
| 110 | i5 | p338i5 | 1 | FP MICO | PaidInappProductsCache — premium SKU set in prefs SP_KEY_INAPP_PRODUCTS_CACHE (§2.1) | C17151a |
| 111 | q2 | p490q2 | 2 | TP Glide | EmptySignature + ObjectKey{object} (MessageDigest cache key) | C19564a, C19565b |
| 112 | zb | p706zb | 2 | TP Dagger/ComponentRuntime | DoubleCheck scoped Provider | C22133a |
| 113 | d1 | p232d1 | 2 | TP Glide | Log-level enum DEBUG/INFO/WARN/ERROR/OFF + Logger interface | EnumC15796a, InterfaceC15797b |
| 114 | v4 | p596v4 | 5 | TP Mintegral | Error classes: code 8 "~App Id Missing~", 101, 3 "~No Fill~" | AbstractC20871e, C20867a, C20870d |
| 115 | yh | p683yh | 1 | TP Dagger/ComponentRuntime | DoubleCheck scoped Provider | C21935a |
| 116 | f1 | p279f1 | 1 | TP Glide | DefaultLogLogger (writes to System.out, level INFO) | C16412a |
| 117 | g4 | p302g4 | 2 | TP Digital Turbine Ignite | Logger facade (d/w/i/e) dispatching to InterfaceC16697a | C16698b |
| 118 | p | p469p | 3 | TP Glide | Cache-key encoders — File path (+lastModified), Uri (+uiMode & 48) | C19283a, C19285c |
| 119 | b9 | p036b9 | 3 | TP Facebook | AIDL stub for com.facebook.ppml.receiver.IReceiverService (transact(1, Bundle) -> int) (§2.7) | AbstractBinderC3527b, C3526a |
| 120 | xn | p661xn | 8 | TP RxJava3/okio | Queue / SpscLinkedArrayQueue / LockFreeLinkedQueue / Callable marker interfaces | InterfaceC21796h, InterfaceC21793e |
| 121 | on | p463on | 3 | TP OpenTelemetry | ContextManager with reflective io.opentelemetry.opencensusshim.OpenTelemetryContextManager load | AbstractC19216a |
| 122 | wh | p630wh | 2 | TP Firebase FIAM (Dagger) | DoubleCheck scoped Provider for In-App Messaging components | C21432a |
| 123 | xq | p664xq | 2 | TP Fyber FairBid | Punycode/IDN encode-decode ("xn--", 36-ary digit alphabet, TypedValues.TransitionType.TYPE_DURATION reuse) | AbstractC21811a, AbstractC21812b |
| 124 | qe | p499qe | 3 | TP AndroidX/Material | Outline.setConvexPath/setPath shims + Drawable → ColorStateList (SDK 29 gated) | AbstractC19717a…AbstractC19719c |
| 125 | rr | p534rr | 2 | TP RxJava2 | SerializedExecutor / ExecutorWorker | C20003a, C20004b |
| 126 | jn | p371jn | 1 | TP Facebook | App-Events HTTP header provider — hardcodes X-Cloud-Trace-Context | C17668a |
| 127 | ah | p013ah | 2 | TP Firebase Crashlytics | No-op session-signal reporter used before the Crashlytics component is ready | C0137b |
| 128 | hj | p327hj | 1 | TP androidx.startup | InitProvider base (wall-clock / elapsed / uptime clocks, AtomicBoolean) | AbstractC17075a |
| 129 | jg | p366jg | 1 | MERGE (TP) | RoundingMode.ordinal() switch-map (BigDecimal / Moshi shared) | AbstractC17625a |
| 130 | b4 | p031b4 | 1 | TP Facebook | App Links classification: "Applink" / "Unclassified" | C3459a |
| 131 | v | p591v | 1 | TP Glide | Unit-type singleton (Size.class-style token) | C20845a |
| 132 | u9 | p577u9 | 1 | TP Fyber FairBid | MediationManager accessor dispatcher | C20707a |
| 133 | j4 | p357j4 | 2 | TP Digital Turbine / Mintegral | Shared 1-2 thread pool (queue 5, 30 s keepalive) + main-looper Handler; RejectedExecutionHandler is a no-op | AbstractC17482b, RejectedExecutionHandlerC17481a |
| 134 | sf | p546sf | 2 | TP google-api-client | ArrayList batch-request holder + Logger | C20116b, InterfaceC20115a |
| 135 | h9 | p317h9 | 2 | TP Fyber FairBid | InterfaceC9469hw impls: (int,String) and (double,String,String,String,boolean) — bid/revenue records | C16953a, C16954b |
| 136 | y8 | p675y8 | 1 | TP Facebook | Soft-exception collector: WeakHashMap guard set + report on first exception (f59044b toggle) | AbstractC21888a |
| 137 | o2 | p446o2 | 1 | TP Glide | DrawableTransition base (SIZE_ORIGINAL sentinel, no-op lifecycle) | AbstractC19046a |
| 138 | x2 | p643x2 | 2 | TP Pangle/ByteDance | CacheDirFactory scan result (File[] files, int count) | C21596a |
| 139 | pd | p481pd | 4 | TP ExoPlayer2 UI | StyledPlayerView/SubtitleView helper constants (merged with a marker interface) | C19345a, C19347c, C19348d |
| 140 | hs | p332hs | 1 | TP Moloco | Ad view state enum LOADING / DEFAULT / HIDDEN | EnumC17125a |
| 141 | i | p333i | 1 | DEAD | SupportSQLiteDatabase exec/isOpen stub — both bodies are throw new UnsupportedOperationException() / return false (§3.2) | AbstractC17126a |
| 142 | fp | p295fp | 6 | TP Kotlin stdlib | kotlin.collections.* marker interface hierarchy hoisted out of kotlin/ (consumed by androidx.collection, kotlinx, Ktor zm, Glide) | InterfaceC16658a…InterfaceC16663f |
| 143 | un | p586un | 4 | TP Kotlin stdlib | Runnable/Consumer/Function/Predicate functional interfaces hoisted out of kotlin.jvm.functions | InterfaceC20751a…InterfaceC20754d |
| 144 | ei | p269ei | 1 | TP Glide | Pre-draw measured-height helper (handles ScrollView child height + padding) | C16276a |
| 145 | xr | p665xr | 3 | TP Mintegral | Resource-id constants (ad_attr, fsn_layout_template, countdown_seconds, banner_ad_size_txt, …) — R-class fragment | AbstractC21813a…AbstractC21815c |
| 146 | ap | p025ap | 1 | TP util | Reflective Build$VERSION.SDK_INT holder (duplicate of zo) | AbstractC3242a |
| 147 | u2 | p570u2 | 3 | TP Pangle/CSJ | Download-progress listener interfaces (psq/xoo with (AbstractC4358a,int[,String])) | InterfaceC20571a, AbstractC20573c |
| 148 | qg | p500qg | 1 | TP Firebase (Dagger) | Memoising Map<String, C19352b> factory over a Context + provider | C19720a |
| 149 | jq | p372jq | 3 | TP kotlinx.serialization | SerializersModuleBuilder — no-op contextual(KClass, List) registration | AbstractC17678b, C17677a |
| 150 | oa | p453oa | 1 | TP Fyber FairBid test suite | EventBus Handler.Callback pump | C19104a |
| 151 | n6 | p431n6 | 1 | TP Moloco | Activity-scoped Closeable that closes the ad activity by string key | C18710a |
| 152 | yo | p690yo | 1 | TP Kotlin stdlib | Reflective lookup of Throwable.addSuppressed(Throwable) | AbstractC21962a |
| 153 | l0 | p395l0 | 1 | DEAD (TP Amazon APS) | All four ApsAdListener forwarders have empty bodies | AbstractC18269a |
| 154 | r3 | p514r3 | 2 | TP Joda-Time | Cron field/parser data holder ("CronFieldName must not be null"), merged with a Facebook context object | C19828a, C19829b |
| 155 | i8 | p341i8 | 1 | TP Facebook | App-Events sampling gate: fires only when random <= 1e-4 and the event name contains "gps" | C17197a |
| 156 | ak | p015ak | 1 | TP libphonenumber | Metadata bootstrap: /com/google/i18n/phonenumbers/data/{PhoneNumber,ShortNumber,PhoneNumberAlternateFormats}Proto | C0154a |
| 157 | l8 | p400l8 | 2 | TP Firebase/GMS | GpsTopicsManager (Google-Play-services topics, not GPS) + cached-thread-pool provider | AbstractC18317b, C18316a |
| 158 | f9 | p286f9 | 1 | TP Fyber FairBid | Ad-format enum OFFER_WALL("ofw") / UNKNOWN("unknown") | EnumC16513a |
| 159 | yp | p691yp | 1 | TP util | Direct (same-thread) Executor | ExecutorC21963a |
| 160 | d2 | p233d2 | 1 | DEAD (TP Glide) | Pass-through ResourceEncoder — empty MessageDigest update + return interfaceC21595z unchanged | C15798a |
| 161 | kj | p388kj | 1 | DEAD (TP FIAM) | Synthetic Runnable whose two switch arms call the same method with the same args | RunnableC17851a |
| 162 | e4 | p259e4 | 2 | TP Digital Turbine Ignite | Service/credential callback interfaces (onIgniteServiceAuthenticated, onOdtUnsupported, onCredentialsRequestSuccess) | InterfaceC16096a, InterfaceC16097b |
| 163 | mf | p423mf | 1 | TP Google Play In-App Review | Error maps: "Call first requestReviewFlow to get the ReviewInfo.", PLAY_STORE_NOT_FOUND, INTERNAL_ERROR | AbstractC18588a |
| 164 | zo | p718zo | 1 | TP util | Reflective Build$VERSION.SDK_INT holder (duplicate of ap) | AbstractC22295a |
| 165 | qm | p504qm | 1 | TP (owner unresolved) | "SSECapability" capability singleton; referenced only from p440nm | C19733a |
| 166 | zj | p714zj | 1 | TP Facebook | Access-ordered LinkedHashMap LRU (removeEldestEntry on AppEvents cache limit) | C22238a |
| 167 | yf | p681yf | 1 | TP google-api-client | Singleton placeholder for the API-client credential factory | AbstractC21930a |
| 168 | f4 | p281f4 | 2 | TP Fyber Inneractive / Ignite | 2-method lifecycle marker (mo14085a(), mo14086a(String)) | InterfaceC16414a |
| 169 | oo | p464oo | 1 | MERGE (TP) | Dagger Provider<T> { Object get(); } hoisted to its own package (21 consumers) | InterfaceC19219a |
| 170 | lh | p405lh | 1 | DEAD (TP Firebase) | Empty marker interface referenced only by FirebaseCrashlyticsKtxRegistrar | C18377a |
| 171 | b | p026b | 1 | TP Amplitude Session Replay | View→windowAttachCount shim for replay target discovery | AbstractC3398a |
| 172 | gc | p307gc | 1 | MERGE (TP) | Clock (long mo32569c()) shared by Glide’s Timer/SystemClock and Google DataTransport | InterfaceC16728a |
| 173 | ns | p442ns | 1 | DEAD (TP) | Empty marker interface; consumers are Moloco AdDataNative and mbridge th | InterfaceC18919a |
| 174 | f | p277f | 1 | DEAD | abstract /* synthetic */ class with a single f5…c field and no methods, no importers anywhere in the APK | AbstractC16400a |
| 175 | o3 | p447o3 | 1 | DEAD (TP) | Empty marker, single importer (p511r0, the Crashlytics component registry) | AbstractC19047a |
| 176 | nh | p437nh | 1 | DEAD (TP Pangle) | Empty marker, single importer (vg) | InterfaceC18806a |
| 177 | dp | p250dp | 1 | TP Kotlin reflect | KotlinReflectionNotFoundError ("kotlin-reflect.jar is not in the classpath") | C15998a |
| 178 | uh | p582uh | 1 | DEAD (TP FCM) | Empty marker, single importer (FirebaseMessagingRegistrar) | InterfaceC20741a |
| 179 | ii | p348ii | 1 | DEAD (TP FIAM) | Empty class, importers are FirebaseInAppMessagingRegistrar, fi, gi, p326hi | C17305a |
| 180 | tb | p558tb | 1 | DEAD (TP) | Empty abstract class; single importer be (an AIDL Binder base) | AbstractC20366a |
| 181 | n9 | p434n9 | 1 | DEAD (TP Fyber) | m35090a(BannerWrapper) with an empty body | AbstractC18747a |
| 182 | lj | p406lj | 1 | DEAD (TP Firebase) | Empty marker; importers CrashlyticsRegistrar, RemoteConfigRegistrar, p349ij | InterfaceC18388a |
| 183 | zg | p711zg | 2 | TP Firebase Crashlytics | SessionCallbacks (onEvent(String, Bundle)) + session-signals provider | InterfaceC22216a, InterfaceC22217b |
| 184 | fc | p288fc | 2 | TP util | Callable-like execute() marker + empty interface | InterfaceC16528a, InterfaceC16529b |
| 185 | d4 | p235d4 | 1 | TP Digital Turbine Ignite | Ignite service-API triple (auth, BinderC22068b callback, IIgniteServiceAPI) | C15800a |
| 186 | rf | p523rf | 1 | DEAD (TP google-api-client) | GoogleTokenResponse base with no body | C19909a |
| 187 | x3 | p644x3 | 1 | TP Firebase Crashlytics | Cron field + clock + thread triple holder for session signals | C21598a |
| 188 | of | p458of | 1 | DEAD (TP google-api-client) | Empty AbstractC19122a extends com.google.api.client.util.Data — OIDC IdTokenResponse base | AbstractC19122a |
| 189 | lm | p408lm | 1 | DEAD (TP Joda-Time) | Empty marker; single importer om | InterfaceC18400a |
| 190 | y2 | p669y2 | 1 | DEAD (TP Pangle) | Empty marker; importers are bytedance.sdk.openadsdk.component.reward.* | InterfaceC21841a |
| 191 | rl | p529rl | 1 | DEAD (TP uCrop) | Empty 3-method interface implemented by yalantis.ucrop.view.widget.HorizontalProgressWheelView | InterfaceC19952a |
| 192 | ue | p581ue | 2 | TP AndroidX | Empty FrameLayout base + abstract view base | AbstractC20735a, AbstractC20736b |
| 193 | p2 | p471p2 | 2 | DEAD (TP Glide) | Empty singleton pair (C19288a/C19289b); importers bumptech.glide.C4204a, C4206c | C19288a, 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 pkg | Disk | Files | Why unresolved | Risk of being wrong |
|---|---|---|---|---|
qm | p504qm | 1 | C19733a 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. |
sf | p546sf | 2 | Attributed 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. |
tb | p558tb | 1 | AbstractC20366a 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 arebm,p029b2(Google Measurement errors),p035b8,p472p3,p511r0,p595v3,p617w3andcom\facebook\C9067e, i.e. R8 collapsed several SDKs’ cron models into one shared package. Treat the “MERGE” label as the reliable part.hc— thePriority→int mapper is genuinely shared by androidx.transition, Facebook AppEvents and Google DataTransport (EnumC20713dis Amplitude’sDEFAULT/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
| Package | What is dead | Evidence |
|---|---|---|
i (p333i) | Both methods of AbstractC17126a — execSQL throws UnsupportedOperationException(), isOpen returns false | class 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 APK | grep for import p277f. returns zero hits |
d2 (p233d2) | Glide ResourceEncoder whose MessageDigest update is empty and whose getData() returns the input unchanged | empty mo3591b(MessageDigest) |
kj (p388kj) | RunnableC17851a — both switch arms call the identical method with identical args | case 0: and default: are byte-identical |
p2 (p471p2) | Empty singleton pair | 2 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, p2 | empty class / empty interface with only marker role | 13 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, y2 | marker 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()returnsnew ArrayList(0)— the AOSP cornermark badge adapter’s “supported launchers” list is empty, so this adapter can never be selected by ShortcutBadger and itsContentResolver.call(content://com.android.launcher3.cornermark.unreadbadge, …)is unreachable. The other three adapters (OPPO / vivo / ZUI) are live.f2/C16413a.mo31905b()hascase 1: throw new UnsupportedOperationException("Not implemented").a2/C0026acase 2andcase 0contain emptytry {}blocks (GlideExecutor,ExoPlayer:SimpleDecoder).j4/RejectedExecutionHandlerC17481a.rejectedExecution()— silently drops every rejected task.wr(Retrofit annotations) andb9/InterfaceC3528care interface-only packages: zero executable code.gg-style hoisting produced 30+ packages whose entire content isequals/hashCode/toStringon 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:
| Package | Merged from |
|---|---|
a2 | Glide GlideExecutor, Coil, ExoPlayer SimpleDecoder, Amplitude timeline, androidx.work |
s3, u3 | ≥4 SDKs’ cron/period models (Google Measurement, Firebase, Facebook, WorkManager) |
hc | Amplitude + androidx.transition + Facebook AppEvents + DataTransport priority enum |
oo | Dagger Provider (21 consumers across the whole APK) |
gc | Glide Timer/SystemClock + Google DataTransport Clock |
fp, un | Kotlin stdlib kotlin.collections.* / kotlin.jvm.functions.* (imported by androidx.*, kotlinx.*, Ktor) |
jg | RoundingMode.ordinal() switch-map shared by BigDecimal and Moshi code |
lq | ShortcutBadger, with MICO’s MyApplication substituted for the library’s ShortcutBadgeApp parameter |
d0 | Lottie path keyframes + a Facebook holder in one class |
ap, zo | Duplicate 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 oflq). - ~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 finding | Confirmed / extended in C6b? |
|---|---|
Blowfish/ECB contact-photo crypto, cipher_key in prefs, ObjectInputStream.readObject() over decryptable file bytes | Not 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, shouldSendNameToServer | Not 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 string | Not in C6b. No SMS/call-log code at all (§3.3). |
| Premium entitlement 100 % client-side, no Google purchase verification | Extended: 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 = ExoPlayer | Extended: 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 mediation | Confirmed 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.