md2link

Art Keyboard — Google Play Malware Policy Remediation Brief

DraftJun 29, 2026

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

  1. Extracted the XAPK bundle (base.apk + locale/abi/dpi config splits).
  2. Decompiled base.apk with jadx to recover Java pseudo-code and the merged AndroidManifest.xml.
  3. 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.
  4. 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|flagIncludeNotImportantViews grants the app the ability to read the content of every other app's UI — including banking apps, password managers, messaging apps, OTP screens.
  • The packageNames attribute targets com.controlcenter.tool.iphonea 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 IsAccessibilityTool rule 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.ScreenshotActivity
  • org.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 NotificationListenerService as 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)