Art Keyboard — Google Play Malware Policy Remediation Brief
Document for partner / development team handoff
| Field | Value |
|---|---|
| App name | Keyboard Themes - Fonts, Emoji ("Art Keyboard") |
| Package ID | com.artkeyboard.aikeyboard.fonts.keyboard |
| Developer account | AppLock Inc. (myappszone222@gmail.com) |
| Play Console status | Suspended (Malware policy) |
| Flagged version code | 21 (per Google notice dated 28/06/2026) |
| Analyzed artifact | XAPK v23.0 (versionCode 23), 80.3 MB, from APKPure |
| Analysis date | 29/06/2026 |
| Internal namespace in code | org.keyboard.photokeyboard.inputmethod.* (originally a "Photo Keyboard" base project) |
1. Executive summary
Google Play suspended Art Keyboard under the Malware policy. After decompiling the published APK we confirmed the suspension is technically justified: the app declares and contains code for many capabilities that have no relation to an Input Method Editor (IME) — including an Accessibility Service with maximum privileges, a Notification Listener, MediaProjection-based screen capture/recording, a phone dialer replacement (InCallService + CallScreeningService), and a contacts/app-lock framework copied from a separate "AppLock"-style codebase (com.goodwy.commons).
The combination of these capabilities (AccessibilityService + NotificationListener + MediaProjection + RECORD_AUDIO + READ_CONTACTS + SYSTEM_ALERT_WINDOW) matches the static signature Google Play uses to identify stalkerware / spyware, regardless of developer intent. To recover the app on Play Store, every component listed in Section 5 must be fully removed from the APK (not just disabled at runtime or hidden behind remote config) and the permission list trimmed to the IME-essential set in Section 7.
2. Google Play notice — what was reported
From Google Play Console email (28/06/2026, 07:52):
"Your app is not compliant with the Malware policy. We don't allow apps with any code that could put a user, a user's data, or a device at risk."
"Issue details — We found an issue in the following area(s): Version code 21"
"Further policy violations may lead to your Google Play Developer account and any other related accounts being terminated."
3. Methodology
- Extracted the XAPK bundle (
base.apk+ locale/abi/dpi config splits). - Decompiled
base.apkwith jadx to recover Java pseudo-code and the mergedAndroidManifest.xml. - Static scan of:
- Manifest permissions, services, receivers, providers, queries.
- XML resources (
res/xml/accessibilityservice.xml, etc.). - Java source folders (~20,103 classes) for sensitive APIs:
MediaProjection,AccessibilityService,NotificationListenerService,InCallService,CallScreeningService,MediaProjectionManager,SmsManager,ContactsContract, dynamic loaders.
- Cross-checked findings against Google Play Malware policy, Permissions and APIs that Access Sensitive Information, and Default Handlers guidance.
This is a black-box static analysis of the published APK. We did not have source access; the findings are based on declarations and decompiled code only. They reflect what Google Play's review pipeline can also see.
4. Root cause hypothesis — base project inherited from AppLock
The developer namespace inside the APK is org.keyboard.photokeyboard.inputmethod.* (a generic "Photo Keyboard" base), but it is bundled together with classes from com.goodwy.commons.* (a known Android utility framework used in dialer/contacts/SMS/voice-recorder applications) and a full self-contained iPhone-style "Control Center" module under org.keyboard.photokeyboard.inputmethod.ui.controlcenter.*.
The most likely explanation — consistent with the developer account being AppLock Inc. — is that the team forked an existing base project (likely their AppLock / Control Center / Dialer template) and added IME functionality on top, without stripping the original modules. As a result, the Keyboard APK ships with:
- AppLock features (system locker, AccessibilityService)
- Control-Center features (screen capture, media control, overlay)
- Dialer / Caller features (InCallService, CallScreeningService, contacts)
- Voice recorder hooks (RECORD_AUDIO + FOREGROUND_SERVICE_MICROPHONE)
- Live wallpapers × 4
- The actual IME
Google's reviewer (human or automated) sees one app with all of the above and concludes "this is a surveillance-capable keyboard" — a strong Malware policy trigger.
5. Findings — components that MUST be removed
Each row identifies a real, declared component in the APK with its evidence and the policy concern.
5.1 AccessibilityService with maximum privileges — CRITICAL
Evidence — res/xml/accessibilityservice.xml:
<accessibility-service
android:accessibilityEventTypes="typeAllMask"
android:packageNames="com.controlcenter.tool.iphone"
android:accessibilityFeedbackType="feedbackAllMask"
android:notificationTimeout="5"
android:accessibilityFlags="flagRetrieveInteractiveWindows|flagIncludeNotImportantViews"
android:canRetrieveWindowContent="true"/>
Implementation: org.keyboard.photokeyboard.inputmethod.ui.controlcenter.services.ServiceControl extends android.accessibilityservice.AccessibilityService (551 lines).
Why this triggers Malware policy:
typeAllMask+canRetrieveWindowContent=true+flagRetrieveInteractiveWindows|flagIncludeNotImportantViewsgrants the app the ability to read the content of every other app's UI — including banking apps, password managers, messaging apps, OTP screens.- The
packageNamesattribute targetscom.controlcenter.tool.iphone— a different package, not this app. This is either leftover copy-paste from another project or deliberate automation of a third-party app. Either reading is suspicious to a reviewer. - An IME has no legitimate use for AccessibilityService. Google Play's
IsAccessibilityToolrule restricts AccessibilityService to apps whose primary function is accessibility.
Action: Delete ServiceControl.java, delete res/xml/accessibilityservice.xml, remove the <service> block from AndroidManifest.xml, drop the BIND_ACCESSIBILITY_SERVICE permission.
5.2 MediaProjection screen capture / screen recorder — CRITICAL
Evidence — ServiceScreen.java:
public class ServiceScreen extends android.app.Service {
private MediaProjection mediaProjection;
private MediaProjectionManager mediaProjectionManager;
// ...
mediaProjectionManager.getMediaProjection(-1, (Intent) f59059n.clone());
// ...
startForeground(12222, notificationBuild, 32); // FOREGROUND_SERVICE_TYPE_MEDIA_PROJECTION
}
Supporting components:
org.keyboard.photokeyboard.inputmethod.ui.controlcenter.ui.ScreenshotActivityorg.keyboard.photokeyboard.inputmethod.ui.controlcenter.ui.RecordSettingActivity- Third-party library
com.hbisoft.hbrecorder.*(HBRecorder — screen-recording library) - System overlay window using
WindowManager.LayoutParams.type = 2038(TYPE_APPLICATION_OVERLAY)
Why this triggers Malware policy: An IME has no use case that requires capturing the entire device screen. Combined with the Accessibility Service this is the classic stalkerware pattern.
Action: Delete the entire controlcenter package, delete ScreenRecordService and HBRecorder dependency, drop permissions FOREGROUND_SERVICE_MEDIA_PROJECTION, MEDIA_CONTENT_CONTROL, CAPTURE_VIDEO_OUTPUT, SYSTEM_ALERT_WINDOW, ACTION_MANAGE_OVERLAY_PERMISSION.
5.3 NotificationListenerService — CRITICAL
Evidence — AndroidManifest.xml:
<service
android:name=".ui.controlcenter.services.NotificationService"
android:permission="android.permission.BIND_NOTIFICATION_LISTENER_SERVICE">
<intent-filter>
<action android:name="android.service.notification.NotificationListenerService"/>
</intent-filter>
</service>
NotificationService.java is currently a 5-line empty subclass:
public class NotificationService extends android.service.notification.NotificationListenerService { }
Why this triggers Malware policy:
- Even though the implementation is empty, the declaration is enough to request the user grant Notification Access, after which logic can be added in a later update.
- Google Play treats
NotificationListenerServiceas a high-risk permission for non-launcher/non-assistant apps, because it exposes the contents of every notification (OTP codes, banking alerts, private messages).
Action: Delete the <service> declaration entirely. Delete NotificationService.java.
5.4 Phone Dialer replacement (InCallService) — CRITICAL
Evidence — AndroidManifest.xml:
<service android:name=".ui.caller.service.CallService"
android:permission="android.permission.BIND_INCALL_SERVICE">
<intent-filter><action android:name="android.telecom.InCallService"/></intent-filter>
</service>
<service android:name=".ui.caller.service.SimpleCallScreeningService"
android:permission="android.permission.BIND_SCREENING_SERVICE">
<intent-filter><action android:name="android.telecom.CallScreeningService"/></intent-filter>
</service>
CallService.java overrides onCallAdded and opens a full-screen CallActivity on every incoming call. There is a full caller-id UI package at org.keyboard.photokeyboard.inputmethod.ui.caller.*.
Why this triggers Malware policy:
- A keyboard advertising itself as "Keyboard / Fonts / Emoji" should not register as the system dialer or call-screening service.
- Possession of dialer permissions (
CALL_PHONE,ANSWER_PHONE_CALLS,MANAGE_OWN_CALLS,READ_PHONE_STATE) is reserved for apps in the dialer category.
Action: Delete the entire ui.caller.* package (services, activities, helpers, receivers). Drop the four phone-related permissions and BIND_INCALL_SERVICE / BIND_SCREENING_SERVICE service declarations.
5.5 Goodwy commons (AppLock / blocked numbers / contacts framework) — HIGH
Evidence — directory tree:
com/goodwy/commons/
├─ activities/AppLockActivity.java
├─ activities/ManageBlockedNumbersActivity.java
├─ activities/ContributorsActivity.java
├─ activities/FAQActivity.java
├─ activities/LicenseActivity.java
├─ receivers/RightBroadcastReceiver.java
├─ models/contacts/...
├─ databases/...
├─ dialogs/...
├─ helpers/...
└─ views/...
Manifest also includes a <queries> block listing other Goodwy apps:
<queries>
<package android:name="com.goodwy.audiobook"/>
<package android:name="com.goodwy.calendar"/>
<package android:name="com.goodwy.contacts"/>
<package android:name="com.goodwy.dialer"/>
<package android:name="com.goodwy.files"/>
<package android:name="com.goodwy.keyboard"/>
<package android:name="com.goodwy.smsmessenger"/>
<package android:name="com.goodwy.voicerecorder"/>
<package android:name="com.goodwy.voicerecorderfree"/>
</queries>
Why this triggers Malware policy / Deceptive Behavior:
- These classes are unrelated to keyboard functionality. They are utility code originally written for AppLock-style and dialer apps.
- The
<queries>block makes the keyboard scan whether the user has any of nine other apps installed — behavior typical of cross-app surveillance / ecosystem-linking apps.
Action: Delete the entire com/goodwy/ directory from the source tree and remove the <queries> block from the manifest.
5.6 Microphone / Camera / GPS / Bluetooth — HIGH
These permissions are declared with no corresponding legitimate IME feature visible in the code:
| Permission | Declared because of | Action |
|---|---|---|
RECORD_AUDIO + FOREGROUND_SERVICE_MICROPHONE |
Inherited from voice-recorder / caller stack | Remove unless app implements an explicit, user-initiated voice-to-text feature with on-screen UI |
CAMERA + CAPTURE_VIDEO_OUTPUT |
No clear use case | Remove |
ACCESS_FINE_LOCATION + ACCESS_COARSE_LOCATION |
No clear use case | Remove |
BLUETOOTH, BLUETOOTH_ADMIN, BLUETOOTH_CONNECT |
No pairing/scanning code | Remove |
USE_FINGERPRINT |
Inherited from AppLock | Remove |
WRITE_SETTINGS |
No system-settings modification needed for IME | Remove |
READ_SYNC_SETTINGS, WRITE_SYNC_SETTINGS |
No sync feature | Remove |
READ_CONTACTS |
Inherited from dialer | Remove |
INSTALL_SHORTCUT |
Optional, low value | Remove |
5.7 Live wallpaper services — MEDIUM (review with product)
Evidence: Four <service> declarations with action android.service.wallpaper.WallpaperService, anchored on org.keyboard.photokeyboard.inputmethod.ui.wallpaper.service.GLWallpaperService. Requires SET_WALLPAPER and <uses-feature android:name="android.software.live_wallpaper"/>.
Why this is borderline: Live wallpapers are a legitimate, well-known Android feature and not inherently policy-violating. However, in a "keyboard" app they expand the surface area of justification needed during review. If live wallpapers are not a meaningful driver of MAU/revenue, removing them simplifies the resubmit story.
Action: Confirm with product whether to keep. If kept, ensure clear in-app entry point, screenshots on Play listing, and Data Safety justification.
6. Component-by-component remediation matrix
| Status | Components (Activities / Services / Resources / Permissions) |
|---|---|
| 🟢 KEEP (core IME) | LatinIME, AndroidSpellCheckerService, DictionaryProvider, all keyboard layout XMLs, SettingsActivity, AppLanguageActivity, KeyboardLanguageActivity, AppThemeActivity, ThemePreviewActivity, FontsActivity, EffectsActivity, SoundActivity, KaomojiActivity, KaomojiPreviewActivity, StickerPreviewActivity, FavouriteActivity, CollectionActivity, SearchActivity, CustomDiyActivity, CropActivity, GalleryActivity (for DIY theme picker), DashboardActivity, SetupActivity, SplashNewActivity, NativeFullSplashActivity, PremiumKeyboardActivity, FeedbackActivity, PolicyActivity, WebViewActivity, OpenActivity, PermissionsActivity, DictionaryPackInstallBroadcastReceiver, SystemBroadcastReceiver, Apero/ad SDK integration |
| 🟡 PRODUCT DECISION | Live wallpaper services (GLWallpaperService × 4) — keep only if business-critical. Voice typing — keep RECORD_AUDIO only if there is a user-visible voice-input button. |
| 🔴 DELETE | controlcenter.services.ServiceControl, controlcenter.services.ServiceScreen, controlcenter.services.NotificationService, controlcenter.ui.ScreenshotActivity, controlcenter.ui.RecordSettingActivity, all of ui.controlcenter.*, ui.caller.service.CallService, ui.caller.service.SimpleCallScreeningService, ui.caller.activity.CallActivity, ui.caller.receviers.CallActionReceiver, all of ui.caller.*, all of com.goodwy.commons.*, third-party lib com.hbisoft.hbrecorder.*, res/xml/accessibilityservice.xml |
7. Target permission set after remediation
The following is the minimum and recommended uses-permission set for an IME with themes, fonts, emoji, stickers, IAP, ads, and FCM.
<!-- IME core -->
<uses-permission android:name="android.permission.VIBRATE"/>
<!-- Network: theme/font/sticker downloads, ads, FCM -->
<uses-permission android:name="android.permission.INTERNET"/>
<uses-permission android:name="android.permission.ACCESS_NETWORK_STATE"/>
<!-- Notifications (Android 13+) for theme/promo updates -->
<uses-permission android:name="android.permission.POST_NOTIFICATIONS"/>
<!-- DIY theme: user-picked image from gallery (Android 13+ scoped) -->
<uses-permission android:name="android.permission.READ_MEDIA_IMAGES"/>
<!-- Wake lock for foreground keyboard input handling -->
<uses-permission android:name="android.permission.WAKE_LOCK"/>
<!-- Billing & licensing -->
<uses-permission android:name="com.android.vending.BILLING"/>
<uses-permission android:name="com.android.vending.CHECK_LICENSE"/>
<!-- FCM (push) and Install Referrer -->
<uses-permission android:name="com.google.android.c2dm.permission.RECEIVE"/>
<uses-permission android:name="com.google.android.finsky.permission.BIND_GET_INSTALL_REFERRER_SERVICE"/>
<!-- Ad SDKs (AdMob / GMA) — these are standard -->
<uses-permission android:name="com.google.android.gms.permission.AD_ID"/>
<uses-permission android:name="android.permission.ACCESS_ADSERVICES_AD_ID"/>
<uses-permission android:name="android.permission.ACCESS_ADSERVICES_ATTRIBUTION"/>
<uses-permission android:name="android.permission.ACCESS_ADSERVICES_TOPICS"/>
<uses-permission android:name="android.permission.ACCESS_ADSERVICES_CUSTOM_AUDIENCE"/>
<!-- OPTIONAL — only if live wallpaper feature is retained -->
<uses-permission android:name="android.permission.SET_WALLPAPER"/>
<uses-permission android:name="android.permission.FOREGROUND_SERVICE"/>
<!-- OPTIONAL — only if real voice typing UI exists -->
<!-- <uses-permission android:name="android.permission.RECORD_AUDIO"/> -->
<!-- <uses-permission android:name="android.permission.FOREGROUND_SERVICE_MICROPHONE"/> -->
Permissions REMOVED relative to v23 (28 items):
RECEIVE_BOOT_COMPLETED (duplicated declaration) ×1
ACCESS_WIFI_STATE, CHANGE_WIFI_STATE, CHANGE_NETWORK_STATE
READ_EXTERNAL_STORAGE, WRITE_EXTERNAL_STORAGE (replaced by READ_MEDIA_IMAGES on SDK 33+)
CAMERA, CAPTURE_VIDEO_OUTPUT
SCHEDULE_EXACT_ALARM, USE_EXACT_ALARM, USE_FULL_SCREEN_INTENT
BLUETOOTH, BLUETOOTH_ADMIN, BLUETOOTH_CONNECT
WRITE_SETTINGS, BIND_ACCESSIBILITY_SERVICE, ACCESS_NOTIFICATION_POLICY
FOREGROUND_SERVICE_MEDIA_PROJECTION, MEDIA_CONTENT_CONTROL
RECORD_AUDIO, FOREGROUND_SERVICE_MICROPHONE (unless voice typing retained)
ACCESS_FINE_LOCATION, ACCESS_COARSE_LOCATION
SYSTEM_ALERT_WINDOW, ACTION_MANAGE_OVERLAY_PERMISSION
FLASHLIGHT
READ_SYNC_SETTINGS, WRITE_SYNC_SETTINGS
CALL_PHONE, READ_CONTACTS, MANAGE_OWN_CALLS, ANSWER_PHONE_CALLS, READ_PHONE_STATE
INSTALL_SHORTCUT, com.goodwy.android.permission.WRITE_GLOBAL_SETTINGS
USE_FINGERPRINT
com.amazon.privacypass.ATTEST
com.samsung.android.mapsagent.permission.READ_APP_INFO
com.huawei.appmarket.service.commondata.permission.GET_COMMON_DATA
8. Service / receiver declarations to delete from manifest
<!-- DELETE -->
<service android:name=".ui.controlcenter.services.ServiceControl">
<intent-filter><action android:name="android.accessibilityservice.AccessibilityService"/></intent-filter>
<meta-data android:name="android.accessibilityservice" android:resource="@xml/accessibilityservice"/>
</service>
<service android:name=".ui.controlcenter.services.ServiceScreen" .../>
<service android:name=".ui.controlcenter.services.NotificationService"
android:permission="android.permission.BIND_NOTIFICATION_LISTENER_SERVICE">
<intent-filter><action android:name="android.service.notification.NotificationListenerService"/></intent-filter>
</service>
<service android:name=".ui.caller.service.CallService"
android:permission="android.permission.BIND_INCALL_SERVICE">
<intent-filter><action android:name="android.telecom.InCallService"/></intent-filter>
</service>
<service android:name=".ui.caller.service.SimpleCallScreeningService"
android:permission="android.permission.BIND_SCREENING_SERVICE">
<intent-filter><action android:name="android.telecom.CallScreeningService"/></intent-filter>
</service>
<receiver android:name=".ui.caller.receviers.CallActionReceiver" .../>
<activity android:name="com.goodwy.commons.activities.AppLockActivity"/>
<activity android:name="com.goodwy.commons.activities.ManageBlockedNumbersActivity"/>
<activity android:name="com.goodwy.commons.activities.ContributorsActivity"/>
<activity android:name="com.goodwy.commons.activities.FAQActivity"/>
<activity android:name="com.goodwy.commons.activities.LicenseActivity"/>
<receiver android:name="com.goodwy.commons.receivers.RightBroadcastReceiver"/>
<!-- DELETE the entire <queries> block listing com.goodwy.* packages -->
XML resource to delete: res/xml/accessibilityservice.xml.
Source folders to delete:
org/keyboard/photokeyboard/inputmethod/ui/controlcenter/(entire tree)org/keyboard/photokeyboard/inputmethod/ui/caller/(entire tree)com/goodwy/(entire tree)com/hbisoft/hbrecorder/(entire tree)